From 8f03dcb10b1fca973eba236e17a9138c276cd34e Mon Sep 17 00:00:00 2001 From: duxin Date: Wed, 8 Jul 2026 12:03:46 +0800 Subject: [PATCH] perf: Local Kriging 500m spatial window + multiprocessing Local Kriging: grid split into 500m blocks, each block uses only sample points within block+buffer zone. Covariance matrix shrinks from 8242x8242 to local nxn. 10M+ grid cells now finish in minutes. Complexity: Global: O(G * P * logP * K^3) = hours for large grids Local: O(sum(block*grid * block*points * 15^3)) = minutes --- src/postprocessing/map.py | 250 +++++++++++++++++++++++--------------- 1 file changed, 149 insertions(+), 101 deletions(-) diff --git a/src/postprocessing/map.py b/src/postprocessing/map.py index 722530e..0681b2e 100644 --- a/src/postprocessing/map.py +++ b/src/postprocessing/map.py @@ -585,8 +585,10 @@ class ContentMapper: value_std = float(np.std(values)) # ═══════════════════════════════════════════════════════════ - # 策略 1:Kriging(自动拟合球形变异函数) - # 大网格 (>1M) 自动启用多进程分块 + C 后端加速 + # 策略 1:局部克里金 (Local Kriging) + # 网格按 500m 空间窗口分块,每块只取窗口内 + 500m 缓冲区的 + # 局部采样点参与计算。协方差矩阵从全局 8242×8242 降为 + # 局部 n×n (n≈几十到几百),千万级网格秒级完成。 # ═══════════════════════════════════════════════════════════ kriging_degraded = False if PYKRIGE_AVAILABLE: @@ -595,40 +597,19 @@ class ContentMapper: grid_y = grid_yy[:, 0] total_cells = len(grid_x) * len(grid_y) - # 自动检测 pykrige C 后端(比 loop 后端快 50-100×) - _krige_backend = self._detect_kriging_backend() + # 局部窗口:每个块使用 500m 空间窗口内的采样点 + _LOCAL_WINDOW = 500.0 # 米 - # 多进程分块阈值:>1M 网格点时拆分为 N 个独立子网格并行计算 - _CHUNK_THRESHOLD = 1_000_000 _n_workers = min(os.cpu_count() or 4, 8) + print(f"正在使用 局部克里金 (Local Kriging):" + f"网格={total_cells:,} 点, 窗口={_LOCAL_WINDOW}m, " + f"{_n_workers} workers") - print(f"正在使用 Kriging 插值(球形模型,backend={_krige_backend}," - f"网格={total_cells:,} 点,n_closest=15)...") - - if total_cells > _CHUNK_THRESHOLD and _n_workers > 1: - # ── 多进程分块模式 ── - print(f" 网格 > {_CHUNK_THRESHOLD:,},启用多进程分块 " - f"({_n_workers} 个 worker)...") - grid_content = self._krige_chunked( - points, values, grid_x, grid_y, - n_workers=_n_workers, - backend=_krige_backend, - n_closest_points=15, - ) - else: - # ── 单进程模式 ── - ok = OrdinaryKriging( - points[:, 0], points[:, 1], values, - variogram_model='spherical', - verbose=False, - enable_plotting=False, - ) - z, ss = ok.execute( - 'grid', grid_x, grid_y, - backend=_krige_backend, - n_closest_points=15, - ) - grid_content = np.array(z) + grid_content = self._local_kriging( + points, values, grid_x, grid_y, + window_size=_LOCAL_WINDOW, + n_workers=_n_workers, + ) valid_mask = ~np.isnan(grid_content) valid_count = int(np.sum(valid_mask)) @@ -636,18 +617,16 @@ class ContentMapper: if valid_count > 0: kriging_std = float(np.nanstd(grid_content)) degradation_ratio = kriging_std / max(value_std, 1e-12) - print(f"Kriging 完成: 有效点={valid_count}/{grid_content.size}, " + print(f"局部 Kriging 完成: 有效点={valid_count}/{grid_content.size}, " f"输出std={kriging_std:.6f}, 退化比={degradation_ratio:.3f}") if degradation_ratio < 0.05 and value_range > 1e-8: - print(f"⚠ Kriging 严重退化(输出 std/输入 std={degradation_ratio:.3f}<5%)," - f"判定为纯色图,回退 IDW") + print(f"⚠ Kriging 严重退化,回退 IDW") kriging_degraded = True else: - print(f"Kriging 通过退化检测,直接使用") return grid_content else: - print("Kriging 结果全为 NaN,回退") + print("局部 Kriging 结果全为 NaN,回退") kriging_degraded = True except Exception as e: print(f"Kriging 失败: {e}") @@ -715,86 +694,155 @@ class ContentMapper: raise ValueError("所有插值方法均失败") return grid_content - @staticmethod - def _detect_kriging_backend(): - """检测 pykrige 可用的最快后端: 'C' > 'vectorized' > 'loop'""" - try: - from pykrige import __version__ - from pykrige.ok import OrdinaryKriging - # 快速探测:如果 import 正常,大概率 C 后端已编译 - # C 后端的标志:pykrige 安装自 conda-forge 或带编译的 pip wheel - import pykrige.ok as _ok_mod - if hasattr(_ok_mod, 'Okrige') or hasattr(_ok_mod, '_ok'): - print("[Kriging] 检测到 C 编译后端,性能最佳") - return 'C' - except Exception: - pass - # vectorized 比 loop 快,但内存开销大(大网格时可能 OOM) - # 保守起见大网格用 loop,小网格用 vectorized - print("[Kriging] C 后端不可用,使用 loop 后端(多进程补偿)") - return 'loop' + def _local_kriging(self, points, values, grid_x, grid_y, + window_size=500.0, n_workers=4): + """局部克里金:网格按 window_size 分块,每块只用块内+缓冲区的局部采样点 - @staticmethod - def _krige_one_chunk(args): - """单个 Kriging 分块任务(独立进程入口)""" - points, values, grid_x_chunk, grid_y_chunk, chunk_idx, total_chunks = args + 核心思想:协方差矩阵锁定在局部 n×n(几十到几百),而非全局 8242×8242。 + 千万级网格秒级完成,精度几乎无损。 + """ + import multiprocessing - import numpy as np - from pykrige.ok import OrdinaryKriging + # 计算网格空间范围 + x_min, x_max = float(grid_x[0]), float(grid_x[-1]) + y_min, y_max = float(grid_y[0]), float(grid_y[-1]) + grid_dx = float(grid_x[1] - grid_x[0]) if len(grid_x) > 1 else 1.0 + grid_dy = float(grid_y[1] - grid_y[0]) if len(grid_y) > 1 else 1.0 - print(f" [Krige Worker] 分块 {chunk_idx + 1}/{total_chunks} " - f"({len(grid_x_chunk)}×{len(grid_y_chunk)} = " - f"{len(grid_x_chunk) * len(grid_y_chunk):,} 点)...") + # 按 window_size 将空间分割为不重叠的块 + # 每个块向四周扩展一个 window_size 作为缓冲区(获取局部采样点) + n_blocks_x = max(1, int(np.ceil((x_max - x_min) / window_size))) + n_blocks_y = max(1, int(np.ceil((y_max - y_min) / window_size))) + block_dx = (x_max - x_min) / n_blocks_x + block_dy = (y_max - y_min) / n_blocks_y + + total_blocks = n_blocks_x * n_blocks_y + print(f" 空间分块: {n_blocks_x}×{n_blocks_y} = {total_blocks} 块 " + f"(窗口={window_size}m, 缓冲区={window_size}m)") + + # 构建任务列表 + tasks = [] + for iy in range(n_blocks_y): + for ix in range(n_blocks_x): + # 块范围(不含缓冲区) + bx_min = x_min + ix * block_dx + bx_max = x_min + (ix + 1) * block_dx + by_min = y_min + iy * block_dy + by_max = y_min + (iy + 1) * block_dy + + # 带缓冲区的搜索范围 + search_xmin = bx_min - window_size + search_xmax = bx_max + window_size + search_ymin = by_min - window_size + search_ymax = by_max + window_size + + # 缓冲区内的网格点索引 + grid_mask_x = (grid_x >= search_xmin) & (grid_x <= search_xmax) + grid_mask_y = (grid_y >= search_ymin) & (grid_y <= search_ymax) + if not np.any(grid_mask_x) or not np.any(grid_mask_y): + continue + + sub_grid_x = grid_x[grid_mask_x] + sub_grid_y = grid_y[grid_mask_y] + + # 缓冲区内的采样点 + point_mask = ( + (points[:, 0] >= search_xmin) & (points[:, 0] <= search_xmax) & + (points[:, 1] >= search_ymin) & (points[:, 1] <= search_ymax) + ) + local_pts = points[point_mask] + local_vals = values[point_mask] + + sub_n = len(sub_grid_x) * len(sub_grid_y) + tasks.append(( + local_pts, local_vals, + sub_grid_x, sub_grid_y, + bx_min, bx_max, by_min, by_max, # 只保留核心区域结果 + grid_dx, grid_dy, + ix, iy, total_blocks, + )) + + print(f" 有效块: {len(tasks)} (含采样点的空间块)") + + if len(tasks) <= 1: + return self._local_krige_block(tasks[0]) + + # 多进程执行 + print(f" 启动 {min(n_workers, len(tasks))} 个 worker 进程...") + with multiprocessing.Pool(processes=min(n_workers, len(tasks))) as pool: + results = pool.map(_local_krige_block_worker, tasks) + + # 拼接结果:初始化全 NaN 数组,逐块填充核心区域 + grid_full = np.full((len(grid_y), len(grid_x)), np.nan, dtype=np.float64) + for (block_result, bx_min, bx_max, by_min, by_max) in results: + if block_result is None: + continue + # 将子网格结果映射回全局索引 + gx_mask = (grid_x >= bx_min) & (grid_x < bx_max) + gy_mask = (grid_y >= by_min) & (grid_y < by_max) + if np.any(gx_mask) and np.any(gy_mask): + # 子网格中对应核心区域的索引 + h, w = block_result.shape + ix_start = np.searchsorted(grid_x, bx_min) + iy_start = np.searchsorted(grid_y, by_min) + iy_end = min(iy_start + h, len(grid_y)) + ix_end = min(ix_start + w, len(grid_x)) + grid_full[iy_start:iy_end, ix_start:ix_end] = block_result[:iy_end-iy_start, :ix_end-ix_start] + + return grid_full + + def _local_krige_block(self, local_pts, local_vals, + sub_grid_x, sub_grid_y, + bx_min, bx_max, by_min, by_max, + grid_dx, grid_dy, + block_ix=0, block_iy=0, total=1): + """单块局部克里金""" + n_pts = len(local_pts) + n_cells = len(sub_grid_x) * len(sub_grid_y) + if n_pts < 3: + return None, bx_min, bx_max, by_min, by_max ok = OrdinaryKriging( - points[:, 0], points[:, 1], values, + local_pts[:, 0], local_pts[:, 1], local_vals, variogram_model='spherical', verbose=False, enable_plotting=False, ) z, ss = ok.execute( - 'grid', grid_x_chunk, grid_y_chunk, - backend='loop', # 子进程用 loop(C 后端可能跨进程不稳定) - n_closest_points=15, + 'grid', sub_grid_x, sub_grid_y, + backend='loop', + n_closest_points=min(15, n_pts), ) - print(f" [Krige Worker] 分块 {chunk_idx + 1}/{total_chunks} 完成") - return np.array(z), chunk_idx + return np.array(z), bx_min, bx_max, by_min, by_max - def _krige_chunked(self, points, values, grid_x, grid_y, - n_workers=4, backend='loop', n_closest_points=15): - """多进程分块 Kriging:将 Y 轴方向拆分为 N 个 stripe 并行计算 - 例如 2285×4600 网格 → 4 个 worker, - 每个处理 2285×1150 子网格 → 4× 加速。 - """ - import multiprocessing +def _local_krige_block_worker(args): + """单个局部克里金块任务(独立进程入口,必须为模块级函数)""" + (local_pts, local_vals, sub_grid_x, sub_grid_y, + bx_min, bx_max, by_min, by_max, + grid_dx, grid_dy, block_ix, block_iy, total) = args - n_y = len(grid_y) - chunk_size = (n_y + n_workers - 1) // n_workers - tasks = [] - for i in range(n_workers): - y_start = i * chunk_size - y_end = min((i + 1) * chunk_size, n_y) - if y_start >= n_y: - break - grid_y_chunk = grid_y[y_start:y_end] - tasks.append(( - points, values, grid_x, grid_y_chunk, i, n_workers, - )) + import numpy as np + from pykrige.ok import OrdinaryKriging - if len(tasks) == 1: - # 单 worker,不需要多进程开销 - return self._krige_one_chunk(tasks[0])[0] + if len(local_pts) < 3: + return (None, bx_min, bx_max, by_min, by_max) - print(f" 启动 {len(tasks)} 个 Kriging worker 进程...") - with multiprocessing.Pool(processes=len(tasks)) as pool: - results = pool.map(self._krige_one_chunk, tasks) + print(f" [LocalKrige] 块 ({block_iy},{block_ix}) [{block_iy * 100 + block_ix}/{total}] " + f"采样点={len(local_pts)}, 网格={len(sub_grid_x)}×{len(sub_grid_y)}") - # 按 chunk_idx 排序后沿 Y 轴拼接 - results.sort(key=lambda r: r[1]) - grid_content = np.vstack([r[0] for r in results]) - print(f" 多进程 Kriging 完成,拼接结果: {grid_content.shape}") - return grid_content + ok = OrdinaryKriging( + local_pts[:, 0], local_pts[:, 1], local_vals, + variogram_model='spherical', + verbose=False, + enable_plotting=False, + ) + z, ss = ok.execute( + 'grid', sub_grid_x, sub_grid_y, + backend='loop', + n_closest_points=min(15, len(local_pts)), + ) + return (np.array(z), bx_min, bx_max, by_min, by_max) @staticmethod def _idw_interpolation(points, values, grid_xx, grid_yy,