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 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
+ # ==================== 主程序 ====================