PyCDFt 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycdft/__init__.py ADDED
@@ -0,0 +1,59 @@
1
+ """PyCDFt -- CDF-t statistical downscaling / bias correction (numpy only).
2
+
3
+ A vectorized, grid-ready implementation of the CDF-t (Cumulative
4
+ Distribution Function transform) method of Michelangeli, Vrac & Loukos
5
+ (2009), with the delta-shift and tail conventions of the R ``CDFt`` package
6
+ and ibicus. See the README and ``docs/design.md`` for the math and the
7
+ comparison with the reference implementations.
8
+
9
+ An optional torch backend (:mod:`pycdft.gpu`) mirrors this API and runs the
10
+ same math on CUDA / ROCm / MPS / CPU devices; it is only available when
11
+ torch is installed (``pip install PyCDFt[gpu]``) and never affects the pure
12
+ numpy core.
13
+ """
14
+
15
+ from .api import CDFt, cdft_transform
16
+ from .core import (Calibration, apply_cdft, apply_cdft_hist, calibrate,
17
+ calibrate_hist, delta_from_means, histograms, transfer)
18
+ from .math import (ecdf, ecdf_at, ecdf_hist, hist_quantile,
19
+ hist_quantile_pc, histogram_accumulate, interp_transfer,
20
+ nanquantile, quantile, quantile_from_sorted,
21
+ quantile_grid, searchsorted_columns, windowed_slope)
22
+
23
+ try: # optional torch backend
24
+ from . import gpu as gpu
25
+ GPU_AVAILABLE = bool(gpu.TORCH_AVAILABLE)
26
+ except Exception: # pragma: no cover - torch-less install
27
+ gpu = None # type: ignore[assignment]
28
+ GPU_AVAILABLE = False
29
+
30
+ __version__ = "0.1.0"
31
+
32
+ __all__ = [
33
+ "CDFt",
34
+ "cdft_transform",
35
+ "Calibration",
36
+ "calibrate",
37
+ "calibrate_hist",
38
+ "apply_cdft",
39
+ "apply_cdft_hist",
40
+ "histograms",
41
+ "delta_from_means",
42
+ "quantile_grid",
43
+ "searchsorted_columns",
44
+ "ecdf",
45
+ "ecdf_at",
46
+ "quantile",
47
+ "nanquantile",
48
+ "quantile_from_sorted",
49
+ "windowed_slope",
50
+ "interp_transfer",
51
+ "transfer",
52
+ "histogram_accumulate",
53
+ "hist_quantile",
54
+ "hist_quantile_pc",
55
+ "ecdf_hist",
56
+ "gpu",
57
+ "GPU_AVAILABLE",
58
+ "__version__",
59
+ ]
pycdft/api.py ADDED
@@ -0,0 +1,199 @@
1
+ """High-level CDF-t API.
2
+
3
+ The :class:`CDFt` class wraps the low-level pipeline
4
+ (:func:`pycdft.core.calibrate` + :func:`pycdft.core.apply_cdft`) in a
5
+ scikit-learn-style ``fit`` / ``transform`` / ``fit_transform`` interface and
6
+ works on plain numpy arrays: 1-D series ``(T,)`` or fields ``(T, N)`` /
7
+ ``(T, Y, X)`` (the trailing dimensions are flattened into independent
8
+ series / grid cells, and the output shape matches the input).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+
15
+ from .core import (Calibration, apply_cdft, apply_cdft_hist, calibrate,
16
+ calibrate_hist)
17
+ from .math import quantile_grid
18
+
19
+ __all__ = ["CDFt", "cdft_transform"]
20
+
21
+ _SHIFT_MODES = ("additive", "multiplicative", "none")
22
+ _TAILS = ("linear", "clip")
23
+
24
+
25
+ class CDFt:
26
+ """CDF-t statistical downscaling / bias correction (Michelangeli et al.
27
+ 2009; Vrac et al. 2016).
28
+
29
+ Parameters
30
+ ----------
31
+ n_quantiles : int, default 1001
32
+ Number of points of the uniform probability grid ``p`` (endpoints 0
33
+ and 1 included), on which the transfer curve is evaluated.
34
+ tail : {"linear", "clip"}, default "linear"
35
+ Behaviour outside the saturated region of the transfer:
36
+ "linear" extrapolates with a 30-rank windowed least-squares slope
37
+ (capped at 2x the median interior slope); "clip" holds the constant
38
+ end values of the transfer.
39
+ delta_shift : {"additive", "multiplicative", "none"}, default "additive"
40
+ Per-cell mean shift applied to the model series (calibration and
41
+ target) before the transfer -- the standard CDF-t practice:
42
+
43
+ - "additive": ``x -> x + (mean(obs) - mean(cm_hist))``
44
+ (default, as in the R ``CDFt`` package and ibicus);
45
+ - "multiplicative": ``x -> x * (mean(obs) / mean(cm_hist))``
46
+ (for strictly positive fluxes; falls back to 1.0 where the model
47
+ mean is ~0);
48
+ - "none": no shift (ibicus ``"no_shift"``).
49
+
50
+ The output is left in the shifted frame: the mean bias
51
+ model-vs-observation is removed, the model change between the
52
+ calibration and target periods is preserved.
53
+ use_histograms : bool, default False
54
+ Accumulate the target CDF as a per-cell histogram instead of an
55
+ exact sort (bounded memory for very long records; approximate).
56
+ n_bins : int, default 2000
57
+ Number of bins per cell in histogram mode.
58
+ workers : int, default 1
59
+ Threads for the per-cell kernels (identical results; numpy releases
60
+ the GIL, so ``workers > 1`` speeds up large grids).
61
+
62
+ Attributes
63
+ ----------
64
+ calibration_ : Calibration or None
65
+ The calibration object from the last :meth:`fit` /
66
+ :meth:`fit_transform` call (``None`` before fitting).
67
+ probability_grid : (Q,) np.ndarray
68
+ The probability grid ``linspace(0, 1, n_quantiles)``.
69
+
70
+ Notes
71
+ -----
72
+ NaN handling: NaNs are excluded from the ECDF counts and from the
73
+ quantile interpolation; NaN inputs pass through as NaN; series (cells)
74
+ whose calibration is empty (all-NaN) map entirely to NaN. The core is
75
+ pure numpy (no scipy, no xarray); the memory-lean design keeps the
76
+ calibration arrays in the input dtype (float32 stays float32) with
77
+ float64 per-chunk math.
78
+
79
+ Examples
80
+ --------
81
+ >>> import numpy as np
82
+ >>> from pycdft import CDFt
83
+ >>> rng = np.random.default_rng(7)
84
+ >>> obs = rng.normal(0.0, 1.0, 1000) # observations (calibration)
85
+ >>> cm_hist = rng.normal(3.0, 1.0, 1000) # model, calibration period
86
+ >>> cm_future = rng.normal(4.0, 1.0, 1000) # model, target period
87
+ >>> ds = CDFt().fit_transform(obs, cm_hist, cm_future)
88
+ >>> ds.shape
89
+ (1000,)
90
+ """
91
+
92
+ def __init__(self, n_quantiles: int = 1001, tail: str = "linear",
93
+ delta_shift: str = "additive", use_histograms: bool = False,
94
+ n_bins: int = 2000, workers: int = 1):
95
+ n_quantiles = int(n_quantiles)
96
+ if n_quantiles < 2:
97
+ raise ValueError(f"n_quantiles must be >= 2, got {n_quantiles}")
98
+ if delta_shift not in _SHIFT_MODES:
99
+ raise ValueError(f"delta_shift must be one of {_SHIFT_MODES}, "
100
+ f"got {delta_shift!r}")
101
+ if tail not in _TAILS:
102
+ raise ValueError(f"tail must be one of {_TAILS}, got {tail!r}")
103
+ if n_bins < 2:
104
+ raise ValueError(f"n_bins must be >= 2, got {n_bins}")
105
+ self.n_quantiles = n_quantiles
106
+ self.tail = tail
107
+ self.delta_shift = delta_shift
108
+ self.use_histograms = bool(use_histograms)
109
+ self.n_bins = int(n_bins)
110
+ self.workers = int(workers)
111
+ self.calibration_: Calibration | None = None
112
+
113
+ # ------------------------------------------------------------------
114
+ @property
115
+ def probability_grid(self) -> np.ndarray:
116
+ """The uniform probability grid ``linspace(0, 1, n_quantiles)``."""
117
+ return quantile_grid(self.n_quantiles)
118
+
119
+ # ------------------------------------------------------------------
120
+ def fit(self, obs, cm_hist) -> "CDFt":
121
+ """Fit the calibration on ``(obs, cm_hist)``.
122
+
123
+ Parameters
124
+ ----------
125
+ obs : (T,) or (T, ...) array
126
+ Observations (fine / high-resolution series) over the
127
+ calibration period.
128
+ cm_hist : (T,) or (T, ...) array
129
+ Model (coarse / low-resolution) series over the same period.
130
+
131
+ Returns
132
+ -------
133
+ self
134
+ """
135
+ self.calibration_ = calibrate(cm_hist, obs, p=self.probability_grid,
136
+ delta_shift=self.delta_shift,
137
+ workers=self.workers)
138
+ return self
139
+
140
+ def transform(self, cm_future):
141
+ """Apply the fitted calibration to the target model series.
142
+
143
+ Parameters
144
+ ----------
145
+ cm_future : (T,) or (T, ...) array
146
+ Model series to downscale / bias-correct.
147
+
148
+ Returns
149
+ -------
150
+ Array of the same shape as ``cm_future`` (in the shifted frame).
151
+ """
152
+ if self.calibration_ is None:
153
+ raise RuntimeError("CDFt is not fitted: call fit() or "
154
+ "fit_transform() first")
155
+ if self.use_histograms:
156
+ return apply_cdft_hist(cm_future, self.calibration_,
157
+ p=self.probability_grid, n_bins=self.n_bins,
158
+ tail=self.tail, workers=self.workers)
159
+ return apply_cdft(cm_future, self.calibration_, p=self.probability_grid,
160
+ tail=self.tail, workers=self.workers)
161
+
162
+ def fit_transform(self, obs, cm_hist, cm_future):
163
+ """Fit on ``(obs, cm_hist)`` and transform ``cm_future`` in one call.
164
+
165
+ Parameters
166
+ ----------
167
+ obs : (T,) or (T, ...) array
168
+ Observations over the calibration period.
169
+ cm_hist : (T,) or (T, ...) array
170
+ Model series over the calibration period.
171
+ cm_future : (T,) or (T, ...) array
172
+ Model series to downscale / bias-correct.
173
+
174
+ Returns
175
+ -------
176
+ Array of the same shape as ``cm_future``.
177
+ """
178
+ return self.fit(obs, cm_hist).transform(cm_future)
179
+
180
+ # ------------------------------------------------------------------
181
+ def __repr__(self) -> str:
182
+ return (f"CDFt(n_quantiles={self.n_quantiles}, tail={self.tail!r}, "
183
+ f"delta_shift={self.delta_shift!r}, "
184
+ f"use_histograms={self.use_histograms}, "
185
+ f"n_bins={self.n_bins}, workers={self.workers})")
186
+
187
+
188
+ def cdft_transform(obs, cm_hist, cm_future, n_quantiles: int = 1001,
189
+ tail: str = "linear", delta_shift: str = "additive",
190
+ use_histograms: bool = False, n_bins: int = 2000,
191
+ workers: int = 1):
192
+ """One-shot CDF-t transform (no persistent calibration object).
193
+
194
+ Equivalent to ``CDFt(...).fit_transform(obs, cm_hist, cm_future)``;
195
+ returns the same-shaped output array.
196
+ """
197
+ return CDFt(n_quantiles=n_quantiles, tail=tail, delta_shift=delta_shift,
198
+ use_histograms=use_histograms, n_bins=n_bins,
199
+ workers=workers).fit_transform(obs, cm_hist, cm_future)