From 082250627311e6652cf209ae1373a6cf200221d7 Mon Sep 17 00:00:00 2001 From: Bortlesboat Date: Mon, 23 Mar 2026 10:47:45 -0400 Subject: [PATCH] Warn when covariance matrix is PSD but ill-conditioned Add a RuntimeWarning in fix_nonpositive_semidefinite() when the matrix passes the PSD check but has a condition number exceeding 1e10. This alerts users to potential numerical instability in downstream optimization without altering the matrix. Closes #694 --- pypfopt/risk_models.py | 11 +++++++++++ tests/test_risk_models.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pypfopt/risk_models.py b/pypfopt/risk_models.py index 171ce0d0..17d5b921 100644 --- a/pypfopt/risk_models.py +++ b/pypfopt/risk_models.py @@ -80,6 +80,17 @@ def fix_nonpositive_semidefinite(matrix, fix_method="spectral"): positive semidefinite covariance matrix """ if _is_positive_semidefinite(matrix): + try: + cond = np.linalg.cond(matrix) + if cond > 1e10: + warnings.warn( + f"The covariance matrix is positive semidefinite but " + f"ill-conditioned (condition number: {cond:.2e}). This may " + f"lead to numerical instability in optimization.", + RuntimeWarning, + ) + except np.linalg.LinAlgError: # pragma: no cover + pass return matrix warnings.warn( diff --git a/tests/test_risk_models.py b/tests/test_risk_models.py index 2b25072d..d68f9b54 100644 --- a/tests/test_risk_models.py +++ b/tests/test_risk_models.py @@ -104,6 +104,25 @@ def test_sample_cov_npd(): risk_models.fix_nonpositive_semidefinite(S, fix_method="blah") +def test_fix_psd_ill_conditioned_warning(): + # PSD matrix with a very high condition number (ill-conditioned) + n = 3 + matrix = np.eye(n) + matrix[0, 0] = 1e12 + assert risk_models._is_positive_semidefinite(matrix) + assert np.linalg.cond(matrix) > 1e10 + + with pytest.warns(RuntimeWarning, match="ill-conditioned"): + result = risk_models.fix_nonpositive_semidefinite(matrix) + np.testing.assert_array_equal(result, matrix) + + # Well-conditioned PSD matrix should NOT warn + good_matrix = np.eye(n) + assert risk_models._is_positive_semidefinite(good_matrix) + result = risk_models.fix_nonpositive_semidefinite(good_matrix) + np.testing.assert_array_equal(result, good_matrix) + + def test_fix_npd_different_method(): df = get_data() S = risk_models.sample_cov(df)