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 +59 -0
- pycdft/api.py +199 -0
- pycdft/core.py +489 -0
- pycdft/gpu/__init__.py +42 -0
- pycdft/gpu/_torch.py +29 -0
- pycdft/gpu/api.py +634 -0
- pycdft/gpu/devices.py +160 -0
- pycdft/gpu/kernels.py +554 -0
- pycdft/math.py +561 -0
- pycdft/py.typed +0 -0
- pycdft-0.1.0.dist-info/METADATA +289 -0
- pycdft-0.1.0.dist-info/RECORD +15 -0
- pycdft-0.1.0.dist-info/WHEEL +5 -0
- pycdft-0.1.0.dist-info/licenses/LICENSE +201 -0
- pycdft-0.1.0.dist-info/top_level.txt +1 -0
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)
|