Coverage for src/cvx/linalg/core/condition.py: 100%
35 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 07:23 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-01 07:23 +0000
1"""Condition-number helpers: the NaN-aware ``cond`` and the ill-conditioning warning checks."""
3from __future__ import annotations
5import warnings
6from typing import Literal
8import numpy as np
10from .exceptions import IllConditionedMatrixWarning
11from .types import Matrix
13try: # SciPy lets check_and_warn_condition skip the SVD for well-conditioned SPD input.
14 from scipy.linalg import cho_factor as _cho_factor # type: ignore[import-untyped]
15 from scipy.linalg.lapack import get_lapack_funcs as _get_lapack_funcs # type: ignore[import-untyped]
17 _HAVE_SCIPY = True
18except ImportError: # pragma: no cover - depends on the environment; the fallback is tested by patching the flag
19 _HAVE_SCIPY = False
21_SCREEN_MARGIN: float = 10.0
22"""Safety factor on the LAPACK condition estimate before it may stand in for the exact 2-norm value."""
24DEFAULT_COND_THRESHOLD: float = 1e12
25"""Default condition-number threshold above which an IllConditionedMatrixWarning is emitted."""
28def cond(matrix: Matrix, p: int | float | Literal["fro", "nuc"] | None = None) -> float:
29 """Return the condition number of a matrix.
31 Returns ``nan`` if the matrix contains any non-finite (NaN or inf) entries.
32 Otherwise delegates to :func:`numpy.linalg.cond`.
34 Args:
35 matrix: Input matrix.
36 p: Order of the norm used to compute the condition number.
37 Accepts the same values as :func:`numpy.linalg.cond`
38 (``None``, ``1``, ``-1``, ``2``, ``-2``, ``numpy.inf``,
39 ``-numpy.inf``, ``'fro'``). Defaults to ``None`` which
40 corresponds to the 2-norm (largest singular value divided by
41 the smallest).
43 Returns:
44 The condition number as a ``float``, or ``nan`` when the matrix
45 contains non-finite entries.
47 Examples:
48 >>> import numpy as np
49 >>> cond(np.eye(3))
50 1.0
51 >>> import math
52 >>> math.isnan(cond(np.array([[float('nan'), 1.0], [1.0, 2.0]])))
53 True
54 >>> cond(np.diag([1.0, 1e10]), p=1)
55 10000000000.0
56 """
57 if not np.all(np.isfinite(matrix)):
58 return float("nan")
59 return float(np.linalg.cond(matrix, p=p))
62def warn_ill_conditioned(cond_value: float, threshold: float | None, stacklevel: int = 3) -> None:
63 """Emit IllConditionedMatrixWarning when *cond_value* exceeds *threshold*.
65 Args:
66 cond_value: Condition number to compare against the threshold.
67 threshold: Upper bound before a warning is issued, or ``None`` to never warn.
68 stacklevel: Stack level passed to :func:`warnings.warn` so the warning
69 points at the caller of the public API. Defaults to ``3``.
71 Example:
72 >>> import warnings
73 >>> with warnings.catch_warnings(record=True) as w:
74 ... warnings.simplefilter("always")
75 ... warn_ill_conditioned(2.0, 0.5)
76 ... len(w)
77 1
78 """
79 if threshold is not None and cond_value > threshold:
80 warnings.warn(
81 f"Matrix condition number {cond_value:.3e} exceeds threshold {threshold:.3e}; "
82 "results may be numerically unreliable.",
83 IllConditionedMatrixWarning,
84 stacklevel=stacklevel,
85 )
88def check_and_warn_condition(matrix: Matrix, threshold: float | None) -> None:
89 """Emit IllConditionedMatrixWarning when the condition number exceeds threshold.
91 Args:
92 matrix: Square matrix whose condition number is checked.
93 threshold: Upper bound before a warning is issued, or ``None`` to skip
94 the check entirely.
96 The condition number is the 2-norm one, as from :func:`cond`. With SciPy
97 installed, a symmetric positive-definite matrix is first screened by a
98 LAPACK estimate from its Cholesky factor; when that proves the matrix well
99 within *threshold*, the full SVD is skipped.
101 Example:
102 >>> import numpy as np
103 >>> import warnings
104 >>> with warnings.catch_warnings(record=True) as w:
105 ... warnings.simplefilter("always")
106 ... check_and_warn_condition(np.eye(2), 0.5)
107 ... len(w)
108 1
109 """
110 if threshold is None or _certainly_within(matrix, threshold):
111 return
112 warn_ill_conditioned(cond(matrix), threshold, stacklevel=4)
115def _certainly_within(matrix: Matrix, threshold: float) -> bool:
116 """Return True when a cheap estimate proves the 2-norm condition number is at most *threshold*.
118 For a symmetric matrix ``||A||_2 <= ||A||_1``, so the 1-norm condition
119 number bounds the 2-norm one from above. LAPACK ``pocon`` estimates the
120 former from a Cholesky factor for about the cost of one more solve, whereas
121 the exact 2-norm value needs a full SVD. The estimate can fall short of the
122 true 1-norm value, hence the :data:`_SCREEN_MARGIN` safety factor.
124 ``False`` means only "not proven": SciPy is missing, or the matrix is not
125 symmetric positive-definite, or the estimate is too close to the threshold.
126 The caller then computes the exact condition number.
128 Args:
129 matrix: Square matrix whose condition number is screened.
130 threshold: Upper bound the condition number is tested against.
132 Returns:
133 ``True`` if the 2-norm condition number is certainly at most *threshold*.
134 """
135 if not _HAVE_SCIPY or not np.array_equal(matrix, matrix.T):
136 return False
137 try:
138 factor, lower = _cho_factor(matrix)
139 except (np.linalg.LinAlgError, ValueError): # not positive-definite, or not finite
140 return False
141 (pocon,) = _get_lapack_funcs(("pocon",), (factor,))
142 rcond, _info = pocon(factor, np.linalg.norm(matrix, 1), uplo="L" if lower else "U")
143 return bool(rcond * threshold >= _SCREEN_MARGIN)