Coverage for src / cvx / linalg / exceptions.py: 100%

33 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-19 05:40 +0000

1"""Domain-specific exceptions and warnings for cvx.linalg.""" 

2 

3from __future__ import annotations 

4 

5import warnings 

6from typing import Literal 

7 

8import numpy as np 

9 

10 

11class NotAMatrixError(TypeError): 

12 """Raised when a 2-D matrix is required but the input has a different number of dimensions. 

13 

14 Args: 

15 ndim: Actual number of dimensions of the offending array. 

16 

17 Examples: 

18 >>> raise NotAMatrixError(3) 

19 Traceback (most recent call last): 

20 ... 

21 cvx.linalg.exceptions.NotAMatrixError: eigvals() expected a 2-D matrix, got 3-D input. 

22 """ 

23 

24 def __init__(self, ndim: int) -> None: 

25 """Initialize with the actual number of dimensions.""" 

26 super().__init__(f"eigvals() expected a 2-D matrix, got {ndim}-D input.") 

27 self.ndim = ndim 

28 

29 

30class NonSquareMatrixError(ValueError): 

31 """Raised when a square matrix is required but the input is not square. 

32 

33 Args: 

34 rows: Number of rows in the offending matrix. 

35 cols: Number of columns in the offending matrix. 

36 

37 Examples: 

38 >>> raise NonSquareMatrixError(3, 2) 

39 Traceback (most recent call last): 

40 ... 

41 cvx.linalg.exceptions.NonSquareMatrixError: Matrix must be square, got shape (3, 2). 

42 """ 

43 

44 def __init__(self, rows: int, cols: int) -> None: 

45 """Initialize with the offending matrix shape.""" 

46 super().__init__(f"Matrix must be square, got shape ({rows}, {cols}).") 

47 self.rows = rows 

48 self.cols = cols 

49 

50 

51class DimensionMismatchError(ValueError): 

52 """Raised when vector and matrix dimensions are incompatible. 

53 

54 Args: 

55 vector_size: Length of the offending vector. 

56 matrix_size: Expected dimension inferred from the matrix. 

57 

58 Examples: 

59 >>> raise DimensionMismatchError(3, 2) 

60 Traceback (most recent call last): 

61 ... 

62 cvx.linalg.exceptions.DimensionMismatchError: Vector length 3 does not match matrix dimension 2. 

63 """ 

64 

65 def __init__(self, vector_size: int, matrix_size: int) -> None: 

66 """Initialize with the offending vector and matrix sizes.""" 

67 super().__init__(f"Vector length {vector_size} does not match matrix dimension {matrix_size}.") 

68 self.vector_size = vector_size 

69 self.matrix_size = matrix_size 

70 

71 

72class SingularMatrixError(ValueError): 

73 """Raised when a matrix is (numerically) singular and cannot be inverted. 

74 

75 Args: 

76 detail: Optional extra detail string to append to the message. 

77 

78 Examples: 

79 >>> raise SingularMatrixError() 

80 Traceback (most recent call last): 

81 ... 

82 cvx.linalg.exceptions.SingularMatrixError: Matrix is singular and cannot be solved. 

83 """ 

84 

85 def __init__(self, detail: str = "") -> None: 

86 """Initialize with an optional extra detail string.""" 

87 msg = "Matrix is singular and cannot be solved." 

88 if detail: 

89 msg = f"{msg} {detail}" 

90 super().__init__(msg) 

91 

92 

93class IllConditionedMatrixWarning(UserWarning): 

94 """Emitted when a matrix condition number exceeds a configurable threshold. 

95 

96 Examples: 

97 >>> import warnings 

98 >>> warnings.warn("condition number 1e13", IllConditionedMatrixWarning) 

99 """ 

100 

101 

102def cond(matrix: np.ndarray, p: int | float | Literal["fro", "nuc"] | None = None) -> float: 

103 """Return the condition number of a matrix. 

104 

105 Returns ``nan`` if the matrix contains any non-finite (NaN or inf) entries. 

106 Otherwise delegates to :func:`numpy.linalg.cond`. 

107 

108 Args: 

109 matrix: Input matrix. 

110 p: Order of the norm used to compute the condition number. 

111 Accepts the same values as :func:`numpy.linalg.cond` 

112 (``None``, ``1``, ``-1``, ``2``, ``-2``, ``numpy.inf``, 

113 ``-numpy.inf``, ``'fro'``). Defaults to ``None`` which 

114 corresponds to the 2-norm (largest singular value divided by 

115 the smallest). 

116 

117 Returns: 

118 The condition number as a ``float``, or ``nan`` when the matrix 

119 contains non-finite entries. 

120 

121 Examples: 

122 >>> import numpy as np 

123 >>> cond(np.eye(3)) 

124 1.0 

125 >>> import math 

126 >>> math.isnan(cond(np.array([[float('nan'), 1.0], [1.0, 2.0]]))) 

127 True 

128 >>> cond(np.diag([1.0, 1e10]), p=1) 

129 10000000000.0 

130 """ 

131 if not np.all(np.isfinite(matrix)): 

132 return float("nan") 

133 return float(np.linalg.cond(matrix, p=p)) 

134 

135 

136def check_and_warn_condition(matrix: np.ndarray, threshold: float) -> None: 

137 """Emit IllConditionedMatrixWarning when the condition number exceeds threshold. 

138 

139 Args: 

140 matrix: Square matrix whose condition number is checked. 

141 threshold: Upper bound before a warning is issued. 

142 

143 Example: 

144 >>> import numpy as np 

145 >>> import warnings 

146 >>> with warnings.catch_warnings(record=True) as w: 

147 ... warnings.simplefilter("always") 

148 ... check_and_warn_condition(np.eye(2), 0.5) 

149 ... len(w) 

150 1 

151 """ 

152 c = cond(matrix) 

153 if c > threshold: 

154 warnings.warn( 

155 f"Matrix condition number {c:.3e} exceeds threshold {threshold:.3e}; " 

156 "results may be numerically unreliable.", 

157 IllConditionedMatrixWarning, 

158 stacklevel=3, 

159 )