Skip to content

Conversation

@igerber
Copy link
Owner

@igerber igerber commented Jan 26, 2026

Summary

  • Replace ndarray-linalg (requires OpenBLAS/MKL) with faer (pure Rust linear algebra)
  • Update Rust dependencies: pyo3 0.22, numpy 0.22, ndarray 0.16, faer 0.24, faer-ext 0.7
  • Migrate linalg.rs SVD/LU to faer equivalents
  • Migrate trop.rs soft_threshold_svd() to faer SVD
  • Update all Rust files for PyO3 0.22 API (Bound<> return types)
  • Add Windows to rust-test.yml CI matrix
  • Add Windows wheel build job to publish.yml

Methodology references (required if estimator / math changes)

  • Method name(s): OLS solver (SVD-based), matrix inversion (LU), nuclear norm soft-thresholding (SVD)
  • Paper / source link(s): N/A - implementation change only, no methodology change
  • Any intentional deviations from the source (and why): None - faer provides equivalent SVD/LU operations

Validation

  • Tests added/updated: No new tests needed - all 62 existing Rust backend equivalence tests pass
  • Full test suite: 1091/1091 tests passed
  • Build verification: maturin build --release succeeds

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Generated with Claude Code

Replace ndarray-linalg (which requires OpenBLAS/MKL) with faer, a pure
Rust linear algebra library with no external dependencies. This enables
Windows wheel builds without complex BLAS/LAPACK CI configuration.

Changes:
- rust/Cargo.toml: Add faer 0.24, faer-ext 0.7; update pyo3 to 0.22,
  numpy to 0.22, ndarray to 0.16; remove platform-conditional deps
