klimapy 1.0.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.
- klimapy/BCSD/BCSD.py +1259 -0
- klimapy/BCSD/__init__.py +5 -0
- klimapy/ETCCDI/ETCCDI.py +2251 -0
- klimapy/ETCCDI/__init__.py +5 -0
- klimapy/PrecioitationVariablity/__init__.py +4 -0
- klimapy/PrecioitationVariablity/precipvariability.py +225 -0
- klimapy/Supporting_Tools/CSM.py +222 -0
- klimapy/Supporting_Tools/NCtoTIFF.py +150 -0
- klimapy/Supporting_Tools/__init__.py +6 -0
- klimapy/__init__.py +10 -0
- klimapy-1.0.0.dist-info/METADATA +362 -0
- klimapy-1.0.0.dist-info/RECORD +15 -0
- klimapy-1.0.0.dist-info/WHEEL +5 -0
- klimapy-1.0.0.dist-info/licenses/LICENSE +21 -0
- klimapy-1.0.0.dist-info/top_level.txt +1 -0
klimapy/BCSD/BCSD.py
ADDED
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
# -*-coding:utf-8 -*-
|
|
2
|
+
'''
|
|
3
|
+
@Author: Haoran Yu
|
|
4
|
+
@Contact: yuhaoran251@mails.ucas.ac.cn
|
|
5
|
+
@Date: 2026/1/19 16:51
|
|
6
|
+
@Version: 3.5 - BCSD批量处理
|
|
7
|
+
@Copyright: Copyright (c) 2026 Haoran Yu
|
|
8
|
+
@Desc: BCSD批量处理:指定模式处理
|
|
9
|
+
@Order: 批量完整流程
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
import xarray as xr
|
|
13
|
+
import numpy as np
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
import re
|
|
18
|
+
import glob
|
|
19
|
+
import cftime
|
|
20
|
+
from typing import cast
|
|
21
|
+
from scipy.interpolate import UnivariateSpline, RegularGridInterpolator
|
|
22
|
+
from scipy.fftpack import fft, ifft
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
warnings.filterwarnings('ignore')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ==================== 处理参数设置 ====================
|
|
31
|
+
# TARGET_LON_RES 和 TARGET_LAT_RES 以观测数据分辨率为准,在SD函数内部自动获取
|
|
32
|
+
WINDOW_SIZE = 15
|
|
33
|
+
N_QUANTILES = 50
|
|
34
|
+
MIN_PRECIP = np.float32(1e-4)
|
|
35
|
+
MAX_RATIO = np.float32(10.0)
|
|
36
|
+
MIN_RATIO = np.float32(0.1)
|
|
37
|
+
|
|
38
|
+
# 诊断开关:True 时输出每一步统计信息,便于定位全0问题
|
|
39
|
+
DEBUG_CHECK = True
|
|
40
|
+
DEBUG_PRINT_SAMPLES = 3
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ==================== 辅助函数 ====================
|
|
44
|
+
def calculate_bounds(coord_values):
|
|
45
|
+
"""计算坐标边界"""
|
|
46
|
+
bounds = np.zeros((len(coord_values), 2))
|
|
47
|
+
for i in range(len(coord_values)):
|
|
48
|
+
if i == 0:
|
|
49
|
+
spacing = coord_values[1] - coord_values[0]
|
|
50
|
+
bounds[i] = [coord_values[0] - spacing / 2, coord_values[0] + spacing / 2]
|
|
51
|
+
elif i == len(coord_values) - 1:
|
|
52
|
+
spacing = coord_values[-1] - coord_values[-2]
|
|
53
|
+
bounds[i] = [coord_values[-1] - spacing / 2, coord_values[-1] + spacing / 2]
|
|
54
|
+
else:
|
|
55
|
+
bounds[i] = [(coord_values[i - 1] + coord_values[i]) / 2,
|
|
56
|
+
(coord_values[i] + coord_values[i + 1]) / 2]
|
|
57
|
+
if bounds[i, 0] > bounds[i, 1]:
|
|
58
|
+
bounds[i] = [bounds[i, 1], bounds[i, 0]]
|
|
59
|
+
return bounds
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_precip_var_name(ds, candidates=('pr', 'pre', 'precip', 'precipitation')):
|
|
63
|
+
"""
|
|
64
|
+
获取降水变量名。
|
|
65
|
+
|
|
66
|
+
参数:
|
|
67
|
+
ds (xr.Dataset): 输入数据集。
|
|
68
|
+
candidates (tuple[str, ...]): 优先匹配的变量名候选。
|
|
69
|
+
|
|
70
|
+
返回:
|
|
71
|
+
str: 识别到的降水变量名。
|
|
72
|
+
"""
|
|
73
|
+
for name in candidates:
|
|
74
|
+
if name in ds.data_vars:
|
|
75
|
+
return name
|
|
76
|
+
for name in ds.data_vars:
|
|
77
|
+
lower = name.lower()
|
|
78
|
+
if 'pr' in lower or 'precip' in lower:
|
|
79
|
+
return name
|
|
80
|
+
raise KeyError(f"未找到降水变量,现有变量: {list(ds.data_vars)}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_precip_to_mm_day(da, var_name='precip'):
|
|
84
|
+
"""
|
|
85
|
+
将降水数据统一到 mm/day。
|
|
86
|
+
|
|
87
|
+
参数:
|
|
88
|
+
da (xr.DataArray): 输入降水数据。
|
|
89
|
+
var_name (str): 变量名,仅用于日志输出。
|
|
90
|
+
|
|
91
|
+
返回:
|
|
92
|
+
xr.DataArray: 单位统一后的降水数据。
|
|
93
|
+
"""
|
|
94
|
+
units = str(da.attrs.get('units', '')).strip()
|
|
95
|
+
units_norm = units.lower().replace(' ', '')
|
|
96
|
+
if units_norm in ['kgm-2s-1', 'kg/m2/s', 'kgm**-2s**-1', 'kgm-2sec-1']:
|
|
97
|
+
da = da * np.float32(86400.0)
|
|
98
|
+
da.attrs['units'] = 'mm/day'
|
|
99
|
+
print(f" 单位转换: {var_name} {units} -> mm/day")
|
|
100
|
+
return da
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cftime_to_datetime(cftime_obj):
|
|
104
|
+
"""将cftime对象转换为datetime字符串"""
|
|
105
|
+
return pd.Timestamp(year=cftime_obj.year, month=cftime_obj.month, day=cftime_obj.day)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_cftime_date(cftime_obj):
|
|
109
|
+
"""解析cftime对象的年月日"""
|
|
110
|
+
if hasattr(cftime_obj, 'year'):
|
|
111
|
+
return cftime_obj.year, cftime_obj.month, cftime_obj.day
|
|
112
|
+
return None, None, None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detect_calendar_type(time_values):
|
|
116
|
+
"""检测日历类型"""
|
|
117
|
+
try:
|
|
118
|
+
# 检查第一个时间值是否是cftime对象
|
|
119
|
+
first_time = time_values[0]
|
|
120
|
+
|
|
121
|
+
# 如果是cftime对象,根据类型判断
|
|
122
|
+
if hasattr(first_time, 'calendar'):
|
|
123
|
+
calendar = first_time.calendar.lower()
|
|
124
|
+
if '360' in calendar:
|
|
125
|
+
return "360_day", 360
|
|
126
|
+
elif 'noleap' in calendar or '365' in calendar:
|
|
127
|
+
return "noleap", 365
|
|
128
|
+
else:
|
|
129
|
+
return "gregorian", 366
|
|
130
|
+
|
|
131
|
+
# 否则尝试统计1961年的天数
|
|
132
|
+
year_1961_mask = []
|
|
133
|
+
for t in time_values[:1000]:
|
|
134
|
+
try:
|
|
135
|
+
if hasattr(t, 'year'):
|
|
136
|
+
year_1961_mask.append(t.year == 1961)
|
|
137
|
+
else:
|
|
138
|
+
dt = pd.to_datetime(str(t))
|
|
139
|
+
year_1961_mask.append(dt.year == 1961)
|
|
140
|
+
except:
|
|
141
|
+
year_1961_mask.append(False)
|
|
142
|
+
|
|
143
|
+
year_1961_count = sum(year_1961_mask)
|
|
144
|
+
|
|
145
|
+
if year_1961_count == 365:
|
|
146
|
+
return "noleap", 365
|
|
147
|
+
elif year_1961_count == 360:
|
|
148
|
+
return "360_day", 360
|
|
149
|
+
else:
|
|
150
|
+
return "gregorian", 366
|
|
151
|
+
except:
|
|
152
|
+
return "gregorian", 366
|
|
153
|
+
|
|
154
|
+
print(f"\n[BCSD主函数]\n 历史期: {gcm_hist_path}\n 观测: {obs_path}\n 未来期: {gcm_fut_path}\n 输出目录: {output_dir}\n 模式: {model_name}\n 参考期: {ref_start_year}-{ref_end_year}")
|
|
155
|
+
def calculate_doy(dates, calendar_type):
|
|
156
|
+
"""计算日序数"""
|
|
157
|
+
doy_list = []
|
|
158
|
+
for date in dates:
|
|
159
|
+
if calendar_type == "360_day":
|
|
160
|
+
if hasattr(date, 'day'):
|
|
161
|
+
doy = (date.month - 1) * 30 + date.day
|
|
162
|
+
else:
|
|
163
|
+
# 尝试解析为datetime
|
|
164
|
+
try:
|
|
165
|
+
if hasattr(date, 'year'):
|
|
166
|
+
doy = (date.month - 1) * 30 + date.day
|
|
167
|
+
else:
|
|
168
|
+
dt = pd.to_datetime(str(date))
|
|
169
|
+
doy = (dt.month - 1) * 30 + dt.day
|
|
170
|
+
except:
|
|
171
|
+
doy = date.timetuple().tm_yday
|
|
172
|
+
elif calendar_type == "noleap":
|
|
173
|
+
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
174
|
+
if hasattr(date, 'day'):
|
|
175
|
+
doy = sum(month_days[:date.month - 1]) + date.day
|
|
176
|
+
else:
|
|
177
|
+
try:
|
|
178
|
+
dt = pd.to_datetime(str(date))
|
|
179
|
+
doy = sum(month_days[:dt.month - 1]) + dt.day
|
|
180
|
+
except:
|
|
181
|
+
doy = date.timetuple().tm_yday
|
|
182
|
+
else:
|
|
183
|
+
if hasattr(date, 'dayofyear'):
|
|
184
|
+
doy = date.dayofyear
|
|
185
|
+
else:
|
|
186
|
+
try:
|
|
187
|
+
dt = pd.to_datetime(str(date))
|
|
188
|
+
doy = dt.dayofyear
|
|
189
|
+
except:
|
|
190
|
+
doy = date.timetuple().tm_yday
|
|
191
|
+
doy_list.append(doy)
|
|
192
|
+
return np.array(doy_list, dtype=np.int32)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def create_cdf_spline(gcm_data, obs_data, n_quantiles=50):
|
|
196
|
+
"""创建CDF分位数匹配的三次光滑样条传递函数"""
|
|
197
|
+
gcm_clean = gcm_data[~np.isnan(gcm_data)]
|
|
198
|
+
obs_clean = obs_data[~np.isnan(obs_data)]
|
|
199
|
+
|
|
200
|
+
gcm_clean = np.maximum(gcm_clean, 0.0)
|
|
201
|
+
obs_clean = np.maximum(obs_clean, 0.0)
|
|
202
|
+
|
|
203
|
+
if len(gcm_clean) < 20 or len(obs_clean) < 20:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
n_min = min(len(gcm_clean), len(obs_clean))
|
|
207
|
+
n_q = min(n_quantiles, max(20, int(n_min / 25)))
|
|
208
|
+
|
|
209
|
+
quantiles = np.linspace(0.02, 0.98, n_q)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
gcm_q = np.percentile(gcm_clean, quantiles * 100)
|
|
213
|
+
obs_q = np.percentile(obs_clean, quantiles * 100)
|
|
214
|
+
|
|
215
|
+
for i in range(1, len(gcm_q)):
|
|
216
|
+
if gcm_q[i] <= gcm_q[i - 1]:
|
|
217
|
+
gcm_q[i] = gcm_q[i - 1] + 1e-10
|
|
218
|
+
|
|
219
|
+
spline = UnivariateSpline(gcm_q, obs_q, s=0.8 * len(gcm_q), k=3)
|
|
220
|
+
|
|
221
|
+
gcm_min, gcm_max = gcm_q[0], gcm_q[-1]
|
|
222
|
+
obs_min, obs_max = obs_q[0], obs_q[-1]
|
|
223
|
+
|
|
224
|
+
def transfer_func(x):
|
|
225
|
+
is_scalar = np.isscalar(x)
|
|
226
|
+
if is_scalar:
|
|
227
|
+
x = np.array([x])
|
|
228
|
+
|
|
229
|
+
x_array = np.asarray(x, dtype=np.float64)
|
|
230
|
+
result = np.zeros_like(x_array)
|
|
231
|
+
|
|
232
|
+
in_range_mask = (x_array >= gcm_min) & (x_array <= gcm_max)
|
|
233
|
+
if np.any(in_range_mask):
|
|
234
|
+
result[in_range_mask] = spline(x_array[in_range_mask])
|
|
235
|
+
|
|
236
|
+
below_mask = x_array < gcm_min
|
|
237
|
+
above_mask = x_array > gcm_max
|
|
238
|
+
|
|
239
|
+
if np.any(below_mask):
|
|
240
|
+
result[below_mask] = obs_min
|
|
241
|
+
if np.any(above_mask):
|
|
242
|
+
result[above_mask] = obs_max
|
|
243
|
+
|
|
244
|
+
result = np.maximum(result, 0.0)
|
|
245
|
+
result = np.where(result < 1e-10, 0.0, result)
|
|
246
|
+
|
|
247
|
+
if is_scalar:
|
|
248
|
+
return float(result[0])
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
return transfer_func
|
|
252
|
+
except Exception as e:
|
|
253
|
+
print(f"创建传递函数失败: {e}")
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def fft_smooth_three_harmonics(daily_series, n_days, n_harmonics=3):
|
|
258
|
+
"""FFT平滑气候态(保留前三个谐波)"""
|
|
259
|
+
if len(daily_series) != n_days:
|
|
260
|
+
raise ValueError(f"序列长度应为{n_days}")
|
|
261
|
+
|
|
262
|
+
series = daily_series.copy().astype(np.float32)
|
|
263
|
+
nan_mask = np.isnan(series)
|
|
264
|
+
|
|
265
|
+
if np.all(nan_mask):
|
|
266
|
+
return series
|
|
267
|
+
|
|
268
|
+
if np.any(nan_mask):
|
|
269
|
+
indices = np.arange(len(series), dtype=np.float32)
|
|
270
|
+
valid = ~nan_mask
|
|
271
|
+
if np.sum(valid) > 1:
|
|
272
|
+
series = np.interp(indices, indices[valid], series[valid]).astype(np.float32)
|
|
273
|
+
else:
|
|
274
|
+
series[nan_mask] = np.nanmean(series).astype(np.float32)
|
|
275
|
+
|
|
276
|
+
fft_result = fft(series)
|
|
277
|
+
n = len(fft_result)
|
|
278
|
+
|
|
279
|
+
keep_indices = [0]
|
|
280
|
+
for i in range(1, n_harmonics + 1):
|
|
281
|
+
keep_indices.append(i)
|
|
282
|
+
keep_indices.append(n - i)
|
|
283
|
+
|
|
284
|
+
mask = np.zeros(n, dtype=bool)
|
|
285
|
+
mask[keep_indices] = True
|
|
286
|
+
|
|
287
|
+
filtered_fft = fft_result.copy()
|
|
288
|
+
filtered_fft[~mask] = 0
|
|
289
|
+
|
|
290
|
+
smoothed = np.real(ifft(filtered_fft)).astype(np.float32)
|
|
291
|
+
smoothed = smoothed[:len(daily_series)]
|
|
292
|
+
|
|
293
|
+
if np.any(nan_mask):
|
|
294
|
+
smoothed[nan_mask] = daily_series[nan_mask]
|
|
295
|
+
|
|
296
|
+
smoothed = np.where(smoothed < 0, 0, smoothed)
|
|
297
|
+
return smoothed
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _safe_min(arr):
|
|
301
|
+
"""计算最小值(自动忽略非有限值)。"""
|
|
302
|
+
finite = arr[np.isfinite(arr)]
|
|
303
|
+
return float(np.min(finite)) if finite.size > 0 else np.nan
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _safe_max(arr):
|
|
307
|
+
"""计算最大值(自动忽略非有限值)。"""
|
|
308
|
+
finite = arr[np.isfinite(arr)]
|
|
309
|
+
return float(np.max(finite)) if finite.size > 0 else np.nan
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _safe_mean(arr):
|
|
313
|
+
"""计算均值(自动忽略非有限值)。"""
|
|
314
|
+
finite = arr[np.isfinite(arr)]
|
|
315
|
+
return float(np.mean(finite)) if finite.size > 0 else np.nan
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def print_array_diagnostics(name, data):
|
|
319
|
+
"""
|
|
320
|
+
打印数组诊断信息。
|
|
321
|
+
|
|
322
|
+
参数:
|
|
323
|
+
name (str): 当前检查步骤名。
|
|
324
|
+
data (np.ndarray | xr.DataArray): 待检查数据。
|
|
325
|
+
"""
|
|
326
|
+
arr = np.asarray(data)
|
|
327
|
+
total = arr.size
|
|
328
|
+
finite_mask = np.isfinite(arr)
|
|
329
|
+
finite_count = int(np.sum(finite_mask))
|
|
330
|
+
nan_count = int(np.sum(np.isnan(arr)))
|
|
331
|
+
inf_count = int(total - finite_count - nan_count)
|
|
332
|
+
|
|
333
|
+
if finite_count > 0:
|
|
334
|
+
finite = arr[finite_mask]
|
|
335
|
+
zero_count = int(np.sum(np.isclose(finite, 0.0, atol=1e-12)))
|
|
336
|
+
non_zero_count = finite_count - zero_count
|
|
337
|
+
min_v = _safe_min(finite)
|
|
338
|
+
max_v = _safe_max(finite)
|
|
339
|
+
mean_v = _safe_mean(finite)
|
|
340
|
+
p95_v = float(np.percentile(finite, 95))
|
|
341
|
+
else:
|
|
342
|
+
zero_count = 0
|
|
343
|
+
non_zero_count = 0
|
|
344
|
+
min_v = np.nan
|
|
345
|
+
max_v = np.nan
|
|
346
|
+
mean_v = np.nan
|
|
347
|
+
p95_v = np.nan
|
|
348
|
+
|
|
349
|
+
finite_ratio = (finite_count / total * 100.0) if total > 0 else np.nan
|
|
350
|
+
zero_ratio = (zero_count / finite_count * 100.0) if finite_count > 0 else np.nan
|
|
351
|
+
non_zero_ratio = (non_zero_count / finite_count * 100.0) if finite_count > 0 else np.nan
|
|
352
|
+
|
|
353
|
+
print(f" [检查] {name}")
|
|
354
|
+
print(f" shape={arr.shape}, dtype={arr.dtype}, 总数={total}")
|
|
355
|
+
print(f" 有效值={finite_count} ({finite_ratio:.2f}%), NaN={nan_count}, Inf={inf_count}")
|
|
356
|
+
print(f" 非零值={non_zero_count} ({non_zero_ratio:.2f}%), 零值={zero_count} ({zero_ratio:.2f}%)")
|
|
357
|
+
print(f" min={min_v:.6g}, max={max_v:.6g}, mean={mean_v:.6g}, p95={p95_v:.6g}")
|
|
358
|
+
|
|
359
|
+
if finite_count > 0 and zero_count == finite_count:
|
|
360
|
+
print(f" [警告] {name} 的有效值全部为0,请重点检查上一步。")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ==================== BC函数:偏差校正 ====================
|
|
364
|
+
def BC(obs_path, gcm_hist_path, gcm_fut_path, model_name, ref_start_year, ref_end_year):
|
|
365
|
+
"""
|
|
366
|
+
偏差校正函数(内部使用,不保存中间结果)
|
|
367
|
+
|
|
368
|
+
参数:
|
|
369
|
+
obs_path (str): 观测数据NetCDF文件路径。
|
|
370
|
+
gcm_hist_path (str): GCM历史期文件路径。
|
|
371
|
+
gcm_fut_path (str): GCM未来期文件路径。
|
|
372
|
+
model_name (str): 模式名称(用于日志输出)。
|
|
373
|
+
|
|
374
|
+
返回:
|
|
375
|
+
dict | None: 成功时返回校正结果字典,失败返回None。
|
|
376
|
+
"""
|
|
377
|
+
print(f"\n 偏差校正 (BC): {model_name}")
|
|
378
|
+
|
|
379
|
+
start_time = time.time()
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
# 1. 读取数据
|
|
383
|
+
print(" 1. 读取数据...")
|
|
384
|
+
|
|
385
|
+
# 读取观测数据
|
|
386
|
+
obs_ds = xr.open_dataset(obs_path)
|
|
387
|
+
obs_time_coord = 'Time' if 'Time' in obs_ds.coords else 'time'
|
|
388
|
+
obs_ref = obs_ds.sel({obs_time_coord: slice(f'{ref_start_year}', f'{ref_end_year}')})
|
|
389
|
+
obs_var_name = get_precip_var_name(obs_ref)
|
|
390
|
+
obs_pr = convert_precip_to_mm_day(obs_ref[obs_var_name], var_name=f'obs_{obs_var_name}')
|
|
391
|
+
print(f" 观测数据: {obs_pr.shape}")
|
|
392
|
+
if DEBUG_CHECK:
|
|
393
|
+
print_array_diagnostics("BC-观测原始", obs_pr.values)
|
|
394
|
+
|
|
395
|
+
# 读取GCM数据,不自动解码时间
|
|
396
|
+
gcm_hist_ds = xr.open_dataset(gcm_hist_path, decode_times=False)
|
|
397
|
+
gcm_hist_pr = convert_precip_to_mm_day(gcm_hist_ds['pr'], var_name='gcm_hist_pr')
|
|
398
|
+
print(f" GCM历史数据: {gcm_hist_pr.shape}")
|
|
399
|
+
if DEBUG_CHECK:
|
|
400
|
+
print_array_diagnostics("BC-GCM历史原始", gcm_hist_pr.values)
|
|
401
|
+
|
|
402
|
+
gcm_fut_ds = xr.open_dataset(gcm_fut_path, decode_times=False)
|
|
403
|
+
gcm_fut_pr = convert_precip_to_mm_day(gcm_fut_ds['pr'], var_name='gcm_fut_pr')
|
|
404
|
+
print(f" GCM未来数据: {gcm_fut_pr.shape}")
|
|
405
|
+
if DEBUG_CHECK:
|
|
406
|
+
print_array_diagnostics("BC-GCM未来原始", gcm_fut_pr.values)
|
|
407
|
+
|
|
408
|
+
# 2. 观测数据升尺度
|
|
409
|
+
print(" 2. 观测数据升尺度...")
|
|
410
|
+
|
|
411
|
+
# 获取坐标
|
|
412
|
+
obs_lat = obs_pr.Lat.values if 'Lat' in obs_pr.coords else obs_pr.lat.values
|
|
413
|
+
obs_lon = obs_pr.Lon.values if 'Lon' in obs_pr.coords else obs_pr.lon.values
|
|
414
|
+
gcm_lat = gcm_hist_pr.Lat.values if 'Lat' in gcm_hist_pr.coords else gcm_hist_pr.lat.values
|
|
415
|
+
gcm_lon = gcm_hist_pr.Lon.values if 'Lon' in gcm_hist_pr.coords else gcm_hist_pr.lon.values
|
|
416
|
+
|
|
417
|
+
# 计算边界
|
|
418
|
+
obs_lat_bounds = calculate_bounds(obs_lat)
|
|
419
|
+
obs_lon_bounds = calculate_bounds(obs_lon)
|
|
420
|
+
gcm_lat_bounds = calculate_bounds(gcm_lat)
|
|
421
|
+
gcm_lon_bounds = calculate_bounds(gcm_lon)
|
|
422
|
+
|
|
423
|
+
# 初始化升尺度数组
|
|
424
|
+
n_time_obs = len(obs_pr.Time) if 'Time' in obs_pr.coords else len(obs_pr.time)
|
|
425
|
+
n_lat_gcm = len(gcm_lat)
|
|
426
|
+
n_lon_gcm = len(gcm_lon)
|
|
427
|
+
obs_coarse_values = np.full((n_time_obs, n_lat_gcm, n_lon_gcm), np.nan)
|
|
428
|
+
|
|
429
|
+
# 面积加权平均
|
|
430
|
+
obs_area_weights = np.cos(np.radians(obs_lat))[:, np.newaxis]
|
|
431
|
+
|
|
432
|
+
for i in range(n_lat_gcm):
|
|
433
|
+
if i % 5 == 0 or i == n_lat_gcm - 1:
|
|
434
|
+
print(f" 纬度进度: {i + 1}/{n_lat_gcm}")
|
|
435
|
+
|
|
436
|
+
for j in range(n_lon_gcm):
|
|
437
|
+
lat_mask = (obs_lat >= gcm_lat_bounds[i, 0]) & (obs_lat <= gcm_lat_bounds[i, 1])
|
|
438
|
+
lon_mask = (obs_lon >= gcm_lon_bounds[j, 0]) & (obs_lon <= gcm_lon_bounds[j, 1])
|
|
439
|
+
|
|
440
|
+
lat_indices = np.where(lat_mask)[0]
|
|
441
|
+
lon_indices = np.where(lon_mask)[0]
|
|
442
|
+
|
|
443
|
+
if len(lat_indices) > 0 and len(lon_indices) > 0:
|
|
444
|
+
for t in range(n_time_obs):
|
|
445
|
+
obs_subset = obs_pr.values[
|
|
446
|
+
t, lat_indices[0]:lat_indices[-1] + 1, lon_indices[0]:lon_indices[-1] + 1]
|
|
447
|
+
weights_subset = obs_area_weights[lat_indices[0]:lat_indices[-1] + 1, :]
|
|
448
|
+
weights_subset = np.repeat(weights_subset, len(lon_indices), axis=1)
|
|
449
|
+
|
|
450
|
+
valid_mask = ~np.isnan(obs_subset)
|
|
451
|
+
|
|
452
|
+
if np.any(valid_mask):
|
|
453
|
+
weighted_sum = np.nansum(obs_subset * weights_subset)
|
|
454
|
+
total_weight = np.nansum(weights_subset * valid_mask)
|
|
455
|
+
|
|
456
|
+
if total_weight > 0:
|
|
457
|
+
obs_coarse_values[t, i, j] = weighted_sum / total_weight
|
|
458
|
+
|
|
459
|
+
print(" 观测数据升尺度完成!")
|
|
460
|
+
if DEBUG_CHECK:
|
|
461
|
+
print_array_diagnostics("BC-观测升尺度到GCM网格", obs_coarse_values)
|
|
462
|
+
|
|
463
|
+
# 3. 时间对齐处理
|
|
464
|
+
print(" 3. 时间对齐处理...")
|
|
465
|
+
|
|
466
|
+
gcm_time_coord = 'Time' if 'Time' in gcm_hist_ds.coords else 'time'
|
|
467
|
+
gcm_time_values = gcm_hist_ds[gcm_time_coord].values
|
|
468
|
+
|
|
469
|
+
# 获取观测日期
|
|
470
|
+
obs_dates = []
|
|
471
|
+
for t in obs_ref[obs_time_coord].values:
|
|
472
|
+
if hasattr(t, 'year'):
|
|
473
|
+
obs_dates.append(pd.Timestamp(year=t.year, month=t.month, day=t.day))
|
|
474
|
+
else:
|
|
475
|
+
t_str = str(t).split('T')[0] if 'T' in str(t) else str(t)
|
|
476
|
+
obs_dates.append(pd.Timestamp(t_str))
|
|
477
|
+
|
|
478
|
+
# 解码GCM时间
|
|
479
|
+
try:
|
|
480
|
+
# 尝试解码时间
|
|
481
|
+
gcm_hist_decoded = xr.decode_cf(gcm_hist_ds)
|
|
482
|
+
gcm_time_decoded = gcm_hist_decoded[gcm_time_coord].values
|
|
483
|
+
# 检测日历类型
|
|
484
|
+
calendar_type, max_doy = detect_calendar_type(gcm_time_decoded)
|
|
485
|
+
except:
|
|
486
|
+
# 如果解码失败,尝试手动处理
|
|
487
|
+
print(" 时间解码失败,尝试手动处理...")
|
|
488
|
+
# 检查文件信息中的日历类型
|
|
489
|
+
calendar = gcm_hist_ds[gcm_time_coord].attrs.get('calendar', '')
|
|
490
|
+
if calendar:
|
|
491
|
+
if '360' in str(calendar):
|
|
492
|
+
calendar_type = "360_day"
|
|
493
|
+
max_doy = 360
|
|
494
|
+
elif 'noleap' in str(calendar) or '365' in str(calendar):
|
|
495
|
+
calendar_type = "noleap"
|
|
496
|
+
max_doy = 365
|
|
497
|
+
else:
|
|
498
|
+
calendar_type = "gregorian"
|
|
499
|
+
max_doy = 366
|
|
500
|
+
else:
|
|
501
|
+
# 默认使用360_day,因为UKESM1-0-LL使用360天日历
|
|
502
|
+
calendar_type = "360_day"
|
|
503
|
+
max_doy = 360
|
|
504
|
+
|
|
505
|
+
print(f" 日历类型: {calendar_type}, 最大DOY: {max_doy}")
|
|
506
|
+
|
|
507
|
+
# 日历调整
|
|
508
|
+
if calendar_type == "noleap":
|
|
509
|
+
feb29_mask = np.array([(d.month == 2 and d.day == 29) for d in obs_dates])
|
|
510
|
+
if feb29_mask.sum() > 0:
|
|
511
|
+
obs_coarse_values = obs_coarse_values[~feb29_mask, :, :]
|
|
512
|
+
obs_dates = [d for i, d in enumerate(obs_dates) if not feb29_mask[i]]
|
|
513
|
+
elif calendar_type == "360_day":
|
|
514
|
+
remove_mask = np.array([d.day == 31 for d in obs_dates])
|
|
515
|
+
if remove_mask.sum() > 0:
|
|
516
|
+
obs_coarse_values = obs_coarse_values[~remove_mask, :, :]
|
|
517
|
+
obs_dates = [d for i, d in enumerate(obs_dates) if not remove_mask[i]]
|
|
518
|
+
|
|
519
|
+
# 对齐长度
|
|
520
|
+
min_length = min(len(obs_dates), len(gcm_hist_pr[gcm_time_coord]))
|
|
521
|
+
obs_coarse_values = obs_coarse_values[:min_length, :, :]
|
|
522
|
+
obs_dates = obs_dates[:min_length]
|
|
523
|
+
gcm_hist_values = gcm_hist_pr.values[:min_length, :, :]
|
|
524
|
+
gcm_fut_values = gcm_fut_pr.values
|
|
525
|
+
|
|
526
|
+
# 同时裁剪GCM DataArray以保证坐标一致
|
|
527
|
+
gcm_hist_pr_aligned = gcm_hist_pr.isel({gcm_time_coord: slice(0, min_length)})
|
|
528
|
+
|
|
529
|
+
print(f" 对齐长度: {min_length} 天")
|
|
530
|
+
if DEBUG_CHECK:
|
|
531
|
+
print_array_diagnostics("BC-时间对齐后观测升尺度", obs_coarse_values)
|
|
532
|
+
print_array_diagnostics("BC-时间对齐后GCM历史", gcm_hist_values)
|
|
533
|
+
print_array_diagnostics("BC-时间对齐后GCM未来", gcm_fut_values)
|
|
534
|
+
|
|
535
|
+
# 4. 计算DOY
|
|
536
|
+
print(" 4. 计算DOY...")
|
|
537
|
+
|
|
538
|
+
obs_doy = calculate_doy(obs_dates, calendar_type)
|
|
539
|
+
|
|
540
|
+
# 计算GCM DOY
|
|
541
|
+
gcm_hist_doy = []
|
|
542
|
+
for t_idx in range(min_length):
|
|
543
|
+
# 使用基于日历类型的DOY计算
|
|
544
|
+
doy = ((t_idx % max_doy) + 1) # 简单按顺序分配DOY
|
|
545
|
+
gcm_hist_doy.append(doy)
|
|
546
|
+
|
|
547
|
+
gcm_fut_doy = []
|
|
548
|
+
for t_idx in range(len(gcm_fut_pr[gcm_time_coord])):
|
|
549
|
+
doy = ((t_idx % max_doy) + 1)
|
|
550
|
+
gcm_fut_doy.append(doy)
|
|
551
|
+
|
|
552
|
+
gcm_hist_doy = np.array(gcm_hist_doy, dtype=np.int32)
|
|
553
|
+
gcm_fut_doy = np.array(gcm_fut_doy, dtype=np.int32)
|
|
554
|
+
|
|
555
|
+
print(f" 历史期: {len(gcm_hist_doy)}天, 未来期: {len(gcm_fut_doy)}天")
|
|
556
|
+
if DEBUG_CHECK:
|
|
557
|
+
print(f" [检查] BC-DOY覆盖: obs唯一DOY={len(np.unique(obs_doy))}, "
|
|
558
|
+
f"hist唯一DOY={len(np.unique(gcm_hist_doy))}, fut唯一DOY={len(np.unique(gcm_fut_doy))}")
|
|
559
|
+
|
|
560
|
+
# 5. CDF分位数匹配
|
|
561
|
+
print(" 5. CDF分位数匹配偏差校正...")
|
|
562
|
+
|
|
563
|
+
n_time_hist = len(gcm_hist_doy)
|
|
564
|
+
n_time_fut = len(gcm_fut_doy)
|
|
565
|
+
n_lat_gcm = gcm_hist_values.shape[1]
|
|
566
|
+
n_lon_gcm = gcm_hist_values.shape[2]
|
|
567
|
+
|
|
568
|
+
corrected_hist_values = np.zeros_like(gcm_hist_values)
|
|
569
|
+
corrected_fut_values = np.zeros_like(gcm_fut_values)
|
|
570
|
+
|
|
571
|
+
print(f" 滑动窗口: ±{WINDOW_SIZE}天, 网格点: {n_lat_gcm}×{n_lon_gcm}")
|
|
572
|
+
|
|
573
|
+
total_doy = 0
|
|
574
|
+
missing_window_doy = 0
|
|
575
|
+
built_func_doy = 0
|
|
576
|
+
|
|
577
|
+
# 逐网格点校正
|
|
578
|
+
for lat_idx in range(n_lat_gcm):
|
|
579
|
+
if lat_idx % 5 == 0 or lat_idx == n_lat_gcm - 1:
|
|
580
|
+
progress = (lat_idx + 1) / n_lat_gcm * 100
|
|
581
|
+
print(f" 纬度进度: {lat_idx + 1}/{n_lat_gcm} ({progress:.1f}%)")
|
|
582
|
+
|
|
583
|
+
for lon_idx in range(n_lon_gcm):
|
|
584
|
+
# 提取数据
|
|
585
|
+
obs_series = obs_coarse_values[:, lat_idx, lon_idx]
|
|
586
|
+
gcm_hist_series = gcm_hist_values[:, lat_idx, lon_idx]
|
|
587
|
+
gcm_fut_series = gcm_fut_values[:, lat_idx, lon_idx]
|
|
588
|
+
|
|
589
|
+
# 存储传递函数
|
|
590
|
+
doy_funcs = {}
|
|
591
|
+
|
|
592
|
+
# 为每个DOY建立传递函数
|
|
593
|
+
for doy in range(1, max_doy + 1):
|
|
594
|
+
total_doy += 1
|
|
595
|
+
# 收集观测窗口数据
|
|
596
|
+
obs_indices = np.where(obs_doy == doy)[0]
|
|
597
|
+
obs_window = []
|
|
598
|
+
for idx in obs_indices:
|
|
599
|
+
start = max(0, idx - WINDOW_SIZE)
|
|
600
|
+
end = min(len(obs_series) - 1, idx + WINDOW_SIZE)
|
|
601
|
+
obs_window.extend(obs_series[start:end + 1])
|
|
602
|
+
|
|
603
|
+
# 收集GCM历史窗口数据
|
|
604
|
+
gcm_indices = np.where(gcm_hist_doy == doy)[0]
|
|
605
|
+
gcm_window = []
|
|
606
|
+
for idx in gcm_indices:
|
|
607
|
+
start = max(0, idx - WINDOW_SIZE)
|
|
608
|
+
end = min(n_time_hist - 1, idx + WINDOW_SIZE)
|
|
609
|
+
gcm_window.extend(gcm_hist_series[start:end + 1])
|
|
610
|
+
|
|
611
|
+
if len(obs_window) == 0 or len(gcm_window) == 0:
|
|
612
|
+
missing_window_doy += 1
|
|
613
|
+
doy_funcs[doy] = None
|
|
614
|
+
continue
|
|
615
|
+
|
|
616
|
+
# 创建传递函数
|
|
617
|
+
func = create_cdf_spline(np.array(gcm_window), np.array(obs_window), N_QUANTILES)
|
|
618
|
+
doy_funcs[doy] = func
|
|
619
|
+
if func is not None:
|
|
620
|
+
built_func_doy += 1
|
|
621
|
+
|
|
622
|
+
# 校正历史期数据
|
|
623
|
+
for t in range(n_time_hist):
|
|
624
|
+
doy = int(gcm_hist_doy[t])
|
|
625
|
+
val = gcm_hist_series[t]
|
|
626
|
+
|
|
627
|
+
if not np.isnan(val) and doy in doy_funcs and doy_funcs[doy] is not None:
|
|
628
|
+
corrected_hist_values[t, lat_idx, lon_idx] = doy_funcs[doy](val)
|
|
629
|
+
else:
|
|
630
|
+
corrected_hist_values[t, lat_idx, lon_idx] = val
|
|
631
|
+
|
|
632
|
+
# 校正未来期数据
|
|
633
|
+
for t in range(n_time_fut):
|
|
634
|
+
doy = int(gcm_fut_doy[t])
|
|
635
|
+
val = gcm_fut_series[t]
|
|
636
|
+
|
|
637
|
+
if not np.isnan(val) and doy in doy_funcs and doy_funcs[doy] is not None:
|
|
638
|
+
corrected_fut_values[t, lat_idx, lon_idx] = doy_funcs[doy](val)
|
|
639
|
+
else:
|
|
640
|
+
corrected_fut_values[t, lat_idx, lon_idx] = val
|
|
641
|
+
|
|
642
|
+
print(" 偏差校正完成!")
|
|
643
|
+
if DEBUG_CHECK:
|
|
644
|
+
print(f" [检查] BC-传递函数统计: 总DOY样本={total_doy}, 窗口缺失={missing_window_doy}, "
|
|
645
|
+
f"成功构建={built_func_doy}")
|
|
646
|
+
print_array_diagnostics("BC-历史校正结果", corrected_hist_values)
|
|
647
|
+
print_array_diagnostics("BC-未来校正结果", corrected_fut_values)
|
|
648
|
+
|
|
649
|
+
# 创建校正数据数组
|
|
650
|
+
corrected_hist_pr = xr.DataArray(
|
|
651
|
+
corrected_hist_values,
|
|
652
|
+
dims=gcm_hist_pr_aligned.dims,
|
|
653
|
+
coords=gcm_hist_pr_aligned.coords,
|
|
654
|
+
attrs=gcm_hist_pr_aligned.attrs.copy()
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
corrected_fut_pr = xr.DataArray(
|
|
658
|
+
corrected_fut_values,
|
|
659
|
+
dims=gcm_fut_pr.dims,
|
|
660
|
+
coords=gcm_fut_pr.coords,
|
|
661
|
+
attrs=gcm_fut_pr.attrs.copy()
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
# 关闭数据集
|
|
665
|
+
obs_ds.close()
|
|
666
|
+
gcm_hist_ds.close()
|
|
667
|
+
gcm_fut_ds.close()
|
|
668
|
+
|
|
669
|
+
elapsed_time = time.time() - start_time
|
|
670
|
+
print(f" 偏差校正耗时: {elapsed_time:.1f}秒")
|
|
671
|
+
|
|
672
|
+
# 返回BC结果,供SD使用
|
|
673
|
+
return {
|
|
674
|
+
'corrected_hist_pr': corrected_hist_pr,
|
|
675
|
+
'corrected_fut_pr': corrected_fut_pr,
|
|
676
|
+
'calendar_type': calendar_type,
|
|
677
|
+
'max_doy': max_doy,
|
|
678
|
+
'gcm_time_coord': gcm_time_coord,
|
|
679
|
+
'gcm_lat_coord': 'Lat' if 'Lat' in gcm_hist_ds.coords else 'lat',
|
|
680
|
+
'gcm_lon_coord': 'Lon' if 'Lon' in gcm_hist_ds.coords else 'lon',
|
|
681
|
+
'original_hist_path': gcm_hist_path,
|
|
682
|
+
'original_fut_path': gcm_fut_path,
|
|
683
|
+
'model_name': model_name
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
except Exception as e:
|
|
687
|
+
print(f" 偏差校正失败: {str(e)}")
|
|
688
|
+
import traceback
|
|
689
|
+
traceback.print_exc()
|
|
690
|
+
for ds_obj in ['obs_ds', 'gcm_hist_ds', 'gcm_fut_ds']:
|
|
691
|
+
if ds_obj in locals():
|
|
692
|
+
try:
|
|
693
|
+
locals()[ds_obj].close()
|
|
694
|
+
except Exception:
|
|
695
|
+
pass
|
|
696
|
+
return None
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
# ==================== SD函数:空间降尺度 ====================
|
|
700
|
+
def area_weighted_regrid(source_lat, source_lon, source_data, target_lat, target_lon):
|
|
701
|
+
"""使用面积加权平均将高分辨率数据粗化到低分辨率网格"""
|
|
702
|
+
source_lat_bounds = calculate_bounds(source_lat)
|
|
703
|
+
source_lon_bounds = calculate_bounds(source_lon)
|
|
704
|
+
target_lat_bounds = calculate_bounds(target_lat)
|
|
705
|
+
target_lon_bounds = calculate_bounds(target_lon)
|
|
706
|
+
|
|
707
|
+
n_lat_target = len(target_lat)
|
|
708
|
+
n_lon_target = len(target_lon)
|
|
709
|
+
regridded_data = np.full((n_lat_target, n_lon_target), np.nan, dtype=np.float32)
|
|
710
|
+
|
|
711
|
+
source_area_weights = np.cos(np.radians(source_lat))[:, np.newaxis]
|
|
712
|
+
|
|
713
|
+
for i in range(n_lat_target):
|
|
714
|
+
for j in range(n_lon_target):
|
|
715
|
+
# 找到与目标网格重叠的源网格
|
|
716
|
+
lat_mask = (source_lat >= target_lat_bounds[i, 0]) & (source_lat <= target_lat_bounds[i, 1])
|
|
717
|
+
lon_mask = (source_lon >= target_lon_bounds[j, 0]) & (source_lon <= target_lon_bounds[j, 1])
|
|
718
|
+
|
|
719
|
+
lat_indices = np.where(lat_mask)[0]
|
|
720
|
+
lon_indices = np.where(lon_mask)[0]
|
|
721
|
+
|
|
722
|
+
if len(lat_indices) > 0 and len(lon_indices) > 0:
|
|
723
|
+
source_subset = source_data[lat_indices[0]:lat_indices[-1] + 1, lon_indices[0]:lon_indices[-1] + 1]
|
|
724
|
+
weights_subset = source_area_weights[lat_indices[0]:lat_indices[-1] + 1, :]
|
|
725
|
+
weights_subset = np.repeat(weights_subset, len(lon_indices), axis=1)
|
|
726
|
+
|
|
727
|
+
valid_mask = ~np.isnan(source_subset)
|
|
728
|
+
if np.any(valid_mask):
|
|
729
|
+
weighted_sum = np.nansum(source_subset * weights_subset)
|
|
730
|
+
total_weight = np.nansum(weights_subset * valid_mask)
|
|
731
|
+
if total_weight > 0:
|
|
732
|
+
regridded_data[i, j] = weighted_sum / total_weight
|
|
733
|
+
return regridded_data
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def SD(obs_path, bc_result, output_dir, ref_start_year, ref_end_year):
|
|
737
|
+
"""
|
|
738
|
+
空间降尺度主函数
|
|
739
|
+
|
|
740
|
+
参数:
|
|
741
|
+
obs_path (str): 观测数据NetCDF文件路径。
|
|
742
|
+
bc_result (dict): BC函数返回的校正结果字典。
|
|
743
|
+
output_dir (str): BCSD结果输出目录。
|
|
744
|
+
|
|
745
|
+
返回:
|
|
746
|
+
dict | None: 成功时返回输出文件路径信息,失败返回None。
|
|
747
|
+
"""
|
|
748
|
+
model_name = bc_result['model_name']
|
|
749
|
+
calendar_type = bc_result['calendar_type']
|
|
750
|
+
max_doy = bc_result['max_doy']
|
|
751
|
+
gcm_time_coord = bc_result['gcm_time_coord']
|
|
752
|
+
original_hist_path = bc_result.get('original_hist_path') # 原始历史文件路径
|
|
753
|
+
original_fut_path = bc_result.get('original_fut_path') # 原始未来文件路径
|
|
754
|
+
|
|
755
|
+
print(f"\n 空间降尺度 (SD): {model_name}")
|
|
756
|
+
|
|
757
|
+
start_time = time.time()
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
print(" 1. 读取数据...")
|
|
761
|
+
|
|
762
|
+
# 读取观测数据
|
|
763
|
+
obs_ds = xr.open_dataset(obs_path)
|
|
764
|
+
obs_time_coord = 'Time' if 'Time' in obs_ds.coords else 'time'
|
|
765
|
+
obs_ref = obs_ds.sel({obs_time_coord: slice(f'{ref_start_year}', f'{ref_end_year}')})
|
|
766
|
+
obs_var_name = get_precip_var_name(obs_ref)
|
|
767
|
+
obs_pr = convert_precip_to_mm_day(obs_ref[obs_var_name], var_name=f'obs_{obs_var_name}')
|
|
768
|
+
|
|
769
|
+
# 获取BC校正后的数据
|
|
770
|
+
corrected_hist_pr = bc_result['corrected_hist_pr']
|
|
771
|
+
corrected_fut_pr = bc_result['corrected_fut_pr']
|
|
772
|
+
|
|
773
|
+
print(f" 观测数据: {obs_pr.shape}")
|
|
774
|
+
print(f" BC历史数据: {corrected_hist_pr.shape}")
|
|
775
|
+
print(f" BC未来数据: {corrected_fut_pr.shape}")
|
|
776
|
+
if DEBUG_CHECK:
|
|
777
|
+
print_array_diagnostics("SD-观测输入", obs_pr.values)
|
|
778
|
+
print_array_diagnostics("SD-BC历史输入", corrected_hist_pr.values)
|
|
779
|
+
print_array_diagnostics("SD-BC未来输入", corrected_fut_pr.values)
|
|
780
|
+
|
|
781
|
+
# 2. 读取原始GCM数据以获取时间变量(不解码时间)
|
|
782
|
+
print(" 2. 读取原始时间坐标...")
|
|
783
|
+
|
|
784
|
+
# 读取历史期原始时间坐标
|
|
785
|
+
hist_ds_original = xr.open_dataset(original_hist_path, decode_times=False)
|
|
786
|
+
hist_time_var = cast(xr.DataArray, hist_ds_original[gcm_time_coord])
|
|
787
|
+
|
|
788
|
+
# 如果BC校正裁剪了数据,需要同步裁剪时间坐标
|
|
789
|
+
n_time_hist = corrected_hist_pr.shape[0]
|
|
790
|
+
if len(hist_time_var) > n_time_hist:
|
|
791
|
+
hist_time_var = hist_time_var.isel({gcm_time_coord: slice(0, n_time_hist)})
|
|
792
|
+
|
|
793
|
+
print(f" 历史原始时间: {len(hist_time_var)}个时间点")
|
|
794
|
+
hist_time_values = np.asarray(hist_time_var.data)
|
|
795
|
+
print(f" 历史原始时间类型: {type(hist_time_values[0])}")
|
|
796
|
+
print(f" 历史原始时间属性: {dict(hist_time_var.attrs)}")
|
|
797
|
+
|
|
798
|
+
# 读取未来期原始时间坐标
|
|
799
|
+
fut_ds_original = xr.open_dataset(original_fut_path, decode_times=False)
|
|
800
|
+
fut_time_var = cast(xr.DataArray, fut_ds_original[gcm_time_coord])
|
|
801
|
+
|
|
802
|
+
# 确保未来时间坐标长度匹配
|
|
803
|
+
n_time_future = corrected_fut_pr.shape[0]
|
|
804
|
+
if len(fut_time_var) > n_time_future:
|
|
805
|
+
fut_time_var = fut_time_var.isel({gcm_time_coord: slice(0, n_time_future)})
|
|
806
|
+
elif len(fut_time_var) < n_time_future:
|
|
807
|
+
if len(fut_time_var) == 0:
|
|
808
|
+
raise ValueError("未来时间坐标为空,无法与未来数据对齐")
|
|
809
|
+
# 如果未来时间坐标长度不足,使用循环扩展
|
|
810
|
+
print(f" 警告: 未来时间坐标长度({len(fut_time_var)})小于数据长度({n_time_future})")
|
|
811
|
+
# 简单重复最后一个时间点(这只是一个临时解决方案)
|
|
812
|
+
fut_time_var = xr.concat([fut_time_var] * (n_time_future // len(fut_time_var) + 1), dim=gcm_time_coord)
|
|
813
|
+
fut_time_var = fut_time_var.isel({gcm_time_coord: slice(0, n_time_future)})
|
|
814
|
+
|
|
815
|
+
print(f" 未来原始时间: {len(fut_time_var)}个时间点")
|
|
816
|
+
fut_time_values = np.asarray(fut_time_var.data)
|
|
817
|
+
print(f" 未来原始时间类型: {type(fut_time_values[0])}")
|
|
818
|
+
|
|
819
|
+
# 3. 计算FFT平滑气候场
|
|
820
|
+
print(" 3. 计算FFT平滑气候场...")
|
|
821
|
+
|
|
822
|
+
# 获取观测坐标
|
|
823
|
+
obs_lat_coord = 'Lat' if 'Lat' in obs_ref.coords else 'lat'
|
|
824
|
+
obs_lon_coord = 'Lon' if 'Lon' in obs_ref.coords else 'lon'
|
|
825
|
+
|
|
826
|
+
obs_pr_array = obs_pr.values.astype(np.float32)
|
|
827
|
+
n_days = max_doy
|
|
828
|
+
n_lat_obs = obs_pr_array.shape[1]
|
|
829
|
+
n_lon_obs = obs_pr_array.shape[2]
|
|
830
|
+
|
|
831
|
+
# 获取观测日期
|
|
832
|
+
obs_dates = []
|
|
833
|
+
for t in obs_ref[obs_time_coord].values:
|
|
834
|
+
if hasattr(t, 'year'):
|
|
835
|
+
obs_dates.append(pd.Timestamp(year=t.year, month=t.month, day=t.day))
|
|
836
|
+
else:
|
|
837
|
+
t_str = str(t).split('T')[0] if 'T' in str(t) else str(t)
|
|
838
|
+
obs_dates.append(pd.Timestamp(t_str))
|
|
839
|
+
|
|
840
|
+
# 日历调整
|
|
841
|
+
if calendar_type == "noleap":
|
|
842
|
+
feb29_mask = np.array([(d.month == 2 and d.day == 29) for d in obs_dates])
|
|
843
|
+
if feb29_mask.sum() > 0:
|
|
844
|
+
obs_pr_array = obs_pr_array[~feb29_mask, :, :]
|
|
845
|
+
obs_dates = [d for i, d in enumerate(obs_dates) if not feb29_mask[i]]
|
|
846
|
+
elif calendar_type == "360_day":
|
|
847
|
+
remove_mask = np.array([d.day == 31 for d in obs_dates])
|
|
848
|
+
if remove_mask.sum() > 0:
|
|
849
|
+
obs_pr_array = obs_pr_array[~remove_mask, :, :]
|
|
850
|
+
obs_dates = [d for i, d in enumerate(obs_dates) if not remove_mask[i]]
|
|
851
|
+
|
|
852
|
+
# 计算DOY
|
|
853
|
+
obs_doy = calculate_doy(obs_dates, calendar_type)
|
|
854
|
+
|
|
855
|
+
# 计算每日气候态
|
|
856
|
+
obs_clim_values = np.zeros((n_days, n_lat_obs, n_lon_obs), dtype=np.float32)
|
|
857
|
+
obs_clim_counts = np.zeros((n_days, n_lat_obs, n_lon_obs), dtype=np.float32)
|
|
858
|
+
|
|
859
|
+
for t in range(len(obs_pr_array)):
|
|
860
|
+
doy_idx = int(obs_doy[t]) - 1
|
|
861
|
+
if doy_idx < n_days:
|
|
862
|
+
obs_clim_values[doy_idx] += obs_pr_array[t]
|
|
863
|
+
obs_clim_counts[doy_idx] += (~np.isnan(obs_pr_array[t])).astype(np.float32)
|
|
864
|
+
|
|
865
|
+
# 计算平均值
|
|
866
|
+
obs_clim_values = np.where(obs_clim_counts > 0, obs_clim_values / obs_clim_counts, np.nan).astype(np.float32)
|
|
867
|
+
obs_clim_values = np.where(obs_clim_values < 0, 0, obs_clim_values)
|
|
868
|
+
|
|
869
|
+
# FFT平滑气候态
|
|
870
|
+
obs_clim_smooth = np.zeros_like(obs_clim_values, dtype=np.float32)
|
|
871
|
+
|
|
872
|
+
for lat_idx in range(n_lat_obs):
|
|
873
|
+
for lon_idx in range(n_lon_obs):
|
|
874
|
+
daily_series = obs_clim_values[:, lat_idx, lon_idx]
|
|
875
|
+
|
|
876
|
+
if np.any(~np.isnan(daily_series)):
|
|
877
|
+
try:
|
|
878
|
+
smoothed = fft_smooth_three_harmonics(daily_series, n_days, n_harmonics=3)
|
|
879
|
+
obs_clim_smooth[:, lat_idx, lon_idx] = smoothed
|
|
880
|
+
except:
|
|
881
|
+
obs_clim_smooth[:, lat_idx, lon_idx] = daily_series
|
|
882
|
+
else:
|
|
883
|
+
obs_clim_smooth[:, lat_idx, lon_idx] = daily_series
|
|
884
|
+
|
|
885
|
+
# 处理NaN值
|
|
886
|
+
obs_clim_median = np.nanmedian(obs_clim_smooth).astype(np.float32)
|
|
887
|
+
obs_clim_smooth = np.where(np.isnan(obs_clim_smooth), obs_clim_median * 0.1, obs_clim_smooth).astype(np.float32)
|
|
888
|
+
obs_clim_smooth = np.where(obs_clim_smooth < 0, 0, obs_clim_smooth).astype(np.float32)
|
|
889
|
+
if DEBUG_CHECK:
|
|
890
|
+
print_array_diagnostics("SD-观测逐日气候态(平滑前)", obs_clim_values)
|
|
891
|
+
print_array_diagnostics("SD-观测逐日气候态(平滑后)", obs_clim_smooth)
|
|
892
|
+
|
|
893
|
+
# 4. 空间降尺度
|
|
894
|
+
print(" 4. 空间降尺度...")
|
|
895
|
+
|
|
896
|
+
# 获取模式数据
|
|
897
|
+
gcm_lat_coord = bc_result['gcm_lat_coord']
|
|
898
|
+
gcm_lon_coord = bc_result['gcm_lon_coord']
|
|
899
|
+
|
|
900
|
+
hist_bc_data = corrected_hist_pr.values.astype(np.float32)
|
|
901
|
+
future_bc_data = corrected_fut_pr.values.astype(np.float32)
|
|
902
|
+
|
|
903
|
+
# 获取模式网格
|
|
904
|
+
model_lon = corrected_hist_pr[gcm_lon_coord].values.astype(np.float32)
|
|
905
|
+
model_lat = corrected_hist_pr[gcm_lat_coord].values.astype(np.float32)
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
# 根据观测数据自动获取分辨率
|
|
909
|
+
obs_lon_vals = obs_ref[obs_lon_coord].values
|
|
910
|
+
obs_lat_vals = obs_ref[obs_lat_coord].values
|
|
911
|
+
if len(obs_lon_vals) > 1:
|
|
912
|
+
lon_res = float(np.round(np.abs(obs_lon_vals[1] - obs_lon_vals[0]), 6))
|
|
913
|
+
else:
|
|
914
|
+
lon_res = 0.25
|
|
915
|
+
if len(obs_lat_vals) > 1:
|
|
916
|
+
lat_res = float(np.round(np.abs(obs_lat_vals[1] - obs_lat_vals[0]), 6))
|
|
917
|
+
else:
|
|
918
|
+
lat_res = 0.25
|
|
919
|
+
target_lon = np.arange(obs_lon_vals.min(), obs_lon_vals.max() + lon_res, lon_res).astype(np.float32)
|
|
920
|
+
target_lat = np.arange(obs_lat_vals.min(), obs_lat_vals.max() + lat_res, lat_res).astype(np.float32)
|
|
921
|
+
if DEBUG_CHECK:
|
|
922
|
+
print(f" [检查] SD-网格尺寸: 观测网格=({n_lat_obs}, {n_lon_obs}), 目标网格=({len(target_lat)}, {len(target_lon)})")
|
|
923
|
+
if (len(target_lat) != n_lat_obs) or (len(target_lon) != n_lon_obs):
|
|
924
|
+
print(" [警告] 目标网格尺寸与观测气候态不一致,可能导致结果异常或回退。")
|
|
925
|
+
|
|
926
|
+
# 初始化降尺度结果
|
|
927
|
+
hist_downscaled = np.zeros((n_time_hist, len(target_lat), len(target_lon)), dtype=np.float32)
|
|
928
|
+
future_downscaled = np.zeros((n_time_future, len(target_lat), len(target_lon)), dtype=np.float32)
|
|
929
|
+
|
|
930
|
+
# 预构建目标网格点(RegularGridInterpolator输入)
|
|
931
|
+
target_lat_grid, target_lon_grid = np.meshgrid(target_lat, target_lon, indexing='ij')
|
|
932
|
+
target_points = np.column_stack((target_lat_grid.ravel(), target_lon_grid.ravel())).astype(np.float32)
|
|
933
|
+
|
|
934
|
+
# 历史数据降尺度
|
|
935
|
+
print(" 处理历史数据...")
|
|
936
|
+
hist_sample_steps = sorted(set([0, max(0, n_time_hist // 2), max(0, n_time_hist - 1)]))
|
|
937
|
+
for t in range(n_time_hist):
|
|
938
|
+
model_slice = hist_bc_data[t]
|
|
939
|
+
doy_idx = t % n_days # 简单基于时间索引的DOY
|
|
940
|
+
|
|
941
|
+
# 获取当天观测气候态
|
|
942
|
+
obs_clim_today = obs_clim_smooth[doy_idx].copy()
|
|
943
|
+
|
|
944
|
+
# 将观测气候态粗化到模式网格
|
|
945
|
+
obs_clim_coarse = area_weighted_regrid(
|
|
946
|
+
source_lat=obs_ref[obs_lat_coord].values,
|
|
947
|
+
source_lon=obs_ref[obs_lon_coord].values,
|
|
948
|
+
source_data=obs_clim_today,
|
|
949
|
+
target_lat=model_lat,
|
|
950
|
+
target_lon=model_lon
|
|
951
|
+
).astype(np.float32)
|
|
952
|
+
obs_clim_coarse = np.where(obs_clim_coarse < 0, 0, obs_clim_coarse)
|
|
953
|
+
|
|
954
|
+
# 处理数据
|
|
955
|
+
model_slice_safe = np.where(model_slice < MIN_PRECIP, MIN_PRECIP, model_slice).astype(np.float32)
|
|
956
|
+
obs_clim_coarse_safe = np.where(obs_clim_coarse < MIN_PRECIP, MIN_PRECIP, obs_clim_coarse).astype(
|
|
957
|
+
np.float32)
|
|
958
|
+
|
|
959
|
+
# 计算相对变化
|
|
960
|
+
relative_change = model_slice_safe / obs_clim_coarse_safe
|
|
961
|
+
relative_change = np.clip(relative_change, MIN_RATIO, MAX_RATIO).astype(np.float32)
|
|
962
|
+
relative_change = np.where(relative_change < 0, 0, relative_change)
|
|
963
|
+
|
|
964
|
+
# 获取高分辨率气候态
|
|
965
|
+
obs_clim_highres = obs_clim_smooth[doy_idx].astype(np.float32)
|
|
966
|
+
obs_clim_highres_safe = np.where(np.isnan(obs_clim_highres), MIN_PRECIP, obs_clim_highres).astype(np.float32)
|
|
967
|
+
obs_clim_highres_safe = np.where(obs_clim_highres_safe < 0, 0, obs_clim_highres_safe)
|
|
968
|
+
|
|
969
|
+
try:
|
|
970
|
+
# 双线性插值相对变化
|
|
971
|
+
interp_func = RegularGridInterpolator(
|
|
972
|
+
(model_lat, model_lon),
|
|
973
|
+
relative_change,
|
|
974
|
+
method='linear',
|
|
975
|
+
bounds_error=False,
|
|
976
|
+
fill_value=cast(float, None)
|
|
977
|
+
)
|
|
978
|
+
relative_change_highres = interp_func(target_points).reshape(len(target_lat), len(target_lon)).astype(
|
|
979
|
+
np.float32)
|
|
980
|
+
|
|
981
|
+
# 计算降尺度结果
|
|
982
|
+
downscaled_result = (relative_change_highres * obs_clim_highres_safe).astype(np.float32)
|
|
983
|
+
hist_downscaled[t] = np.clip(downscaled_result, 0, None)
|
|
984
|
+
|
|
985
|
+
if DEBUG_CHECK and t in hist_sample_steps:
|
|
986
|
+
print(f" [检查] SD-历史样本时间步 t={t}")
|
|
987
|
+
print_array_diagnostics("SD-历史 model_slice", model_slice)
|
|
988
|
+
print_array_diagnostics("SD-历史 obs_clim_coarse", obs_clim_coarse)
|
|
989
|
+
print_array_diagnostics("SD-历史 relative_change", relative_change)
|
|
990
|
+
print_array_diagnostics("SD-历史 downscaled_result", downscaled_result)
|
|
991
|
+
except Exception as e:
|
|
992
|
+
print(f" 历史数据降尺度失败,时间步 {t}: {e}")
|
|
993
|
+
hist_downscaled[t] = obs_clim_highres_safe
|
|
994
|
+
|
|
995
|
+
# 未来数据降尺度
|
|
996
|
+
print(" 处理未来数据...")
|
|
997
|
+
future_sample_steps = sorted(set([0, max(0, n_time_future // 2), max(0, n_time_future - 1)]))
|
|
998
|
+
for t in range(n_time_future):
|
|
999
|
+
model_slice = future_bc_data[t]
|
|
1000
|
+
doy_idx = t % n_days # 简单基于时间索引的DOY
|
|
1001
|
+
|
|
1002
|
+
# 获取当天观测气候态
|
|
1003
|
+
obs_clim_today = obs_clim_smooth[doy_idx].copy()
|
|
1004
|
+
|
|
1005
|
+
# 将观测气候态粗化到模式网格
|
|
1006
|
+
obs_clim_coarse = area_weighted_regrid(
|
|
1007
|
+
source_lat=obs_ref[obs_lat_coord].values,
|
|
1008
|
+
source_lon=obs_ref[obs_lon_coord].values,
|
|
1009
|
+
source_data=obs_clim_today,
|
|
1010
|
+
target_lat=model_lat,
|
|
1011
|
+
target_lon=model_lon
|
|
1012
|
+
).astype(np.float32)
|
|
1013
|
+
|
|
1014
|
+
# 处理数据
|
|
1015
|
+
model_slice_safe = np.where(model_slice < MIN_PRECIP, MIN_PRECIP, model_slice).astype(np.float32)
|
|
1016
|
+
obs_clim_coarse_safe = np.where(obs_clim_coarse < MIN_PRECIP, MIN_PRECIP, obs_clim_coarse).astype(
|
|
1017
|
+
np.float32)
|
|
1018
|
+
|
|
1019
|
+
# 计算相对变化
|
|
1020
|
+
relative_change = model_slice_safe / obs_clim_coarse_safe
|
|
1021
|
+
relative_change = np.clip(relative_change, MIN_RATIO, MAX_RATIO).astype(np.float32)
|
|
1022
|
+
|
|
1023
|
+
# 获取高分辨率气候态
|
|
1024
|
+
obs_clim_highres = obs_clim_smooth[doy_idx].astype(np.float32)
|
|
1025
|
+
obs_clim_highres_safe = np.where(np.isnan(obs_clim_highres), MIN_PRECIP, obs_clim_highres).astype(
|
|
1026
|
+
np.float32)
|
|
1027
|
+
|
|
1028
|
+
try:
|
|
1029
|
+
# 双线性插值相对变化
|
|
1030
|
+
interp_func = RegularGridInterpolator(
|
|
1031
|
+
(model_lat, model_lon),
|
|
1032
|
+
relative_change,
|
|
1033
|
+
method='linear',
|
|
1034
|
+
bounds_error=False,
|
|
1035
|
+
fill_value=cast(float, None)
|
|
1036
|
+
)
|
|
1037
|
+
relative_change_highres = interp_func(target_points).reshape(len(target_lat), len(target_lon)).astype(
|
|
1038
|
+
np.float32)
|
|
1039
|
+
|
|
1040
|
+
# 计算降尺度结果
|
|
1041
|
+
downscaled_result = (relative_change_highres * obs_clim_highres_safe).astype(np.float32)
|
|
1042
|
+
downscaled_result = np.where(downscaled_result < 0, 0, downscaled_result)
|
|
1043
|
+
future_downscaled[t] = downscaled_result
|
|
1044
|
+
|
|
1045
|
+
if DEBUG_CHECK and t in future_sample_steps:
|
|
1046
|
+
print(f" [检查] SD-未来样本时间步 t={t}")
|
|
1047
|
+
print_array_diagnostics("SD-未来 model_slice", model_slice)
|
|
1048
|
+
print_array_diagnostics("SD-未来 obs_clim_coarse", obs_clim_coarse)
|
|
1049
|
+
print_array_diagnostics("SD-未来 relative_change", relative_change)
|
|
1050
|
+
print_array_diagnostics("SD-未来 downscaled_result", downscaled_result)
|
|
1051
|
+
except Exception as e:
|
|
1052
|
+
print(f" 未来数据降尺度失败,时间步 {t}: {e}")
|
|
1053
|
+
future_downscaled[t] = obs_clim_highres_safe
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
# 写入前再次全量非负截断,确保无负降水
|
|
1057
|
+
hist_downscaled = np.where(hist_downscaled < 0, 0, hist_downscaled)
|
|
1058
|
+
future_downscaled = np.where(future_downscaled < 0, 0, future_downscaled)
|
|
1059
|
+
# 5. 保存BCSD结果(使用原始时间变量)
|
|
1060
|
+
print(" 5. 保存BCSD结果...")
|
|
1061
|
+
|
|
1062
|
+
# 以模型名创建输出目录
|
|
1063
|
+
model_output_dir = os.path.join(output_dir, str(model_name))
|
|
1064
|
+
os.makedirs(model_output_dir, exist_ok=True)
|
|
1065
|
+
|
|
1066
|
+
# 保存文件名
|
|
1067
|
+
hist_bcsd_output_path = os.path.join(model_output_dir, f"{model_name}_historical_BCSD.nc")
|
|
1068
|
+
fut_bcsd_output_path = os.path.join(model_output_dir, f"{model_name}_future_BCSD.nc")
|
|
1069
|
+
|
|
1070
|
+
# 准备时间坐标 - 直接使用原始GCM的时间变量
|
|
1071
|
+
time_coords_hist = np.asarray(hist_time_var.data)
|
|
1072
|
+
time_coords_fut = np.asarray(fut_time_var.data)
|
|
1073
|
+
|
|
1074
|
+
print(f" 历史时间坐标类型: {type(time_coords_hist[0])}")
|
|
1075
|
+
print(f" 未来时间坐标类型: {type(time_coords_fut[0])}")
|
|
1076
|
+
|
|
1077
|
+
# 创建数据集 - 直接使用原始时间坐标,保持不解码状态
|
|
1078
|
+
ds_hist_bcsd = xr.Dataset(
|
|
1079
|
+
{
|
|
1080
|
+
'pr': xr.DataArray(
|
|
1081
|
+
hist_downscaled.astype(np.float32),
|
|
1082
|
+
dims=[gcm_time_coord, obs_lat_coord, obs_lon_coord],
|
|
1083
|
+
coords={
|
|
1084
|
+
gcm_time_coord: time_coords_hist,
|
|
1085
|
+
obs_lat_coord: target_lat.astype(np.float32),
|
|
1086
|
+
obs_lon_coord: target_lon.astype(np.float32)
|
|
1087
|
+
},
|
|
1088
|
+
attrs={
|
|
1089
|
+
'long_name': 'BCSD Downscaled precipitation (historical)',
|
|
1090
|
+
'units': 'mm/day',
|
|
1091
|
+
'method': 'BCSD: CDF Quantile-Matched + FFT smoothed climatology + bilinear interpolation',
|
|
1092
|
+
'calendar_type': calendar_type,
|
|
1093
|
+
# 'resolution'和'reference_period'可由调用者自定义
|
|
1094
|
+
'version': 'v4.0',
|
|
1095
|
+
'model_name': model_name,
|
|
1096
|
+
'processing_step': 'BCSD_complete',
|
|
1097
|
+
'time_coordinate': 'original GCM time coordinate (not decoded)'
|
|
1098
|
+
}
|
|
1099
|
+
)
|
|
1100
|
+
}
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
# 保留原始时间变量的属性
|
|
1104
|
+
if hasattr(hist_time_var, 'attrs'):
|
|
1105
|
+
ds_hist_bcsd[gcm_time_coord].attrs.update(hist_time_var.attrs)
|
|
1106
|
+
ds_hist_bcsd[gcm_time_coord].attrs['note'] = 'Original GCM time coordinate, not decoded'
|
|
1107
|
+
|
|
1108
|
+
ds_future_bcsd = xr.Dataset(
|
|
1109
|
+
{
|
|
1110
|
+
'pr': xr.DataArray(
|
|
1111
|
+
future_downscaled.astype(np.float32),
|
|
1112
|
+
dims=[gcm_time_coord, obs_lat_coord, obs_lon_coord],
|
|
1113
|
+
coords={
|
|
1114
|
+
gcm_time_coord: time_coords_fut,
|
|
1115
|
+
obs_lat_coord: target_lat.astype(np.float32),
|
|
1116
|
+
obs_lon_coord: target_lon.astype(np.float32)
|
|
1117
|
+
},
|
|
1118
|
+
attrs={
|
|
1119
|
+
'long_name': 'BCSD Downscaled precipitation (future)',
|
|
1120
|
+
'units': 'mm/day',
|
|
1121
|
+
'method': 'BCSD: CDF Quantile-Matched + FFT smoothed climatology + bilinear interpolation',
|
|
1122
|
+
'calendar_type': calendar_type,
|
|
1123
|
+
# 'resolution'和'reference_period'可由调用者自定义
|
|
1124
|
+
'version': 'v4.0',
|
|
1125
|
+
'model_name': model_name,
|
|
1126
|
+
'processing_step': 'BCSD_complete',
|
|
1127
|
+
'time_coordinate': 'original GCM time coordinate (not decoded)'
|
|
1128
|
+
}
|
|
1129
|
+
)
|
|
1130
|
+
}
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# 保留未来时间变量的属性
|
|
1134
|
+
if hasattr(fut_time_var, 'attrs'):
|
|
1135
|
+
ds_future_bcsd[gcm_time_coord].attrs.update(fut_time_var.attrs)
|
|
1136
|
+
ds_future_bcsd[gcm_time_coord].attrs['note'] = 'Original GCM time coordinate, not decoded'
|
|
1137
|
+
|
|
1138
|
+
# 保存BCSD结果文件
|
|
1139
|
+
print(" 编码并保存NetCDF文件...")
|
|
1140
|
+
|
|
1141
|
+
# 编码历史数据 - 明确指定不解码时间
|
|
1142
|
+
encoding = {
|
|
1143
|
+
'pr': {'dtype': 'float32', 'zlib': True, 'complevel': 4},
|
|
1144
|
+
gcm_time_coord: {'dtype': 'double', '_FillValue': None}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
try:
|
|
1148
|
+
ds_hist_bcsd.to_netcdf(
|
|
1149
|
+
hist_bcsd_output_path,
|
|
1150
|
+
encoding=encoding,
|
|
1151
|
+
unlimited_dims=[gcm_time_coord]
|
|
1152
|
+
)
|
|
1153
|
+
except Exception as e:
|
|
1154
|
+
print(f" 历史数据保存失败: {e}")
|
|
1155
|
+
# 尝试更简单的保存方式
|
|
1156
|
+
ds_hist_bcsd.to_netcdf(hist_bcsd_output_path, unlimited_dims=[gcm_time_coord])
|
|
1157
|
+
|
|
1158
|
+
# 编码未来数据
|
|
1159
|
+
try:
|
|
1160
|
+
ds_future_bcsd.to_netcdf(
|
|
1161
|
+
fut_bcsd_output_path,
|
|
1162
|
+
encoding=encoding,
|
|
1163
|
+
unlimited_dims=[gcm_time_coord]
|
|
1164
|
+
)
|
|
1165
|
+
except Exception as e:
|
|
1166
|
+
print(f" 未来数据保存失败: {e}")
|
|
1167
|
+
ds_future_bcsd.to_netcdf(fut_bcsd_output_path, unlimited_dims=[gcm_time_coord])
|
|
1168
|
+
|
|
1169
|
+
print(f" BCSD历史数据保存: {hist_bcsd_output_path}")
|
|
1170
|
+
print(f" BCSD未来数据保存: {fut_bcsd_output_path}")
|
|
1171
|
+
|
|
1172
|
+
# 打印时间信息验证
|
|
1173
|
+
print(f"\n 时间坐标验证:")
|
|
1174
|
+
print(f" 历史: {len(time_coords_hist)}个时间点, 类型: {type(time_coords_hist[0])}")
|
|
1175
|
+
print(f" 未来: {len(time_coords_fut)}个时间点, 类型: {type(time_coords_fut[0])}")
|
|
1176
|
+
|
|
1177
|
+
# 检查保存的文件
|
|
1178
|
+
try:
|
|
1179
|
+
check_hist = xr.open_dataset(hist_bcsd_output_path, decode_times=False)
|
|
1180
|
+
check_fut = xr.open_dataset(fut_bcsd_output_path, decode_times=False)
|
|
1181
|
+
print(f" 文件验证通过")
|
|
1182
|
+
check_hist.close()
|
|
1183
|
+
check_fut.close()
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
print(f" 文件验证失败: {e}")
|
|
1186
|
+
|
|
1187
|
+
# 关闭数据集
|
|
1188
|
+
obs_ds.close()
|
|
1189
|
+
hist_ds_original.close()
|
|
1190
|
+
fut_ds_original.close()
|
|
1191
|
+
|
|
1192
|
+
elapsed_time = time.time() - start_time
|
|
1193
|
+
print(f" 空间降尺度耗时: {elapsed_time:.1f}秒")
|
|
1194
|
+
|
|
1195
|
+
return {
|
|
1196
|
+
'hist_bcsd_path': hist_bcsd_output_path,
|
|
1197
|
+
'fut_bcsd_path': fut_bcsd_output_path,
|
|
1198
|
+
'time_coordinate': gcm_time_coord
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
except Exception as e:
|
|
1202
|
+
print(f" 空间降尺度失败: {str(e)}")
|
|
1203
|
+
import traceback
|
|
1204
|
+
traceback.print_exc()
|
|
1205
|
+
for ds_obj in ['obs_ds', 'hist_ds_original', 'fut_ds_original']:
|
|
1206
|
+
if ds_obj in locals():
|
|
1207
|
+
try:
|
|
1208
|
+
locals()[ds_obj].close()
|
|
1209
|
+
except Exception:
|
|
1210
|
+
pass
|
|
1211
|
+
return None
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
# ==================== BCSD完整流程函数 ====================
|
|
1215
|
+
def BCSD(obs_path, gcm_hist_path, gcm_fut_path, model_name, output_dir, ref_start_year, ref_end_year):
|
|
1216
|
+
"""
|
|
1217
|
+
BCSD完整流程:先BC后SD
|
|
1218
|
+
"""
|
|
1219
|
+
print(f"\n处理模式: {model_name}")
|
|
1220
|
+
print("=" * 60)
|
|
1221
|
+
|
|
1222
|
+
# 第一步:偏差校正 (BC)
|
|
1223
|
+
bc_result = BC(
|
|
1224
|
+
obs_path=obs_path,
|
|
1225
|
+
gcm_hist_path=gcm_hist_path,
|
|
1226
|
+
gcm_fut_path=gcm_fut_path,
|
|
1227
|
+
model_name=model_name,
|
|
1228
|
+
ref_start_year=ref_start_year,
|
|
1229
|
+
ref_end_year=ref_end_year
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
if not bc_result:
|
|
1233
|
+
print(f"✗ {model_name} BC失败,停止处理")
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
# 第二步:空间降尺度 (SD)
|
|
1237
|
+
sd_result = SD(
|
|
1238
|
+
obs_path=obs_path,
|
|
1239
|
+
bc_result=bc_result,
|
|
1240
|
+
output_dir=output_dir,
|
|
1241
|
+
ref_start_year=ref_start_year,
|
|
1242
|
+
ref_end_year=ref_end_year
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
if not sd_result:
|
|
1246
|
+
print(f"✗ {model_name} SD失败")
|
|
1247
|
+
return None
|
|
1248
|
+
|
|
1249
|
+
print(f"✓ {model_name} BCSD完整流程完成!")
|
|
1250
|
+
print("=" * 60)
|
|
1251
|
+
|
|
1252
|
+
return {
|
|
1253
|
+
'model_name': model_name,
|
|
1254
|
+
'hist_bcsd_path': sd_result['hist_bcsd_path'],
|
|
1255
|
+
'fut_bcsd_path': sd_result['fut_bcsd_path']
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
# ==================== 主程序 ====================
|