Coverage for src/cvxcla/_lasso_validate.py: 100%

42 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-11 09:58 +0000

1"""Input validation for :class:`cvxcla.lasso.Lasso` construction. 

2 

3The LASSO accepts its quadratic form either as a dense design ``(x, y)`` or as a 

4``QuadraticForm`` operator plus the linear term ``X^T y``, optionally with linear 

5inequality constraints ``G beta <= h``. The shape and consistency checks for those 

6two input modes, plus the constraint check, are pure functions here so 

7``Lasso.__post_init__`` reduces to dispatching to them before handing off to the 

8tracer. 

9""" 

10 

11from __future__ import annotations 

12 

13import numpy as np 

14from numpy.typing import NDArray 

15 

16from .operators import QuadraticForm 

17 

18 

19def validate_operator_inputs( 

20 quad_form: QuadraticForm | None, 

21 linear: NDArray[np.float64] | None, 

22 x: NDArray[np.float64] | None, 

23 y: NDArray[np.float64] | None, 

24) -> NDArray[np.float64]: 

25 """Validate the operator-mode inputs ``quad_form`` and ``linear`` (``X^T y``). 

26 

27 Args: 

28 quad_form: The quadratic form ``H`` as a :class:`QuadraticForm` operator. 

29 linear: The linear term ``X^T y`` (must be the 1d vector). 

30 x: The dense design matrix, which must be absent in operator mode. 

31 y: The dense response vector, which must be absent in operator mode. 

32 

33 Returns: 

34 The validated ``linear`` term as a 1d ``float64`` array. 

35 

36 Raises: 

37 ValueError: If only one of ``quad_form``/``linear`` is given, a design 

38 ``(x, y)`` is also supplied, or ``linear`` is not the 1d ``X^T y``. 

39 """ 

40 if quad_form is None or linear is None: 

41 msg = "quad_form and linear (X^T y) must be provided together" 

42 raise ValueError(msg) 

43 _reject_conflicting_design(x, y) 

44 linear = np.asarray(linear, dtype=np.float64) 

45 if linear.ndim != 1: 

46 msg = f"linear must be the 1d vector X^T y, got shape {linear.shape}" 

47 raise ValueError(msg) 

48 return linear 

49 

50 

51def _reject_conflicting_design(x: NDArray[np.float64] | None, y: NDArray[np.float64] | None) -> None: 

52 """Reject a dense design ``(x, y)`` supplied alongside the operator inputs. 

53 

54 Args: 

55 x: The dense design matrix, which must be absent in operator mode. 

56 y: The dense response vector, which must be absent in operator mode. 

57 

58 Raises: 

59 ValueError: If either ``x`` or ``y`` is provided. 

60 """ 

61 if x is not None or y is not None: 

62 msg = "supply either a design (x, y) or an operator (quad_form, linear), not both" 

63 raise ValueError(msg) 

64 

65 

66def validate_design_inputs(x: NDArray[np.float64] | None, y: NDArray[np.float64] | None) -> None: 

67 """Validate the dense-design inputs ``x`` and ``y``. 

68 

69 Args: 

70 x: The design matrix of shape ``(m, n)``. 

71 y: The response vector of shape ``(m,)``. 

72 

73 Raises: 

74 ValueError: If ``x``/``y`` are missing, ``x`` is not a 2d design matrix, 

75 or ``y``'s length does not match ``x``'s row count. 

76 """ 

77 if x is None or y is None: 

78 msg = "provide a design (x, y) or an operator (quad_form, linear)" 

79 raise ValueError(msg) 

80 if x.ndim != 2: 

81 msg = f"x must be a 2d design matrix, got shape {x.shape}" 

82 raise ValueError(msg) 

83 if y.shape != (x.shape[0],): 

84 msg = f"y must have shape ({x.shape[0]},), got {y.shape}" 

85 raise ValueError(msg) 

86 

87 

88def validate_constraints( 

89 g: NDArray[np.float64] | None, 

90 h: NDArray[np.float64] | None, 

91 dimension: int, 

92 tol: float, 

93) -> None: 

94 """Validate the optional inequality constraints ``G beta <= h``. 

95 

96 Args: 

97 g: Inequality matrix ``G`` of ``G beta <= h`` (``None`` for the plain LASSO). 

98 h: Inequality right-hand side ``h`` (``None`` for the plain LASSO). 

99 dimension: The problem dimension ``n`` (number of features). 

100 tol: Tolerance below which an ``h`` entry counts as non-positive. 

101 

102 Raises: 

103 ValueError: If only one of ``g``/``h`` is given, their shapes are 

104 inconsistent with the problem dimension, or any ``h`` entry is not 

105 strictly positive (which would make ``beta = 0`` infeasible). 

106 """ 

107 if g is None and h is None: 

108 return 

109 if g is None or h is None: 

110 msg = "g and h must be provided together" 

111 raise ValueError(msg) 

112 _validate_constraint_shapes(g, h, dimension, tol) 

113 

114 

115def _validate_constraint_shapes( 

116 g: NDArray[np.float64], 

117 h: NDArray[np.float64], 

118 dimension: int, 

119 tol: float, 

120) -> None: 

121 """Validate the shapes and positivity of a fully-provided ``G beta <= h``. 

122 

123 Args: 

124 g: Inequality matrix ``G`` (both ``g`` and ``h`` known to be present). 

125 h: Inequality right-hand side ``h``. 

126 dimension: The problem dimension ``n`` (number of features). 

127 tol: Tolerance below which an ``h`` entry counts as non-positive. 

128 

129 Raises: 

130 ValueError: If ``g``'s shape is inconsistent with ``h`` and the problem 

131 dimension, or any ``h`` entry is not strictly positive. 

132 """ 

133 if g.shape != (h.shape[0], dimension): 

134 msg = f"g must have shape ({h.shape[0]}, {dimension}), got {g.shape}" 

135 raise ValueError(msg) 

136 if np.any(h <= tol): 

137 msg = "h must be strictly positive so beta = 0 is feasible (equality/zero-h needs a feasibility seed)" 

138 raise ValueError(msg)