Coverage for src/gwtransport/_validation.py: 100%
35 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 21:17 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 21:17 +0000
1"""
2Composable input-validation atoms for transport-module entry points.
4The public ``infiltration_to_extraction`` / ``extraction_to_infiltration`` functions in
5:mod:`gwtransport.advection`, :mod:`gwtransport.diffusion`, and
6:mod:`gwtransport.deposition` share a small set of input-validation invariants
7(bin-edge parity, NaN-free arrays, non-negative flow, positive physical
8parameters). The atoms here factor those invariants once so that each module
9exposes a single ``_validate_<module>_inputs`` wrapper composing the atoms with
10module-specific error-message wording and ordering.
12Each atom is keyword-only (except for the array under test) and raises
13``ValueError`` with a default message; the optional ``message`` keyword lets the
14module wrapper preserve the historical wording verbatim so that downstream
15``pytest.raises(..., match=...)`` tests keep passing without modification.
17This module has no public API; importers are the transport modules themselves
18plus ``tests/src/test_validation.py``.
19"""
21from __future__ import annotations
23import numpy as np
24import numpy.typing as npt
25import pandas as pd # noqa: TC002 -- pandas is a hard runtime dependency; import unconditionally
28def _validate_tedges_parity(
29 tedges: pd.DatetimeIndex,
30 values: npt.ArrayLike,
31 *,
32 tedges_name: str,
33 values_name: str,
34) -> None:
35 """Validate bin-edge parity: ``len(tedges) == len(values) + 1``.
37 Parameters
38 ----------
39 tedges : DatetimeIndex
40 Bin edges (length ``n + 1``).
41 values : array-like
42 Bin-constant values (length ``n``).
43 tedges_name, values_name : str
44 Names used in the error message, e.g. ``"tedges"`` and ``"cin"``.
46 Raises
47 ------
48 ValueError
49 If ``len(tedges) != len(values) + 1``.
50 """
51 n_values = np.asarray(values).shape[0]
52 if len(tedges) != n_values + 1:
53 msg = f"{tedges_name} must have one more element than {values_name}"
54 raise ValueError(msg)
57def _validate_no_nan(
58 arr: npt.ArrayLike,
59 *,
60 name: str,
61 message: str | None = None,
62) -> None:
63 """Validate that ``arr`` contains no NaN values.
65 Parameters
66 ----------
67 arr : array-like
68 Array to check.
69 name : str
70 Variable name used in the default error message.
71 message : str, optional
72 Override the default ``"{name} contains NaN values, which are not allowed"``
73 wording. Used by module wrappers that need to preserve historical strings
74 pinned by ``pytest.raises(..., match=...)`` tests.
76 Raises
77 ------
78 ValueError
79 If any element of ``arr`` is NaN.
80 """
81 if np.any(np.isnan(np.asarray(arr))):
82 msg = message if message is not None else f"{name} contains NaN values, which are not allowed"
83 raise ValueError(msg)
86def _validate_non_negative_array(
87 arr: npt.ArrayLike,
88 *,
89 name: str,
90 message: str | None = None,
91) -> None:
92 """Validate that every element of ``arr`` is finite and non-negative (``>= 0``).
94 Zeros are allowed. The companion ``_validate_positive_array`` rejects zeros too. NaN and
95 ``+inf`` are rejected: both pass every ``< 0`` comparison, so a bare inequality would let
96 them slip through and poison the downstream computation.
98 Parameters
99 ----------
100 arr : array-like
101 Array to check (any shape).
102 name : str
103 Variable name used in the default error message.
104 message : str, optional
105 Override the default ``"{name} must be non-negative"`` wording.
107 Raises
108 ------
109 ValueError
110 If any element of ``arr`` is negative or non-finite (NaN or infinite).
111 """
112 a = np.asarray(arr, dtype=float)
113 if not np.all(np.isfinite(a) & (a >= 0.0)):
114 msg = message if message is not None else f"{name} must be non-negative"
115 raise ValueError(msg)
118def _validate_positive_array(
119 arr: npt.ArrayLike,
120 *,
121 name: str,
122 message: str | None = None,
123) -> None:
124 """Validate that every element of ``arr`` is finite and strictly positive (``> 0``).
126 NaN and ``+inf`` are rejected: both pass every ``<= 0`` comparison, so a bare inequality
127 would let them slip through and poison the downstream computation.
129 Parameters
130 ----------
131 arr : array-like
132 Array to check.
133 name : str
134 Variable name used in the default error message.
135 message : str, optional
136 Override the default ``"{name} must be positive"`` wording.
138 Raises
139 ------
140 ValueError
141 If any element of ``arr`` is ``<= 0`` or non-finite (NaN or infinite).
142 """
143 a = np.asarray(arr, dtype=float)
144 if np.any(~np.isfinite(a)) or np.any(a <= 0.0):
145 msg = message if message is not None else f"{name} must be positive"
146 raise ValueError(msg)
149def _validate_positive_scalar(
150 value: float,
151 *,
152 name: str,
153 message: str | None = None,
154) -> None:
155 """Validate that ``value`` is finite and strictly positive (``> 0``).
157 NaN and ``+inf`` are rejected: both pass the bare ``<= 0`` comparison, so an unchecked
158 inequality would let them slip through and poison the downstream computation (e.g. a
159 ``+inf`` thickness silently zeroed the deposition output).
161 Parameters
162 ----------
163 value : float
164 Scalar to check.
165 name : str
166 Variable name used in the default error message.
167 message : str, optional
168 Override the default ``"{name} must be positive, got {value}"`` wording.
170 Raises
171 ------
172 ValueError
173 If ``value <= 0`` or is non-finite (NaN or infinite).
174 """
175 if not np.isfinite(value) or value <= 0:
176 msg = message if message is not None else f"{name} must be positive, got {value}"
177 raise ValueError(msg)
180def _validate_retardation_factor(value: float) -> None:
181 """Validate that the retardation factor is ``>= 1`` (anti-retardation is unphysical).
183 The check is written as ``not value >= 1.0`` rather than ``value < 1.0`` so that NaN is
184 rejected too: ``NaN >= 1.0`` is False, so the bare ``< 1.0`` form would let NaN pass and
185 silently propagate an all-NaN transport output.
187 Parameters
188 ----------
189 value : float
190 Retardation factor to check.
192 Raises
193 ------
194 ValueError
195 If ``value`` is NaN or ``value < 1.0``.
196 """
197 if not value >= 1.0:
198 msg = "retardation_factor must be >= 1.0"
199 raise ValueError(msg)
202def _validate_scalar_or_matching_length(
203 arr: npt.ArrayLike,
204 *,
205 name: str,
206 expected_len: int,
207 ref_name: str,
208) -> None:
209 """Validate length: ``len(arr) == expected_len``.
211 Intended for inputs that the caller accepts as either a scalar or an
212 array matching some reference length; the caller is expected to have
213 broadcast size-1 arrays to ``expected_len`` *before* calling this atom.
214 The error message refers to the user-facing scalar-or-matching contract.
216 Parameters
217 ----------
218 arr : array-like
219 Array to check (already broadcast from scalar form by the caller).
220 name : str
221 Variable name used in the error message.
222 expected_len : int
223 Required length.
224 ref_name : str
225 Name of the reference array whose length is the contract.
227 Raises
228 ------
229 ValueError
230 If ``len(arr) != expected_len``.
231 """
232 if np.asarray(arr).shape[0] != expected_len:
233 msg = f"{name} must be a scalar or have same length as {ref_name}"
234 raise ValueError(msg)