Skip to content

Commit 338ec22

Browse files
authored
Merge pull request #115 from igerber/feature/windows-wheels-faer
Add Windows wheel builds using faer pure Rust linear algebra
2 parents 36b57d0 + 69fde20 commit 338ec22

11 files changed

Lines changed: 456 additions & 204 deletions

File tree

.github/workflows/publish.yml

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
- name: Install dependencies
1717
run: |
18-
dnf install -y openblas-devel openssl-devel perl-IPC-Cmd
18+
dnf install -y openssl-devel perl-IPC-Cmd
1919
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
2020
/opt/python/cp312-cp312/bin/pip install maturin
2121
@@ -54,24 +54,48 @@ jobs:
5454
- name: Install Rust
5555
uses: dtolnay/rust-toolchain@stable
5656

57-
- name: Install OpenBLAS
58-
run: brew install openblas
59-
6057
- name: Install maturin
6158
run: pip install maturin
6259

6360
- name: Build wheel
64-
run: |
65-
export OPENBLAS_DIR=$(brew --prefix openblas)
66-
export PKG_CONFIG_PATH=$(brew --prefix openblas)/lib/pkgconfig
67-
maturin build --release --out dist --features extension-module
61+
run: maturin build --release --out dist --features extension-module
6862

6963
- name: Upload wheels
7064
uses: actions/upload-artifact@v4
7165
with:
7266
name: wheels-macos-arm64-py${{ matrix.python-version }}
7367
path: dist/*.whl
7468

69+
# Build wheels on Windows
70+
build-windows:
71+
name: Build Windows wheels
72+
runs-on: windows-latest
73+
strategy:
74+
matrix:
75+
python-version: ['3.9', '3.10', '3.11', '3.12']
76+
steps:
77+
- uses: actions/checkout@v4
78+
79+
- name: Set up Python
80+
uses: actions/setup-python@v5
81+
with:
82+
python-version: ${{ matrix.python-version }}
83+
84+
- name: Install Rust
85+
uses: dtolnay/rust-toolchain@stable
86+
87+
- name: Install maturin
88+
run: pip install maturin
89+
90+
- name: Build wheel
91+
run: maturin build --release --out dist --features extension-module
92+
93+
- name: Upload wheels
94+
uses: actions/upload-artifact@v4
95+
with:
96+
name: wheels-windows-py${{ matrix.python-version }}
97+
path: dist/*.whl
98+
7599
# Build source distribution
76100
build-sdist:
77101
name: Build source distribution
@@ -97,10 +121,9 @@ jobs:
97121
path: dist/*.tar.gz
98122

99123
# Publish to PyPI
100-
# Windows and macOS x86_64 users install from sdist and get pure Python fallback
101124
publish:
102125
name: Publish to PyPI
103-
needs: [build-linux, build-macos-arm, build-sdist]
126+
needs: [build-linux, build-macos-arm, build-windows, build-sdist]
104127
runs-on: ubuntu-latest
105128
environment: pypi
106129
permissions:

.github/workflows/rust-test.yml

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,23 @@ env:
2222
CARGO_TERM_COLOR: always
2323

2424
jobs:
25-
# Run Rust unit tests
25+
# Run Rust unit tests on all platforms
2626
rust-tests:
27-
name: Rust Unit Tests
28-
runs-on: ubuntu-latest
27+
name: Rust Unit Tests (${{ matrix.os }})
28+
runs-on: ${{ matrix.os }}
29+
env:
30+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: 1
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
os: [ubuntu-latest, macos-latest, windows-latest]
35+
2936
steps:
3037
- uses: actions/checkout@v4
3138

3239
- name: Install Rust toolchain
3340
uses: dtolnay/rust-toolchain@stable
3441

35-
- name: Install OpenBLAS
36-
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
37-
3842
- name: Run Rust tests
3943
working-directory: rust
4044
run: cargo test --verbose
@@ -46,8 +50,7 @@ jobs:
4650
strategy:
4751
fail-fast: false
4852
matrix:
49-
os: [ubuntu-latest, macos-latest]
50-
# Windows excluded due to Intel MKL build complexity
53+
os: [ubuntu-latest, macos-latest, windows-latest]
5154

5255
steps:
5356
- uses: actions/checkout@v4
@@ -57,20 +60,6 @@ jobs:
5760
with:
5861
python-version: '3.11'
5962

60-
- name: Install OpenBLAS (Ubuntu)
61-
if: matrix.os == 'ubuntu-latest'
62-
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
63-
64-
- name: Install OpenBLAS (macOS)
65-
if: matrix.os == 'macos-latest'
66-
run: brew install openblas
67-
68-
- name: Set OpenBLAS paths (macOS)
69-
if: matrix.os == 'macos-latest'
70-
run: |
71-
echo "OPENBLAS_DIR=$(brew --prefix openblas)" >> $GITHUB_ENV
72-
echo "PKG_CONFIG_PATH=$(brew --prefix openblas)/lib/pkgconfig" >> $GITHUB_ENV
73-
7463
- name: Install Rust toolchain
7564
uses: dtolnay/rust-toolchain@stable
7665

@@ -82,28 +71,66 @@ jobs:
8271
pip install maturin
8372
maturin build --release -o dist
8473
echo "=== Built wheels ==="
85-
ls -la dist/
86-
# --no-index ensures we install from local wheel, not PyPI
87-
pip install --no-index --find-links=dist diff-diff
74+
ls -la dist/ || dir dist
75+
shell: bash
76+
77+
- name: Install wheel (Unix)
78+
if: runner.os != 'Windows'
79+
run: pip install --no-index --find-links=dist diff-diff
8880

89-
- name: Verify Rust backend is available
90-
# Run from /tmp to avoid source directory shadowing installed package
81+
- name: Install wheel (Windows)
82+
if: runner.os == 'Windows'
83+
run: |
84+
$wheel = Get-ChildItem dist/*.whl | Select-Object -First 1
85+
pip install $wheel.FullName
86+
shell: pwsh
87+
88+
- name: Verify Rust backend is available (Unix)
89+
if: runner.os != 'Windows'
9190
working-directory: /tmp
9291
run: |
9392
python -c "import diff_diff; print('Location:', diff_diff.__file__)"
9493
python -c "from diff_diff import HAS_RUST_BACKEND; print('HAS_RUST_BACKEND:', HAS_RUST_BACKEND); assert HAS_RUST_BACKEND, 'Rust backend not available'"
9594
96-
- name: Copy tests to isolated location
95+
- name: Verify Rust backend is available (Windows)
96+
if: runner.os == 'Windows'
97+
working-directory: ${{ runner.temp }}
98+
run: |
99+
python -c "import diff_diff; print('Location:', diff_diff.__file__)"
100+
python -c "from diff_diff import HAS_RUST_BACKEND; print('HAS_RUST_BACKEND:', HAS_RUST_BACKEND); assert HAS_RUST_BACKEND, 'Rust backend not available'"
101+
102+
- name: Copy tests to isolated location (Unix)
103+
if: runner.os != 'Windows'
97104
run: cp -r tests /tmp/tests
98105

99-
- name: Run Rust backend tests
106+
- name: Copy tests to isolated location (Windows)
107+
if: runner.os == 'Windows'
108+
run: Copy-Item -Recurse tests $env:RUNNER_TEMP\tests
109+
shell: pwsh
110+
111+
- name: Run Rust backend tests (Unix)
112+
if: runner.os != 'Windows'
100113
working-directory: /tmp
101114
run: pytest tests/test_rust_backend.py -v
102115

103-
- name: Run tests with Rust backend
116+
- name: Run Rust backend tests (Windows)
117+
if: runner.os == 'Windows'
118+
working-directory: ${{ runner.temp }}
119+
run: pytest tests/test_rust_backend.py -v
120+
121+
- name: Run tests with Rust backend (Unix)
122+
if: runner.os != 'Windows'
104123
working-directory: /tmp
105124
run: DIFF_DIFF_BACKEND=rust pytest tests/ -x -q
106125

126+
- name: Run tests with Rust backend (Windows)
127+
if: runner.os == 'Windows'
128+
working-directory: ${{ runner.temp }}
129+
run: |
130+
$env:DIFF_DIFF_BACKEND="rust"
131+
pytest tests/ -x -q
132+
shell: pwsh
133+
107134
# Test pure Python fallback (without Rust extension)
108135
python-fallback:
109136
name: Pure Python Fallback

CLAUDE.md

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ maturin develop
4040
# Build with release optimizations
4141
maturin develop --release
4242

43-
# Run Rust unit tests
44-
cd rust && cargo test
45-
4643
# Force pure Python mode (disable Rust backend)
4744
DIFF_DIFF_BACKEND=python pytest
4845

@@ -53,35 +50,9 @@ DIFF_DIFF_BACKEND=rust pytest
5350
pytest tests/test_rust_backend.py -v
5451
```
5552

56-
#### Troubleshooting Rust Tests (PyO3 Linking)
57-
58-
If `cargo test` fails with `library 'pythonX.Y' not found`, PyO3 cannot find the Python library. This commonly happens on macOS when using the system Python (which lacks development headers in expected locations).
59-
60-
**Solution**: Use a Python environment with proper library paths (e.g., conda, Homebrew, or pyenv):
61-
62-
```bash
63-
# Using miniconda (example path - adjust for your system)
64-
cd rust
65-
PYO3_PYTHON=/path/to/miniconda3/bin/python3 \
66-
DYLD_LIBRARY_PATH="/path/to/miniconda3/lib" \
67-
cargo test
68-
69-
# Using Homebrew Python
70-
PYO3_PYTHON=/opt/homebrew/bin/python3 \
71-
DYLD_LIBRARY_PATH="/opt/homebrew/lib" \
72-
cargo test
73-
```
74-
75-
**Environment variables:**
76-
- `PYO3_PYTHON`: Path to Python interpreter with development headers
77-
- `DYLD_LIBRARY_PATH` (macOS) / `LD_LIBRARY_PATH` (Linux): Path to `libpythonX.Y.dylib`/`.so`
78-
79-
**Verification**: All 22 Rust tests should pass, including bootstrap weight tests:
80-
```
81-
test bootstrap::tests::test_webb_variance_approx_correct ... ok
82-
test bootstrap::tests::test_webb_values_correct ... ok
83-
test bootstrap::tests::test_webb_mean_approx_zero ... ok
84-
```
53+
**Note**: As of v2.2.0, the Rust backend uses the pure-Rust `faer` library for linear algebra,
54+
eliminating external BLAS/LAPACK dependencies. This enables Windows wheel builds and simplifies
55+
cross-platform compilation - no OpenBLAS or Intel MKL installation required.
8556

8657
## Architecture
8758

@@ -173,16 +144,17 @@ test bootstrap::tests::test_webb_mean_approx_zero ... ok
173144
- Exports `HAS_RUST_BACKEND` flag and Rust function references
174145
- Other modules import from here to avoid circular imports with `__init__.py`
175146

176-
- **`rust/`** - Optional Rust backend for accelerated computation (v2.0.0):
147+
- **`rust/`** - Optional Rust backend for accelerated computation (v2.0.0+):
177148
- **`rust/src/lib.rs`** - PyO3 module definition, exports Python bindings
178149
- **`rust/src/bootstrap.rs`** - Parallel bootstrap weight generation (Rademacher, Mammen, Webb)
179-
- **`rust/src/linalg.rs`** - OLS solver and cluster-robust variance estimation
150+
- **`rust/src/linalg.rs`** - OLS solver (SVD-based) and cluster-robust variance estimation
180151
- **`rust/src/weights.rs`** - Synthetic control weights and simplex projection
181152
- **`rust/src/trop.rs`** - TROP estimator acceleration:
182153
- `compute_unit_distance_matrix()` - Parallel pairwise RMSE distance computation (4-8x speedup)
183154
- `loocv_grid_search()` - Parallel LOOCV across tuning parameters (10-50x speedup)
184155
- `bootstrap_trop_variance()` - Parallel bootstrap variance estimation (5-15x speedup)
185-
- Uses ndarray-linalg with OpenBLAS (Linux/macOS) or Intel MKL (Windows)
156+
- Uses pure-Rust `faer` library for linear algebra (no external BLAS/LAPACK dependencies)
157+
- Cross-platform: builds on Linux, macOS, and Windows without additional setup
186158
- Provides 4-8x speedup for SyntheticDiD, 5-20x speedup for TROP
187159

188160
- **`diff_diff/results.py`** - Dataclass containers for estimation results:

0 commit comments

Comments
 (0)