- rust/src/linalg.rs: Replace ndarray-linalg SVD/LU with faer equivalents
- rust/src/trop.rs: Update soft_threshold_svd() to use faer SVD
- rust/src/*.rs: Update for PyO3 0.22 API (Bound<> return types)
- .github/workflows/rust-test.yml: Add Windows to CI matrix
- .github/workflows/publish.yml: Add Windows wheel build job

All 1091 Python tests pass. All 62 Rust backend equivalence tests pass.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link

Overall Assessment

  • ⚠️ Needs changes

Executive Summary

  • P1: The new faer SVD path in OLS breaks underdetermined designs (n<k), so regression-based estimators can panic or return invalid β.
  • TROP soft-threshold SVD changes still align with the Methodology Registry; no deviations found there.
  • Tests don’t cover the new n<k SVD path, so this regression could slip into releases.

Methodology

  • Severity: P1 | Impact: Under-determined designs (n<k) now hit a shape mismatch in the SVD pseudoinverse path, so OLS can panic or return invalid coefficients; this violates the registry’s rank-deficient SVD handling for regression-based estimators (DifferenceInDifferences, MultiPeriodDiD, TwoWayFixedEffects, SunAbraham). | Concrete fix: Use r=min(n,k); build s_inv_uty with length r and compute β via V (k×r) × s_inv_uty (or request full Vᵀ from faer), plus an explicit guard for n<k. | Location: rust/src/linalg.rs:121, rust/src/linalg.rs:132

Code Quality

  • No issues found.

Performance

  • No issues found.

Maintainability

  • No issues found.

Tech Debt

  • No issues found.

Security

  • No issues found.

Documentation/Tests

  • Severity: P2 | Impact: The faer-based OLS path lacks a regression test for underdetermined matrices, so the n<k failure mode can recur unnoticed. | Concrete fix: Add a unit test in rust/src/linalg.rs that exercises n<k (and, if possible, a Python rust-backend equivalence test for that case). | Location: rust/src/linalg.rs:336

igerber and others added 2 commits January 26, 2026 17:57
The macOS runner has Python 3.14 installed by default, but PyO3 0.22.6
only supports up to Python 3.13. Setting this env var tells PyO3 to
build using the stable ABI, which is forward-compatible.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
For underdetermined systems (n < k), thin SVD returns:
- U: (n, n)
- S: (n,) - only n singular values
- V: (k, n) - only n right singular vectors

The bug was creating s_inv_uty with size k instead of s.len()=min(n,k),
causing a dimension mismatch in the dot product vt.t().dot(&s_inv_uty).

Fix: Use s.len() for s_inv_uty array size, which correctly handles both
overdetermined (n >= k) and underdetermined (n < k) cases.

Addresses P1 bug identified in PR #115 code review.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@igerber
Copy link
Owner Author

igerber commented Jan 26, 2026

/ai-review

@github-actions
Copy link

🔁 AI review rerun (requested by @igerber)

Head SHA: 5ddc8b17013d839df966dcbd557673655e0af0b2


Overall assessment: ⚠️ Needs changes

Methodology

  • P1 | Impact: LU-based inversion only checks for NaN/Inf, so singular or near‑singular X'X can yield finite but invalid vcov/SEs; this conflicts with the Registry’s rank‑deficient handling for OLS‑backed estimators (DiD/TWFE/etc.). | Fix: add an explicit invertibility/rank check (pivot threshold or is_invertible/rank if available) and raise on singular X'X, or restore Cholesky+LU with failure detection. | Ref: rust/src/linalg.rs:L286

Code Quality

  • No findings.

Performance

  • P3 | Impact: Per‑element ndarray↔faer conversions and manual reconstruction loops add overhead in OLS SVD and TROP soft‑thresholding; may regress large‑matrix performance vs prior BLAS path. | Fix: use faer-ext ndarray interop or keep computations in faer and convert outputs once. | Ref: rust/src/linalg.rs:L63, rust/src/trop.rs:L770

Maintainability

  • No findings.

Tech Debt

  • P3 | Impact: faer-ext is declared but unused in Rust sources, adding dependency and build surface. | Fix: remove faer-ext from rust/Cargo.toml or adopt it for ndarray conversion. | Ref: rust/Cargo.toml:L18

Security

  • No findings.

Documentation/Tests

  • P2 | Impact: No regression test covering singular X'X handling after the LU switch; silent failures could slip through. | Fix: add a unit test that passes a singular design into invert_symmetric or compute_robust_vcov and asserts a ValueError. | Ref: rust/src/linalg.rs:L286

Open Questions/Assumptions

  • None.

Executive Summary

  • P1 risk: singular X'X may now produce finite but invalid SEs unless LU invertibility is explicitly checked.
  • Add a regression test for singular X'X to lock in rank‑deficient behavior.
  • Minor perf regression risk from per‑element conversions in OLS/TROP paths; consider faer-ext interop.
  • Minor tech debt: faer-ext currently unused; remove or leverage it.

- Add residual-based verification to invert_symmetric (||A*A^{-1} - I||)
  to catch near-singular matrices that produce finite but inaccurate results
- Use threshold of 1e-4 * n to allow ill-conditioned but valid matrices
  while catching truly pathological cases
- Add unit tests for singular and near-singular matrix inversion
- Remove unused faer-ext dependency from Cargo.toml

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@igerber
Copy link
Owner Author

igerber commented Jan 26, 2026

/ai-review

@github-actions
Copy link

🔁 AI review rerun (requested by @igerber)

Head SHA: 2341e4f529baaf8cd49bb2567b60a005738c4e1c


Overall Assessment

  • ⚠️ Needs changes

Methodology

  • P0 Impact: bootstrap_trop_variance and bootstrap_trop_variance_joint return se=0.0 when fewer than 2 bootstrap estimates, violating NaN propagation and risking misleading inference in TROP; Concrete fix: return f64::NAN (and ensure downstream inference fields are NaN or force Python fallback) when bootstrap_estimates.len() < 2; Locations: rust/src/trop.rs:L1038-L1054, rust/src/trop.rs:L1703-L1719.
  • P1 Impact: New LU residual gating throws on near‑singular but full‑rank X'X, changing default behavior from the registry’s rank‑deficient warn/NA path and causing Rust solve_ols to hard‑fail without a Python fallback; Concrete fix: align instability threshold with the rcond=1e-07 rank check or return NaN vcov instead of error (and/or catch Rust errors in _solve_ols_rust to fall back to NumPy/QR); Locations: rust/src/linalg.rs:L286-L351, rust/src/linalg.rs:L140-L205, diff_diff/linalg.py:L247-L320.

Code Quality

  • No issues found.

Performance

  • P2 Impact: Residual verification multiplies A * A^{-1} on every vcov computation, adding an extra O(k^3) matmul per solve that can be costly with large dummy‑expanded designs; Concrete fix: gate the check behind a debug flag, use a cheaper condition‑number estimate from LU, or only run the residual check when pivots indicate instability; Location: rust/src/linalg.rs:L326-L351.

Maintainability

  • No issues found.

Tech Debt

  • No issues found.

Security

  • No issues found.

Documentation/Tests

  • No issues found.

Executive Summary

  • P0: TROP bootstrap SE returns 0.0 when insufficient samples; should be NaN or trigger fallback to avoid misleading inference.
  • P1: LU residual gating can hard‑fail full‑rank regressions and bypass the registry’s warn/NA behavior; align thresholds or add fallback.
  • P2: Added residual check incurs an extra O(k^3) matmul per vcov; consider gating for large models.
  • Otherwise, the faer SVD/soft‑threshold logic appears aligned with registry formulas.

P0: Return f64::NAN instead of 0.0 in TROP bootstrap when < 2 samples
P1: Add Python fallback in _solve_ols_rust for numerical instability
P2: Gate expensive O(n³) residual check behind LU pivot ratio detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@igerber
Copy link
Owner Author

igerber commented Jan 27, 2026

/ai-review

@github-actions
Copy link

🔁 AI review rerun (requested by @igerber)

Head SHA: f3b1566ad53f105b4efb7525db6fafce3fa757ff


Overall Assessment: ⚠️ Needs changes

Executive Summary

  • P1: solve_ols(skip_rank_check=True) can return None when the Rust backend flags numerical instability, breaking the documented return contract (diff_diff/linalg.py:482).
  • P2: New "numerically unstable" inversion errors from Rust propagate through compute_robust_vcov without fallback, so estimators that call it directly can now hard‑fail (diff_diff/linalg.py:752, rust/src/linalg.rs:367, diff_diff/estimators.py:436).
  • P3: Registry/docstring cross‑check for OLS HC1/cluster SEs and TROP soft‑threshold SVD shows no methodology deviations (docs/methodology/REGISTRY.md:53, docs/methodology/REGISTRY.md:482, rust/src/linalg.rs:1, rust/src/trop.rs:1).

Methodology

  • Severity: P3 | Impact: OLS‑based estimators (DiD/MultiPeriodDiD/TWFE) and TROP remain aligned with registry requirements (HC1/cluster SEs; soft‑threshold SVD); no deviations detected. | Concrete fix: None. (docs/methodology/REGISTRY.md:53, docs/methodology/REGISTRY.md:482, rust/src/linalg.rs:1, rust/src/trop.rs:1)

Code Quality

  • Severity: P1 | Impact: solve_ols(skip_rank_check=True) may return None on Rust instability, which can crash tuple unpacking or leak None into estimator pipelines. | Concrete fix: In the skip_rank_check branch, detect None and fall back to _solve_ols_numpy to preserve the function’s return contract. (diff_diff/linalg.py:482)
  • Severity: P2 | Impact: compute_robust_vcov now surfaces "numerically unstable" Rust errors without fallback, potentially breaking inference paths that use it directly (e.g., wild bootstrap storage). | Concrete fix: In compute_robust_vcov, treat "numerically unstable" like "Matrix inversion failed" and fall back to _compute_robust_vcov_numpy before raising. (diff_diff/linalg.py:752, rust/src/linalg.rs:367, diff_diff/estimators.py:436)

Performance

  • Severity: P3 | Impact: Manual element‑wise copies and triple loops in faer SVD conversion/reconstruction may be slower for large matrices. | Concrete fix: Consider using faer’s matmul/conversion helpers to avoid nested loops where possible. (rust/src/linalg.rs:85, rust/src/trop.rs:837)

Maintainability

  • Severity: P3 | Impact: No maintainability issues found in this diff. | Concrete fix: None.

Tech Debt

  • Severity: P3 | Impact: No tech‑debt concerns identified in this diff. | Concrete fix: None.

Security

  • Severity: P3 | Impact: No security issues found. | Concrete fix: None.

Documentation/Tests

  • Severity: P3 | Impact: New Rust instability fallback paths are not covered by tests; regressions could slip in as faer behavior evolves. | Concrete fix: Add Rust‑backend tests that trigger instability and assert fallback behavior (and that skip_rank_check=True still returns valid tuples). (tests/test_linalg.py:303, diff_diff/linalg.py:482, diff_diff/linalg.py:752)

Open Questions/Assumptions

  • Do you want compute_robust_vcov to always fall back to NumPy on numerical instability, or should it be a hard error for ill‑conditioned X'X?

Change Summary

  • Replaced BLAS‑backed ndarray‑linalg with faer for Rust SVD/LU and added numerical‑instability checks/fallback hooks.
  • Expanded CI to build/test Windows wheels and updated workflows accordingly.

- P1: solve_ols(skip_rank_check=True) now checks for None return from
  Rust and falls through to NumPy on numerical instability
- P2: compute_robust_vcov now catches "numerically unstable" errors
  and falls back to NumPy instead of propagating the error
- Remove PyO3-dependent tests from Rust that caused cargo test to fail
  without Python initialization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@igerber
Copy link
Owner Author

igerber commented Jan 27, 2026

/ai-review

@github-actions
Copy link

🔁 AI review rerun (requested by @igerber)

Head SHA: 1a00fb9ee17fab5863fd02c1ff8a87694483f7bd


Overall assessment: ⚠️ Needs changes

Executive Summary

  • Methodology cross-check: OLS solver (SVD + robust vcov) and TROP soft-threshold SVD remain aligned with docs/methodology/REGISTRY.md and Athey et al. (2025).
  • P2: compute_robust_vcov silently falls back on Rust numerical-instability errors, which can mask unstable SEs.
  • P3: No explicit test coverage for the new numerical-instability path.

Methodology

  • Severity: P3 | Impact: None (alignment confirmed) | Fix: None — OLS SVD/robust SE formulas and TROP soft-threshold SVD remain consistent with docs/methodology/REGISTRY.md, rust/src/linalg.rs:44, and rust/src/trop.rs:784.
  • Severity: P2 | Impact: compute_robust_vcov returns fallback SEs without warning when Rust reports numerical instability, so inference can appear valid despite ill-conditioned X'X | Fix: emit a warning or surface the error on fallback in diff_diff/linalg.py:764.

Code Quality

  • No findings.

Performance

  • No findings.

Maintainability

  • No findings.

Tech Debt

  • No findings.

Security

  • No findings.

Documentation/Tests

  • Severity: P3 | Impact: The new LU instability path is untested, so warning/fallback behavior could regress | Fix: add a regression test that triggers Matrix inversion numerically unstable in rust/src/linalg.rs:365 and asserts the warning/fallback behavior in diff_diff/linalg.py:764.

…llback

Address PR #115 review round 3:
- P2: Add UserWarning when Rust backend falls back to Python on numerical instability
- P3: Add test_numerical_instability_fallback_warns to verify warning and fallback behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@igerber
Copy link
Owner Author

igerber commented Jan 27, 2026

/ai-review

@github-actions
Copy link

🔁 AI review rerun (requested by @igerber)

Head SHA: 69fde20457939a36d70c59245a149e93421f7147


Overall assessment: ✅ Looks good

Methodology

  • P3: No deviations found versus docs/methodology/REGISTRY.md for SVD-based OLS, HC1/cluster vcov, or TROP soft-threshold SVD. Impact: N/A. Fix: N/A.

Code Quality

  • P3: _solve_ols_rust only falls back on "numerically unstable"/"singular" errors; faer SVD failures raise a ValueError and bypass fallback. Impact: rare ill-conditioned inputs could hard-fail instead of using the Python backend. Fix: treat "SVD computation failed" as a fallback trigger (warn + return None). (diff_diff/linalg.py:307, rust/src/linalg.rs:70)

Performance

  • P3: No performance regressions identified in this diff. Impact: N/A. Fix: N/A.

Maintainability

  • P3: No maintainability issues identified in this diff. Impact: N/A. Fix: N/A.

Tech Debt

  • P3: No tech-debt issues identified in this diff. Impact: N/A. Fix: N/A.

Security

  • P3: No security issues identified in this diff. Impact: N/A. Fix: N/A.

Documentation/Tests

  • P3: Backend swap to faer adds only a warning/fallback test; no explicit parity/regression tests for OLS coefficients/vcov or TROP soft-threshold SVD. Impact: numerical drift across platforms could slip through. Fix: add small deterministic parity tests comparing Rust vs NumPy/SciPy outputs. (tests/test_linalg.py:523)

Open Questions

  • Do you want a small cross-platform parity test suite specifically for faer SVD/LU vs SciPy to guard against numeric drift?

Executive Summary

  • No methodology deviations detected vs the registry for OLS/robust vcov or TROP soft-thresholding.
  • Minor reliability gap: faer SVD failures bypass the Python fallback; consider mapping that error into the fallback path.
  • Test gap: add parity tests for faer vs NumPy/SciPy to catch numerical drift across platforms.

@igerber igerber merged commit 338ec22 into main Jan 27, 2026
7 checks passed
@igerber igerber deleted the feature/windows-wheels-faer branch January 27, 2026 12:20
@igerber igerber mentioned this pull request Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants