Coverage for src/gwtransport/deposition_utils.py: 100%
13 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"""
2Utility Functions for the Deposition Module.
4This module provides the clipped-trapezoid integral helpers (``_clipped_linear_integral``
5and ``_positive_part_integral``) used by the deposition module's banded weight builder to
6integrate ``clip(y(x), y_lower, y_upper)`` over each cin bin of a streamtube's residence window.
8This file is part of gwtransport which is released under AGPL-3.0 license.
9See the ./LICENSE file or go to https://github.com/gwtransport/gwtransport/blob/main/LICENSE for full license details.
10"""
12import numpy as np
13import numpy.typing as npt
16def _positive_part_integral(
17 a: npt.NDArray[np.floating], b: npt.NDArray[np.floating], w: npt.NDArray[np.floating]
18) -> npt.NDArray[np.floating]:
19 """
20 Integrate max(f(x), 0) from x=0 to x=w where f is linear from a to b.
22 Parameters
23 ----------
24 a : ndarray
25 Function values at x=0.
26 b : ndarray
27 Function values at x=w.
28 w : ndarray
29 Integration width.
31 Returns
32 -------
33 ndarray
34 Integral values.
35 """
36 both_pos = (a > 0) & (b > 0)
38 abs_diff = np.abs(a - b)
39 # Sentinel ``1.0`` avoids division by zero in the ``excess**2 / (2*safe_diff)``
40 # branch when a == b; the surrounding ``np.where`` discards this branch
41 # whenever both endpoints have the same sign (where the trapezoid formula
42 # is used instead), so the sentinel value is never observed in the output.
43 safe_diff = np.where(abs_diff > 0, abs_diff, 1.0)
45 # When exactly one endpoint is positive, ``excess`` is that endpoint;
46 # when neither is positive it is 0. ``max(max(a, b), 0)`` yields both:
47 # the positive endpoint when only one is positive, 0 otherwise. The
48 # both-positive case is discarded by the ``np.where`` below.
49 excess = np.maximum(np.maximum(a, b), 0.0)
51 return np.where(
52 both_pos,
53 w * (a + b) / 2,
54 w * excess**2 / (2 * safe_diff),
55 )
58def _clipped_linear_integral(
59 a: npt.NDArray[np.floating],
60 b: npt.NDArray[np.floating],
61 w: npt.NDArray[np.floating],
62 lo: float,
63 hi: float,
64) -> npt.NDArray[np.floating]:
65 """
66 Integrate clip(f(x), lo, hi) from x=0 to x=w where f is linear from a to b.
68 Uses the identity ``clip(f) = f - max(f - hi, 0) + max(lo - f, 0)`` to
69 compute the exact integral analytically.
71 Parameters
72 ----------
73 a : ndarray
74 Function values at x=0.
75 b : ndarray
76 Function values at x=w.
77 w : ndarray
78 Integration width.
79 lo : float
80 Lower clipping bound.
81 hi : float
82 Upper clipping bound.
84 Returns
85 -------
86 ndarray
87 Integral values.
88 """
89 raw = w * (a + b) / 2
90 excess_above = _positive_part_integral(a - hi, b - hi, w)
91 deficit_below = _positive_part_integral(lo - a, lo - b, w)
92 return raw - excess_above + deficit_below