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
« 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.
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"""
11from __future__ import annotations
13import numpy as np
14from numpy.typing import NDArray
16from .operators import QuadraticForm
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``).
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.
33 Returns:
34 The validated ``linear`` term as a 1d ``float64`` array.
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
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.
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.
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)
66def validate_design_inputs(x: NDArray[np.float64] | None, y: NDArray[np.float64] | None) -> None:
67 """Validate the dense-design inputs ``x`` and ``y``.
69 Args:
70 x: The design matrix of shape ``(m, n)``.
71 y: The response vector of shape ``(m,)``.
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)
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``.
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.
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)
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``.
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.
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)