Skip to content

Commit deeea5a

Browse files
igerberclaude
andcommitted
fix(rdd): cross-BLAS tolerance for constant-covariate exclusion tests
CI round 1 (Linux/Windows/pure-python; macOS passed): the two constant-covariate exclusion tests asserted EXACT equality between the fit with the excluded constant column and the fit without it. The equality is exact mathematically (the excluded column's gamma row is exactly 0) but only roundoff-level numerically: the response matrix still carries the excluded column, and BLAS matmul kernels differ with matrix shape across platforms (bit-equal on Accelerate, last-ULP differences on OpenBLAS/Windows). Asserts relaxed to rel=1e-12 and the "bit-for-bit" claims in the port docstring / REGISTRY / CHANGELOG softened to "to floating-point roundoff". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QGca52n6H8oDDXALjjrsp4
1 parent ececea3 commit deeea5a

5 files changed

Lines changed: 26 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4444
`covs_drop` echo on the results. Degenerate adjustments are GUARDED, not
4545
reproduced (documented deviation): R's `ginv(tol=1e-20)` inverts a float-noise
4646
singular value on constant covariates / full dummy sets, silently returning
47-
platform-dependent estimates; diff-diff excludes degenerate columns, applies a
47+
platform-dependent estimates; diff-diff excludes degenerate columns (a constant
48+
covariate reproduces the fit without it to numerical precision), applies a
4849
scale-invariant stabilized cut for rank-deficient sets (a full dummy set
49-
reproduces the drop-one-category fit exactly), and warns naming the columns.
50+
reproduces the drop-one-category fit to numerical precision), and warns
51+
naming the columns.
5052
**Canonical binding:** `att`/`se`/`t_stat`/`p_value`/`conf_int` are ONE
5153
coherent row - the robust bias-corrected row (`att = tau_bc`, CI centered on it,
5254
`t_stat == att/se`), preserving the library-wide field identities; the new

diff_diff/_rdrobust_port.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,11 @@ def _covs_gamma(
439439
440440
* per-column: ``diag(ZWZ)_j / (z_j' W z_j) < 1e-14`` means column j is
441441
numerically fully explained by the design -> excluded (gamma row 0;
442-
a constant covariate then contributes exactly nothing, matching the
443-
fit without it bit-for-bit);
442+
a constant covariate then contributes exactly nothing and the fit
443+
matches the one without it to floating-point roundoff - not
444+
bit-for-bit, because the response matrix still carries the excluded
445+
column and BLAS matmul kernels differ with matrix SHAPE on some
446+
platforms);
444447
* set-level: equilibrated (scale-invariant) singular values of the
445448
remaining block with ``sv_min < 1e-12 * sv_max`` -> stabilized
446449
equilibrated pseudo-inverse with the noise directions cut

docs/methodology/REGISTRY.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3854,7 +3854,10 @@ per-side and comb chains.
38543854
(~0.5% in the smoke). The port instead (a) EXCLUDES per-column
38553855
degeneracies - explained-ratio `diag(ZWZ)_j/(z_j'Wz_j) < 1e-14` -
38563856
zeroing their gamma rows, so a constant covariate reproduces the fit
3857-
without it bit-for-bit; (b) cuts SET-level noise directions with an
3857+
without it to floating-point roundoff (tested at rel 1e-12; not
3858+
bit-for-bit - the response matrix still carries the excluded column
3859+
and BLAS matmul kernels differ with matrix shape across platforms);
3860+
(b) cuts SET-level noise directions with an
38583861
equilibrated (scale-invariant) pseudo-inverse (`sv_min < 1e-12 *
38593862
sv_max` -> `rcond=1e-12` cut), so a full dummy set reproduces the
38603863
drop-one-category fit (span invariance, tested at rel 1e-9); and (c)

tests/test_rdd_methodology.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,13 @@ def test_constant_covariate_excluded_warns(self):
518518
fit_c = RegressionDiscontinuity(h=0.3).fit(df, "y", "x", covariates=["zlong", "zconst"])
519519
fit_1 = RegressionDiscontinuity(h=0.3).fit(df, "y", "x", covariates=["zlong"])
520520
assert fit_c.covariate_coefficients["zconst"] == 0.0
521-
assert fit_c.att == fit_1.att
522-
assert fit_c.se == fit_1.se
521+
# Equality holds mathematically (the excluded column's gamma is
522+
# exactly 0) but only to float roundoff numerically: the response
523+
# matrix still carries the constant column, and BLAS matmul
524+
# kernels differ with matrix SHAPE across platforms (bit-equal on
525+
# Accelerate, last-ULP off on OpenBLAS/Windows - CI round 1).
526+
assert fit_c.att == pytest.approx(fit_1.att, rel=1e-12)
527+
assert fit_c.se == pytest.approx(fit_1.se, rel=1e-12)
523528

524529
def test_dummy_set_stabilized_equals_drop_one(self):
525530
# A full one-hot set passes the covariate-only QR but is

tests/test_rdrobust_port.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -864,8 +864,12 @@ def test_constant_covariate_excluded_equals_fit_without_it(self):
864864
assert fit_c.covs_excluded.tolist() == [False, True]
865865
# gamma row zeroed -> the constant contributes exactly nothing.
866866
assert fit_c.gamma_p is not None and fit_c.gamma_p[1, 0] == 0.0
867-
assert fit_c.tau_bc == fit_1.tau_bc
868-
assert fit_c.se_rb == fit_1.se_rb
867+
# Mathematically identical; numerically only to float roundoff -
868+
# the (n, 3) vs (n, 2) response shapes route through different
869+
# BLAS matmul kernels on some platforms (CI round 1: last-ULP
870+
# diffs on OpenBLAS/Windows, bit-equal on Accelerate).
871+
assert fit_c.tau_bc == pytest.approx(fit_1.tau_bc, rel=1e-12)
872+
assert fit_c.se_rb == pytest.approx(fit_1.se_rb, rel=1e-12)
869873

870874
def test_dummy_set_stabilized_equals_drop_one(self):
871875
# A full one-hot set passes the intercept-free QR (rank 3) but the

0 commit comments

Comments
 (0)