klimapy 1.1.0__py3-none-win_amd64.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.
- klimapy/BCSD/BiasCorrection.py +209 -0
- klimapy/BCSD/BiasCorrectionSpatialDownscaling.py +89 -0
- klimapy/BCSD/SpatialDownscaling.py +198 -0
- klimapy/BCSD/__init__.py +20 -0
- klimapy/ETCCDI/ETCCDI.py +684 -0
- klimapy/ETCCDI/__init__.py +150 -0
- klimapy/Interpolation/__init__.py +23 -0
- klimapy/Interpolation/spatial_interpolation.py +233 -0
- klimapy/PrecipitationVariability/__init__.py +27 -0
- klimapy/PrecipitationVariability/precip_variability_netcdf.py +167 -0
- klimapy/PrecipitationVariability/precipvariability.py +25 -0
- klimapy/Supporting_Tools/CSM.py +69 -0
- klimapy/Supporting_Tools/NCtoTIFF.py +133 -0
- klimapy/Supporting_Tools/__init__.py +23 -0
- klimapy/__init__.py +54 -0
- klimapy/_backends/__init__.py +5 -0
- klimapy/_backends/_native_core.pyd +0 -0
- klimapy/_backends/rust.py +582 -0
- klimapy/_compat.py +43 -0
- klimapy/_core/__init__.py +1 -0
- klimapy/_core/common.py +161 -0
- klimapy/_core/engine.py +177 -0
- klimapy/_core/native_adapter.py +122 -0
- klimapy/_core/validation.py +164 -0
- klimapy/_licenses/splinefit-APACHE.txt +176 -0
- klimapy/_licenses/splinefit-MIT.txt +21 -0
- klimapy/_licenses/splinefit-NOTICE.md +26 -0
- klimapy/native/__init__.py +36 -0
- klimapy/native/correction.py +96 -0
- klimapy/native/field.py +309 -0
- klimapy/native/geometry.py +203 -0
- klimapy/native/io.py +448 -0
- klimapy/native/linalg.py +248 -0
- klimapy/native/statistics.py +142 -0
- klimapy/workflows/__init__.py +5 -0
- klimapy/workflows/batch.py +48 -0
- klimapy-1.1.0.dist-info/METADATA +110 -0
- klimapy-1.1.0.dist-info/RECORD +41 -0
- klimapy-1.1.0.dist-info/WHEEL +5 -0
- klimapy-1.1.0.dist-info/licenses/LICENSE +21 -0
- klimapy-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Calendar-aligned precipitation quantile correction using Rust FITPACK."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from contextlib import ExitStack
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..native import Field, TimeAxis
|
|
10
|
+
from ..native.correction import Transfer
|
|
11
|
+
from .._core.common import WINDOW_SIZE, N_QUANTILES
|
|
12
|
+
from .._core.native_adapter import open_precip_dataset
|
|
13
|
+
from .._core.engine import (
|
|
14
|
+
calendar_info,
|
|
15
|
+
align_reference,
|
|
16
|
+
mapped_days,
|
|
17
|
+
GridWeights,
|
|
18
|
+
allocate,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
DEBUG_CHECK = False
|
|
23
|
+
__all__ = ["BC", "create_cdf_spline", "close_result"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_cdf_spline(gcm_data, obs_data, n_quantiles=50):
|
|
27
|
+
try:
|
|
28
|
+
return Transfer(gcm_data, obs_data, n_quantiles)
|
|
29
|
+
except ValueError as exc:
|
|
30
|
+
if "20 finite observations" in str(exc):
|
|
31
|
+
return None
|
|
32
|
+
raise
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def BC(
|
|
36
|
+
obs_path,
|
|
37
|
+
gcm_hist_path,
|
|
38
|
+
gcm_fut_path,
|
|
39
|
+
model_name,
|
|
40
|
+
ref_start_year,
|
|
41
|
+
ref_end_year,
|
|
42
|
+
*,
|
|
43
|
+
strict=False,
|
|
44
|
+
time_chunk_size=128,
|
|
45
|
+
work_dir=None,
|
|
46
|
+
window_size=None,
|
|
47
|
+
n_quantiles=None,
|
|
48
|
+
):
|
|
49
|
+
buffers = []
|
|
50
|
+
try:
|
|
51
|
+
window = WINDOW_SIZE if window_size is None else window_size
|
|
52
|
+
quantiles = N_QUANTILES if n_quantiles is None else n_quantiles
|
|
53
|
+
if (
|
|
54
|
+
isinstance(time_chunk_size, bool)
|
|
55
|
+
or not isinstance(time_chunk_size, int)
|
|
56
|
+
or time_chunk_size < 1
|
|
57
|
+
or isinstance(window, bool)
|
|
58
|
+
or not isinstance(window, int)
|
|
59
|
+
or window < 0
|
|
60
|
+
or isinstance(quantiles, bool)
|
|
61
|
+
or not isinstance(quantiles, int)
|
|
62
|
+
or quantiles < 4
|
|
63
|
+
):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"Positive time_chunk_size, nonnegative window_size and n_quantiles>=4 required"
|
|
66
|
+
)
|
|
67
|
+
with ExitStack() as stack:
|
|
68
|
+
obs, historical, future = [
|
|
69
|
+
stack.enter_context(open_precip_dataset(p))
|
|
70
|
+
for p in (obs_path, gcm_hist_path, gcm_fut_path)
|
|
71
|
+
]
|
|
72
|
+
for d in ("lat", "lon"):
|
|
73
|
+
if not np.array_equal(historical.coords[d], future.coords[d]):
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"Historical and future model grids must match exactly"
|
|
76
|
+
)
|
|
77
|
+
cal, ndays = calendar_info(historical.time)
|
|
78
|
+
if calendar_info(future.time)[0] != cal:
|
|
79
|
+
raise ValueError("Historical and future calendars must match")
|
|
80
|
+
oi, hi = align_reference(
|
|
81
|
+
obs.time, historical.time, ref_start_year, ref_end_year
|
|
82
|
+
)
|
|
83
|
+
weights = stack.enter_context(
|
|
84
|
+
GridWeights(
|
|
85
|
+
obs.coords["lat"],
|
|
86
|
+
obs.coords["lon"],
|
|
87
|
+
historical.coords["lat"],
|
|
88
|
+
historical.coords["lon"],
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
reference = allocate(
|
|
92
|
+
(len(oi), *historical.shape[1:]), np.float64, work_dir, "reference"
|
|
93
|
+
)
|
|
94
|
+
buffers.append(reference)
|
|
95
|
+
for start in range(0, len(oi), time_chunk_size):
|
|
96
|
+
indices = oi[start : start + time_chunk_size]
|
|
97
|
+
reference[start : start + len(indices)] = weights.apply(
|
|
98
|
+
obs.read(time=indices)
|
|
99
|
+
)
|
|
100
|
+
ref_time = TimeAxis(
|
|
101
|
+
historical.coords["time"][hi],
|
|
102
|
+
historical.coordinate_attrs.get("time", {}),
|
|
103
|
+
)
|
|
104
|
+
reference_days = mapped_days(ref_time, cal)
|
|
105
|
+
history_days, future_days = (
|
|
106
|
+
mapped_days(historical.time, cal),
|
|
107
|
+
mapped_days(future.time, cal),
|
|
108
|
+
)
|
|
109
|
+
windows = {}
|
|
110
|
+
for day in range(1, ndays + 1):
|
|
111
|
+
distance = np.abs(reference_days - day)
|
|
112
|
+
windows[day] = np.flatnonzero(
|
|
113
|
+
(reference_days > 0)
|
|
114
|
+
& (np.minimum(distance, ndays - distance) <= window)
|
|
115
|
+
)
|
|
116
|
+
outputs = [
|
|
117
|
+
allocate(a.shape, np.float64, work_dir, "corrected")
|
|
118
|
+
for a in (historical, future)
|
|
119
|
+
]
|
|
120
|
+
buffers.extend(outputs)
|
|
121
|
+
for out in outputs:
|
|
122
|
+
out[:] = np.nan
|
|
123
|
+
stats = {
|
|
124
|
+
"matching_reference_days": len(oi),
|
|
125
|
+
"fitted_functions": 0,
|
|
126
|
+
"unfitted_functions": 0,
|
|
127
|
+
"approximate_functions": 0,
|
|
128
|
+
"reference_period": [ref_start_year, ref_end_year],
|
|
129
|
+
"calendar": cal,
|
|
130
|
+
}
|
|
131
|
+
for row in range(historical.shape[1]):
|
|
132
|
+
histrow, futrow = historical.read(lat=row), future.read(lat=row)
|
|
133
|
+
for col in range(historical.shape[2]):
|
|
134
|
+
if not np.isfinite(reference[:, row, col]).any():
|
|
135
|
+
stats["unfitted_functions"] += ndays
|
|
136
|
+
continue
|
|
137
|
+
train = histrow[hi, col]
|
|
138
|
+
for day, indices in windows.items():
|
|
139
|
+
function = create_cdf_spline(
|
|
140
|
+
train[indices], reference[indices, row, col], quantiles
|
|
141
|
+
)
|
|
142
|
+
stats[
|
|
143
|
+
"fitted_functions"
|
|
144
|
+
if function is not None
|
|
145
|
+
else "unfitted_functions"
|
|
146
|
+
] += 1
|
|
147
|
+
if (
|
|
148
|
+
function is not None
|
|
149
|
+
and getattr(function, "fit_status", 0) > 0
|
|
150
|
+
):
|
|
151
|
+
stats["approximate_functions"] += 1
|
|
152
|
+
try:
|
|
153
|
+
for series, days, out in [
|
|
154
|
+
(histrow[:, col], history_days, outputs[0]),
|
|
155
|
+
(futrow[:, col], future_days, outputs[1]),
|
|
156
|
+
]:
|
|
157
|
+
mask = days == day
|
|
158
|
+
out[mask, row, col] = (
|
|
159
|
+
function(series[mask])
|
|
160
|
+
if function is not None
|
|
161
|
+
else series[mask]
|
|
162
|
+
)
|
|
163
|
+
finally:
|
|
164
|
+
if function is not None:
|
|
165
|
+
function.close()
|
|
166
|
+
answer = {
|
|
167
|
+
"corrected_hist_pr": historical.result(outputs[0]),
|
|
168
|
+
"corrected_fut_pr": future.result(outputs[1]),
|
|
169
|
+
"calendar_type": cal,
|
|
170
|
+
"max_doy": ndays,
|
|
171
|
+
"gcm_time_coord": "time",
|
|
172
|
+
"gcm_lat_coord": "lat",
|
|
173
|
+
"gcm_lon_coord": "lon",
|
|
174
|
+
"original_hist_path": str(gcm_hist_path),
|
|
175
|
+
"original_fut_path": str(gcm_fut_path),
|
|
176
|
+
"model_name": model_name,
|
|
177
|
+
"diagnostics": stats,
|
|
178
|
+
}
|
|
179
|
+
for array in (answer["corrected_hist_pr"], answer["corrected_fut_pr"]):
|
|
180
|
+
array.attrs["calibration_period"] = f"{ref_start_year}-{ref_end_year}"
|
|
181
|
+
_release(reference, remove=True)
|
|
182
|
+
return answer
|
|
183
|
+
except Exception:
|
|
184
|
+
for buffer in buffers:
|
|
185
|
+
_release(buffer, remove=True)
|
|
186
|
+
if strict:
|
|
187
|
+
raise
|
|
188
|
+
logger.exception("Bias correction failed")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _release(values, *, remove=False):
|
|
193
|
+
while (
|
|
194
|
+
not isinstance(values, np.memmap) and getattr(values, "base", None) is not None
|
|
195
|
+
):
|
|
196
|
+
values = values.base
|
|
197
|
+
if isinstance(values, np.memmap):
|
|
198
|
+
path = values.filename
|
|
199
|
+
if not values._mmap.closed:
|
|
200
|
+
values._mmap.close()
|
|
201
|
+
if remove:
|
|
202
|
+
Path(path).unlink(missing_ok=True)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def close_result(result, *, remove_work_files=False):
|
|
206
|
+
if result:
|
|
207
|
+
for key in ("corrected_hist_pr", "corrected_fut_pr"):
|
|
208
|
+
if key in result:
|
|
209
|
+
_release(result[key].values, remove=remove_work_files)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""BCSD 一体化入口及旧导入路径;计算实现位于同一子包下的 BiasCorrection 和 SpatialDownscaling。"""
|
|
2
|
+
|
|
3
|
+
from .BiasCorrection import BC, create_cdf_spline, close_result
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
from .SpatialDownscaling import SD, area_weighted_regrid, fft_smooth_three_harmonics
|
|
9
|
+
from .._core.common import (
|
|
10
|
+
WINDOW_SIZE,
|
|
11
|
+
N_QUANTILES,
|
|
12
|
+
MIN_PRECIP,
|
|
13
|
+
MAX_RATIO,
|
|
14
|
+
MIN_RATIO,
|
|
15
|
+
DEBUG_CHECK,
|
|
16
|
+
DEBUG_PRINT_SAMPLES,
|
|
17
|
+
calculate_bounds,
|
|
18
|
+
get_precip_var_name,
|
|
19
|
+
convert_precip_to_mm_day,
|
|
20
|
+
cftime_to_datetime,
|
|
21
|
+
parse_cftime_date,
|
|
22
|
+
detect_calendar_type,
|
|
23
|
+
calculate_doy,
|
|
24
|
+
_safe_min,
|
|
25
|
+
_safe_max,
|
|
26
|
+
_safe_mean,
|
|
27
|
+
print_array_diagnostics,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def BCSD(
|
|
32
|
+
obs_path,
|
|
33
|
+
gcm_hist_path,
|
|
34
|
+
gcm_fut_path,
|
|
35
|
+
model_name,
|
|
36
|
+
output_dir,
|
|
37
|
+
ref_start_year,
|
|
38
|
+
ref_end_year,
|
|
39
|
+
*,
|
|
40
|
+
strict=False,
|
|
41
|
+
bc_options=None,
|
|
42
|
+
sd_options=None,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
BCSD完整流程:先BC后SD
|
|
46
|
+
"""
|
|
47
|
+
logger.info(f"\n处理模式: {model_name}")
|
|
48
|
+
logger.info("=" * 60)
|
|
49
|
+
|
|
50
|
+
# 第一步:偏差校正 (BC)
|
|
51
|
+
bc_result = BC(
|
|
52
|
+
obs_path=obs_path,
|
|
53
|
+
gcm_hist_path=gcm_hist_path,
|
|
54
|
+
gcm_fut_path=gcm_fut_path,
|
|
55
|
+
model_name=model_name,
|
|
56
|
+
ref_start_year=ref_start_year,
|
|
57
|
+
ref_end_year=ref_end_year,
|
|
58
|
+
**dict(bc_options or {}, strict=strict),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not bc_result:
|
|
62
|
+
logger.info(f"✗ {model_name} BC失败,停止处理")
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# 第二步:空间降尺度 (SD)
|
|
66
|
+
try:
|
|
67
|
+
sd_result = SD(
|
|
68
|
+
obs_path=obs_path,
|
|
69
|
+
bc_result=bc_result,
|
|
70
|
+
output_dir=output_dir,
|
|
71
|
+
ref_start_year=ref_start_year,
|
|
72
|
+
ref_end_year=ref_end_year,
|
|
73
|
+
**dict(sd_options or {}, strict=strict),
|
|
74
|
+
)
|
|
75
|
+
finally:
|
|
76
|
+
close_result(bc_result, remove_work_files=True)
|
|
77
|
+
|
|
78
|
+
if not sd_result:
|
|
79
|
+
logger.info(f"✗ {model_name} SD失败")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
logger.info(f"✓ {model_name} BCSD完整流程完成!")
|
|
83
|
+
logger.info("=" * 60)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"model_name": model_name,
|
|
87
|
+
"hist_bcsd_path": sd_result["hist_bcsd_path"],
|
|
88
|
+
"fut_bcsd_path": sd_result["fut_bcsd_path"],
|
|
89
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Native streamed spatial downscaling; climatology and harmonics run in Rust."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from contextlib import ExitStack
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .._backends import rust as _accelerate
|
|
11
|
+
from ..native.correction import smooth
|
|
12
|
+
from .._core.common import MIN_PRECIP, MIN_RATIO, MAX_RATIO
|
|
13
|
+
from .._core.native_adapter import open_precip_dataset
|
|
14
|
+
from .._core.engine import (
|
|
15
|
+
read_precip,
|
|
16
|
+
mapped_days,
|
|
17
|
+
calendar_info,
|
|
18
|
+
GridWeights,
|
|
19
|
+
NetCDFWriter,
|
|
20
|
+
allocate,
|
|
21
|
+
check_reference,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
DEBUG_CHECK = False
|
|
26
|
+
__all__ = ["SD", "area_weighted_regrid", "fft_smooth_three_harmonics"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def fft_smooth_three_harmonics(daily_series, n_days, n_harmonics=3):
|
|
30
|
+
if len(daily_series) != n_days:
|
|
31
|
+
raise ValueError("Series length differs from n_days")
|
|
32
|
+
return smooth(daily_series, n_harmonics)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _smooth_block(block, n_harmonics=3):
|
|
36
|
+
return smooth(block, n_harmonics)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def area_weighted_regrid(source_lat, source_lon, source_data, target_lat, target_lon):
|
|
40
|
+
with GridWeights(source_lat, source_lon, target_lat, target_lon) as plan:
|
|
41
|
+
return plan.apply(source_data).astype("float32")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def SD(
|
|
45
|
+
obs_path,
|
|
46
|
+
bc_result,
|
|
47
|
+
output_dir,
|
|
48
|
+
ref_start_year,
|
|
49
|
+
ref_end_year,
|
|
50
|
+
*,
|
|
51
|
+
strict=False,
|
|
52
|
+
time_chunk_size=32,
|
|
53
|
+
spatial_chunk_size=16,
|
|
54
|
+
):
|
|
55
|
+
try:
|
|
56
|
+
if any(
|
|
57
|
+
isinstance(n, bool) or not isinstance(n, int) or n < 1
|
|
58
|
+
for n in (time_chunk_size, spatial_chunk_size)
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("Chunk sizes must be positive integers")
|
|
61
|
+
with ExitStack() as stack:
|
|
62
|
+
obs = stack.enter_context(open_precip_dataset(obs_path))
|
|
63
|
+
check_reference(obs.time, ref_start_year, ref_end_year)
|
|
64
|
+
history, future = [
|
|
65
|
+
stack.enter_context(open_precip_dataset(bc_result[key]))
|
|
66
|
+
for key in ("corrected_hist_pr", "corrected_fut_pr")
|
|
67
|
+
]
|
|
68
|
+
for d in ("lat", "lon"):
|
|
69
|
+
if not np.array_equal(history.coords[d], future.coords[d]):
|
|
70
|
+
raise ValueError("Historical/future model grids differ")
|
|
71
|
+
cal, ndays = calendar_info(history.time)
|
|
72
|
+
if calendar_info(future.time)[0] != cal:
|
|
73
|
+
raise ValueError("Historical/future calendars differ")
|
|
74
|
+
model_name = str(bc_result.get("model_name", "unknown"))
|
|
75
|
+
outdir = Path(output_dir) / model_name
|
|
76
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
work = stack.enter_context(
|
|
78
|
+
tempfile.TemporaryDirectory(prefix=".climate-", dir=outdir)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def mapped(shape, dtype, prefix):
|
|
82
|
+
a = allocate(shape, dtype, work, prefix)
|
|
83
|
+
stack.callback(a._mmap.close)
|
|
84
|
+
return a
|
|
85
|
+
|
|
86
|
+
shape = (ndays, *obs.shape[1:])
|
|
87
|
+
sums, counts, climate = (
|
|
88
|
+
mapped(shape, np.float64, "sum"),
|
|
89
|
+
mapped(shape, np.int32, "count"),
|
|
90
|
+
mapped(shape, np.float64, "climatology"),
|
|
91
|
+
)
|
|
92
|
+
sums[:] = 0.0
|
|
93
|
+
counts[:] = 0
|
|
94
|
+
years = obs.time.labels()[0]
|
|
95
|
+
days = mapped_days(obs.time, cal)
|
|
96
|
+
chunk = min(
|
|
97
|
+
time_chunk_size,
|
|
98
|
+
max(1, 64 * 1024**2 // (obs.shape[1] * obs.shape[2] * 8)),
|
|
99
|
+
)
|
|
100
|
+
for start in range(0, obs.shape[0], chunk):
|
|
101
|
+
stop = min(start + chunk, obs.shape[0])
|
|
102
|
+
labels = days[start:stop].copy() - 1
|
|
103
|
+
labels[
|
|
104
|
+
(years[start:stop] < ref_start_year)
|
|
105
|
+
| (years[start:stop] > ref_end_year)
|
|
106
|
+
] = -1
|
|
107
|
+
_accelerate.climate_accumulate(
|
|
108
|
+
obs.read(time=slice(start, stop)), labels, sums, counts
|
|
109
|
+
)
|
|
110
|
+
for row in range(0, obs.shape[1], spatial_chunk_size):
|
|
111
|
+
sl = (slice(None), slice(row, row + spatial_chunk_size), slice(None))
|
|
112
|
+
# Group means are part of the Rust climate pipeline, not a NumPy
|
|
113
|
+
# fallback. Use the native normalizer before harmonic smoothing.
|
|
114
|
+
climate[sl] = smooth(_accelerate.climate_mean(sums[sl], counts[sl]))
|
|
115
|
+
coarse_weights = stack.enter_context(
|
|
116
|
+
GridWeights(
|
|
117
|
+
obs.coords["lat"],
|
|
118
|
+
obs.coords["lon"],
|
|
119
|
+
history.coords["lat"],
|
|
120
|
+
history.coords["lon"],
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
coarse = np.empty((ndays, *history.shape[1:]))
|
|
124
|
+
for start in range(0, ndays, chunk):
|
|
125
|
+
coarse[start : start + chunk] = coarse_weights.apply(
|
|
126
|
+
climate[start : start + chunk]
|
|
127
|
+
)
|
|
128
|
+
downscaler = stack.enter_context(
|
|
129
|
+
_accelerate.Downscaler(
|
|
130
|
+
history.coords["lat"],
|
|
131
|
+
history.coords["lon"],
|
|
132
|
+
obs.coords["lat"],
|
|
133
|
+
obs.coords["lon"],
|
|
134
|
+
coarse,
|
|
135
|
+
climate,
|
|
136
|
+
minimum=MIN_PRECIP,
|
|
137
|
+
low=MIN_RATIO,
|
|
138
|
+
high=MAX_RATIO,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
paths, failures = [], []
|
|
142
|
+
for label, data in [("historical", history), ("future", future)]:
|
|
143
|
+
labels = mapped_days(data.time, cal) - 1
|
|
144
|
+
if np.any(labels < 0):
|
|
145
|
+
raise ValueError("Unmappable calendar days in model output")
|
|
146
|
+
path = outdir / f"{model_name}_{label}_BCSD.nc"
|
|
147
|
+
attrs = {
|
|
148
|
+
"units": "mm/day",
|
|
149
|
+
"method": "Rust FITPACK BC + low-harmonic SD",
|
|
150
|
+
"calendar_type": cal,
|
|
151
|
+
"reference_period": f"{ref_start_year}-{ref_end_year}",
|
|
152
|
+
"model_name": model_name,
|
|
153
|
+
"failed_time_steps": 0,
|
|
154
|
+
}
|
|
155
|
+
with NetCDFWriter(
|
|
156
|
+
path, data.time, obs.coords["lat"], obs.coords["lon"], attrs
|
|
157
|
+
) as writer:
|
|
158
|
+
for start in range(0, len(labels), chunk):
|
|
159
|
+
stop = min(start + chunk, len(labels))
|
|
160
|
+
try:
|
|
161
|
+
scaled = downscaler.apply(
|
|
162
|
+
data.read(time=slice(start, stop)), labels[start:stop]
|
|
163
|
+
)
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
if strict:
|
|
166
|
+
raise
|
|
167
|
+
failures.append(
|
|
168
|
+
{
|
|
169
|
+
"period": label,
|
|
170
|
+
"start": start,
|
|
171
|
+
"stop": stop,
|
|
172
|
+
"error": str(exc),
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
scaled = np.full(
|
|
176
|
+
(stop - start, *obs.shape[1:]), np.nan, dtype="float32"
|
|
177
|
+
)
|
|
178
|
+
writer.write(start, scaled)
|
|
179
|
+
attrs["failed_time_steps"] = sum(
|
|
180
|
+
f["stop"] - f["start"] for f in failures if f["period"] == label
|
|
181
|
+
)
|
|
182
|
+
paths.append(str(path))
|
|
183
|
+
return {
|
|
184
|
+
"hist_bcsd_path": paths[0],
|
|
185
|
+
"fut_bcsd_path": paths[1],
|
|
186
|
+
"time_coordinate": "time",
|
|
187
|
+
"diagnostics": {
|
|
188
|
+
"failed_chunks": failures,
|
|
189
|
+
"climatology_days": ndays,
|
|
190
|
+
"time_chunk_size": chunk,
|
|
191
|
+
"spatial_chunk_size": spatial_chunk_size,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
except Exception:
|
|
195
|
+
if strict:
|
|
196
|
+
raise
|
|
197
|
+
logger.exception("Spatial downscaling failed")
|
|
198
|
+
return None
|
klimapy/BCSD/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Bias correction and spatial downscaling stages and combined workflow."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from . import BiasCorrection, SpatialDownscaling, BiasCorrectionSpatialDownscaling
|
|
6
|
+
from .BiasCorrection import BC
|
|
7
|
+
from .SpatialDownscaling import SD
|
|
8
|
+
|
|
9
|
+
# Preserve the existing combined-workflow module import.
|
|
10
|
+
BCSD = BiasCorrectionSpatialDownscaling
|
|
11
|
+
sys.modules[__name__ + ".BCSD"] = BCSD
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BiasCorrection",
|
|
15
|
+
"SpatialDownscaling",
|
|
16
|
+
"BiasCorrectionSpatialDownscaling",
|
|
17
|
+
"BC",
|
|
18
|
+
"SD",
|
|
19
|
+
"BCSD",
|
|
20
|
+
]
|