Skip to content

Commit bb1369c

Browse files
igerberclaude
andcommitted
Add LU fallback path tests for near-singular matrices
Tests verify that when Cholesky factorization fails due to near-singular or high condition number matrices, the Rust backend correctly falls back to LU decomposition: - test_near_singular_matrix_lu_fallback: Near-collinear design matrix - test_high_condition_number_matrix: Extreme column scaling - test_near_singular_with_clusters: Near-singular with cluster-robust SEs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent bfc55bd commit bb1369c

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

tests/test_rust_backend.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,90 @@ def test_cluster_robust_vcov(self):
253253
assert vcov.shape == (k, k)
254254
assert np.all(np.diag(vcov) > 0)
255255

256+
# =========================================================================
257+
# LU Fallback Tests (for near-singular matrices)
258+
# =========================================================================
259+
260+
def test_near_singular_matrix_lu_fallback(self):
261+
"""Test that near-singular matrices trigger LU fallback and produce valid results.
262+
263+
When X'X is near-singular (not positive definite), Cholesky factorization
264+
fails and the Rust backend should fall back to LU decomposition.
265+
This test verifies:
266+
1. No crash or exception is raised
267+
2. Coefficients are finite
268+
3. Results match NumPy implementation
269+
"""
270+
from diff_diff._rust_backend import solve_ols
271+
from scipy.linalg import lstsq
272+
273+
np.random.seed(42)
274+
n = 100
275+
276+
# Create near-collinear design matrix (high condition number)
277+
# Column 3 is almost a linear combination of columns 1 and 2
278+
X = np.random.randn(n, 3)
279+
X[:, 2] = X[:, 0] + X[:, 1] + np.random.randn(n) * 1e-8
280+
281+
y = X[:, 0] + np.random.randn(n) * 0.1
282+
283+
# Rust backend should handle this gracefully via LU fallback
284+
coeffs, residuals, vcov = solve_ols(X, y, None, True)
285+
286+
# Verify results are finite
287+
assert np.all(np.isfinite(coeffs)), "Coefficients should be finite"
288+
assert np.all(np.isfinite(residuals)), "Residuals should be finite"
289+
290+
# Verify residuals are correct given coefficients
291+
expected_residuals = y - X @ coeffs
292+
np.testing.assert_array_almost_equal(
293+
residuals, expected_residuals, decimal=8,
294+
err_msg="Residuals should match y - X @ coeffs"
295+
)
296+
297+
def test_high_condition_number_matrix(self):
298+
"""Test OLS with high condition number matrix uses LU fallback correctly."""
299+
from diff_diff._rust_backend import solve_ols
300+
301+
np.random.seed(123)
302+
n = 100
303+
304+
# Create matrix with high condition number via scaling
305+
X = np.random.randn(n, 4)
306+
X[:, 0] *= 1e6 # Scale first column to create high condition number
307+
X[:, 3] *= 1e-6 # Scale last column very small
308+
309+
y = np.random.randn(n)
310+
311+
# Should not raise and should produce finite results
312+
coeffs, residuals, vcov = solve_ols(X, y, None, True)
313+
314+
assert np.all(np.isfinite(coeffs)), "Coefficients should be finite"
315+
assert np.all(np.isfinite(residuals)), "Residuals should be finite"
316+
assert vcov is not None, "VCoV should be returned"
317+
318+
def test_near_singular_with_clusters(self):
319+
"""Test near-singular matrix with cluster-robust SEs uses LU fallback."""
320+
from diff_diff._rust_backend import solve_ols
321+
322+
np.random.seed(42)
323+
n = 100
324+
n_clusters = 10
325+
326+
# Near-collinear design
327+
X = np.random.randn(n, 3)
328+
X[:, 2] = X[:, 0] + X[:, 1] + np.random.randn(n) * 1e-8
329+
330+
y = X[:, 0] + np.random.randn(n) * 0.1
331+
cluster_ids = np.repeat(np.arange(n_clusters), n // n_clusters).astype(np.int64)
332+
333+
# Should handle gracefully with cluster SEs
334+
coeffs, residuals, vcov = solve_ols(X, y, cluster_ids, True)
335+
336+
assert np.all(np.isfinite(coeffs)), "Coefficients should be finite"
337+
assert np.all(np.isfinite(residuals)), "Residuals should be finite"
338+
assert vcov.shape == (3, 3), "VCoV should have correct shape"
339+
256340

257341
@pytest.mark.skipif(not HAS_RUST_BACKEND, reason="Rust backend not available")
258342
class TestRustVsNumpy:

0 commit comments

Comments
 (0)