Coverage for src/cvx/linalg/core/exceptions.py: 100%
54 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
1"""Domain-specific exceptions and warnings for cvx.linalg."""
3from __future__ import annotations
5import warnings
6from typing import Literal
8import numpy as np
10from .types import Matrix
12DEFAULT_COND_THRESHOLD: float = 1e12
13"""Default condition-number threshold above which an IllConditionedMatrixWarning is emitted."""
16class NotAMatrixError(TypeError):
17 """Raised when a 2-D matrix is required but the input has a different number of dimensions.
19 Args:
20 ndim: Actual number of dimensions of the offending array.
21 func: Name of the function that rejected the input.
23 Examples:
24 >>> raise NotAMatrixError(3)
25 Traceback (most recent call last):
26 ...
27 cvx.linalg.core.exceptions.NotAMatrixError: eigvals() expected a 2-D matrix, got 3-D input.
28 >>> raise NotAMatrixError(3, func="qr")
29 Traceback (most recent call last):
30 ...
31 cvx.linalg.core.exceptions.NotAMatrixError: qr() expected a 2-D matrix, got 3-D input.
32 """
34 def __init__(self, ndim: int, func: str = "eigvals") -> None:
35 """Initialize with the actual number of dimensions and the rejecting function."""
36 super().__init__(f"{func}() expected a 2-D matrix, got {ndim}-D input.")
37 self.ndim = ndim
38 self.func = func
41class NonSquareMatrixError(ValueError):
42 """Raised when a square matrix is required but the input is not square.
44 Args:
45 rows: Number of rows in the offending matrix.
46 cols: Number of columns in the offending matrix.
48 Examples:
49 >>> raise NonSquareMatrixError(3, 2)
50 Traceback (most recent call last):
51 ...
52 cvx.linalg.core.exceptions.NonSquareMatrixError: Matrix must be square, got shape (3, 2).
53 """
55 def __init__(self, rows: int, cols: int) -> None:
56 """Initialize with the offending matrix shape."""
57 super().__init__(f"Matrix must be square, got shape ({rows}, {cols}).")
58 self.rows = rows
59 self.cols = cols
62class DimensionMismatchError(ValueError):
63 """Raised when vector and matrix dimensions are incompatible.
65 Args:
66 vector_size: Length of the offending vector.
67 matrix_size: Expected dimension inferred from the matrix.
69 Examples:
70 >>> raise DimensionMismatchError(3, 2)
71 Traceback (most recent call last):
72 ...
73 cvx.linalg.core.exceptions.DimensionMismatchError: Vector length 3 does not match matrix dimension 2.
74 """
76 def __init__(self, vector_size: int, matrix_size: int) -> None:
77 """Initialize with the offending vector and matrix sizes."""
78 super().__init__(f"Vector length {vector_size} does not match matrix dimension {matrix_size}.")
79 self.vector_size = vector_size
80 self.matrix_size = matrix_size
83class SingularMatrixError(ValueError):
84 """Raised when a matrix is (numerically) singular and cannot be inverted.
86 Args:
87 detail: Optional extra detail string to append to the message.
89 Examples:
90 >>> raise SingularMatrixError()
91 Traceback (most recent call last):
92 ...
93 cvx.linalg.core.exceptions.SingularMatrixError: Matrix is singular and cannot be solved.
94 """
96 def __init__(self, detail: str = "") -> None:
97 """Initialize with an optional extra detail string."""
98 msg = "Matrix is singular and cannot be solved."
99 if detail:
100 msg = f"{msg} {detail}"
101 super().__init__(msg)
104class NegativeWarmupError(ValueError):
105 """Raised when a negative warmup period is requested.
107 Args:
108 warmup: The offending warmup value.
110 Examples:
111 >>> raise NegativeWarmupError(-3)
112 Traceback (most recent call last):
113 ...
114 cvx.linalg.core.exceptions.NegativeWarmupError: warmup must be non-negative, got -3.
115 """
117 def __init__(self, warmup: int | None = None) -> None:
118 """Initialize with the offending warmup value."""
119 msg = "warmup must be non-negative."
120 if warmup is not None:
121 msg = f"warmup must be non-negative, got {warmup}."
122 super().__init__(msg)
123 self.warmup = warmup
126class NonIntegerWarmupError(TypeError):
127 """Raised when warmup is not an integer (booleans are rejected as well).
129 Args:
130 value: The offending warmup value.
132 Examples:
133 >>> raise NonIntegerWarmupError(True)
134 Traceback (most recent call last):
135 ...
136 cvx.linalg.core.exceptions.NonIntegerWarmupError: warmup must be an integer, got bool.
137 """
139 def __init__(self, value: object) -> None:
140 """Initialize with the offending warmup value."""
141 super().__init__(f"warmup must be an integer, got {type(value).__name__}.")
142 self.value = value
145class InvalidComponentsError(ValueError):
146 """Raised when the requested number of principal components is out of range.
148 Args:
149 n_components: The requested number of components.
150 max_components: The largest number of components supported by the data.
152 Examples:
153 >>> raise InvalidComponentsError(10, 5)
154 Traceback (most recent call last):
155 ...
156 cvx.linalg.core.exceptions.InvalidComponentsError: n_components must be between 1 and 5, got 10.
157 """
159 def __init__(self, n_components: int, max_components: int) -> None:
160 """Initialize with the requested and maximum number of components."""
161 super().__init__(f"n_components must be between 1 and {max_components}, got {n_components}.")
162 self.n_components = n_components
163 self.max_components = max_components
166class IllConditionedMatrixWarning(UserWarning):
167 """Emitted when a matrix condition number exceeds a configurable threshold.
169 Examples:
170 >>> import warnings
171 >>> with warnings.catch_warnings(record=True) as w:
172 ... warnings.simplefilter("always")
173 ... warnings.warn("condition number 1e13", IllConditionedMatrixWarning)
174 ... issubclass(w[-1].category, IllConditionedMatrixWarning)
175 True
176 """
179def cond(matrix: Matrix, p: int | float | Literal["fro", "nuc"] | None = None) -> float:
180 """Return the condition number of a matrix.
182 Returns ``nan`` if the matrix contains any non-finite (NaN or inf) entries.
183 Otherwise delegates to :func:`numpy.linalg.cond`.
185 Args:
186 matrix: Input matrix.
187 p: Order of the norm used to compute the condition number.
188 Accepts the same values as :func:`numpy.linalg.cond`
189 (``None``, ``1``, ``-1``, ``2``, ``-2``, ``numpy.inf``,
190 ``-numpy.inf``, ``'fro'``). Defaults to ``None`` which
191 corresponds to the 2-norm (largest singular value divided by
192 the smallest).
194 Returns:
195 The condition number as a ``float``, or ``nan`` when the matrix
196 contains non-finite entries.
198 Examples:
199 >>> import numpy as np
200 >>> cond(np.eye(3))
201 1.0
202 >>> import math
203 >>> math.isnan(cond(np.array([[float('nan'), 1.0], [1.0, 2.0]])))
204 True
205 >>> cond(np.diag([1.0, 1e10]), p=1)
206 10000000000.0
207 """
208 if not np.all(np.isfinite(matrix)):
209 return float("nan")
210 return float(np.linalg.cond(matrix, p=p))
213def warn_ill_conditioned(cond_value: float, threshold: float, stacklevel: int = 3) -> None:
214 """Emit IllConditionedMatrixWarning when *cond_value* exceeds *threshold*.
216 Args:
217 cond_value: Condition number to compare against the threshold.
218 threshold: Upper bound before a warning is issued.
219 stacklevel: Stack level passed to :func:`warnings.warn` so the warning
220 points at the caller of the public API. Defaults to ``3``.
222 Example:
223 >>> import warnings
224 >>> with warnings.catch_warnings(record=True) as w:
225 ... warnings.simplefilter("always")
226 ... warn_ill_conditioned(2.0, 0.5)
227 ... len(w)
228 1
229 """
230 if cond_value > threshold:
231 warnings.warn(
232 f"Matrix condition number {cond_value:.3e} exceeds threshold {threshold:.3e}; "
233 "results may be numerically unreliable.",
234 IllConditionedMatrixWarning,
235 stacklevel=stacklevel,
236 )
239def check_and_warn_condition(matrix: Matrix, threshold: float) -> None:
240 """Emit IllConditionedMatrixWarning when the condition number exceeds threshold.
242 Args:
243 matrix: Square matrix whose condition number is checked.
244 threshold: Upper bound before a warning is issued.
246 Example:
247 >>> import numpy as np
248 >>> import warnings
249 >>> with warnings.catch_warnings(record=True) as w:
250 ... warnings.simplefilter("always")
251 ... check_and_warn_condition(np.eye(2), 0.5)
252 ... len(w)
253 1
254 """
255 warn_ill_conditioned(cond(matrix), threshold, stacklevel=4)