Coverage for graphica/core/grid_data.py: 100.0%
70 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""(x, y, z) の点から 2D マップの格子を作る。GUI には依存しない。
3(x, y) が完全な格子なら pivot でそのまま並べ(is_regular=True、値を変えない)、そうでなければ griddata で補間する。
4"""
5import numpy as np
6import pandas as pd
7from scipy.interpolate import griddata, RegularGridInterpolator
8from typing import Any
10# ドラッグでぴったり水平・垂直には引けないので、各軸の範囲に対するこの割合までは水平・垂直とみなす
11SLICE_AXIS_ALIGNMENT_TOLERANCE = 0.01
13GRID_INTERP_METHODS = ('linear', 'cubic', 'nearest')
16class GridDataError(ValueError):
17 """2D の格子を作れない。"""
20def is_regular_grid(x: Any, y: Any) -> bool:
21 """x と y の全部の組み合わせが、ちょうど1回ずつあるか。"""
22 unique_x = np.unique(x)
23 unique_y = np.unique(y)
24 if len(unique_x) * len(unique_y) != len(x):
25 return False
26 pairs = set(zip(x.tolist(), y.tolist()))
27 return len(pairs) == len(x)
30def compute_z_grid(x: Any, y: Any, z: Any, interp_method: str = 'linear',
31 resolution: tuple[int, int] | list[int] | None = None) -> dict[str, Any]:
32 """{'x_grid', 'y_grid'(ソート済みの1次元), 'z_grid'(shape=(len(y), len(x))、データの無い所は nan), 'is_regular'}。
34 resolution=(nx, ny) は補間するときだけ使う(None なら点の数から決める)。interp_method も補間のときだけ。
35 """
36 if interp_method not in GRID_INTERP_METHODS:
37 raise GridDataError(f"不明な補間方法です: {interp_method}")
39 x = np.asarray(x, dtype=float)
40 y = np.asarray(y, dtype=float)
41 z = np.asarray(z, dtype=float)
43 valid = ~(np.isnan(x) | np.isnan(y) | np.isnan(z))
44 x, y, z = x[valid], y[valid], z[valid]
46 if len(x) == 0:
47 raise GridDataError("有効なデータ点がありません(すべて欠損値です)。")
49 if is_regular_grid(x, y):
50 x_grid = np.unique(x)
51 y_grid = np.unique(y)
52 pivot = pd.DataFrame({'x': x, 'y': y, 'z': z}).pivot(index='y', columns='x', values='z')
53 pivot = pivot.reindex(index=y_grid, columns=x_grid)
54 return {
55 'x_grid': x_grid,
56 'y_grid': y_grid,
57 'z_grid': pivot.values,
58 'is_regular': True,
59 }
61 if resolution is None:
62 n = max(int(np.sqrt(len(x)) * 2), 10)
63 nx, ny = n, n
64 else:
65 nx, ny = resolution
66 if nx < 2 or ny < 2:
67 raise GridDataError("補間グリッドの解像度は2以上である必要があります。")
69 if len(x) < 3:
70 raise GridDataError(
71 "散在データの補間には最低3点が必要です(規則格子ではない点が"
72 f"{len(x)}点しかありません)。"
73 )
75 x_grid = np.linspace(x.min(), x.max(), nx)
76 y_grid = np.linspace(y.min(), y.max(), ny)
77 xx, yy = np.meshgrid(x_grid, y_grid)
78 z_grid = griddata((x, y), z, (xx, yy), method=interp_method)
80 return {
81 'x_grid': x_grid,
82 'y_grid': y_grid,
83 'z_grid': z_grid,
84 'is_regular': False,
85 }
88def extract_slice(x_grid: Any, y_grid: Any, z_grid: Any, start: tuple[float, float], end: tuple[float, float],
89 n_points: int = 200) -> dict[str, Any]:
90 """格子上の線分に沿った断面を返す({'axis_values', 'axis_kind', 'z_values'})。
92 axis_kind はほぼ水平なら 'x'、ほぼ垂直なら 'y'、斜めなら 'distance'(始点からの距離)。格子の外は nan。
93 """
94 x0, y0 = float(start[0]), float(start[1])
95 x1, y1 = float(end[0]), float(end[1])
96 if x0 == x1 and y0 == y1:
97 raise GridDataError("始点と終点が同じ位置です(長さ0の線分は抽出できません)。")
99 x_grid = np.asarray(x_grid, dtype=float)
100 y_grid = np.asarray(y_grid, dtype=float)
101 z_grid = np.asarray(z_grid, dtype=float)
103 x_range = x_grid.max() - x_grid.min() if len(x_grid) > 1 else 1.0
104 y_range = y_grid.max() - y_grid.min() if len(y_grid) > 1 else 1.0
106 is_horizontal = abs(y1 - y0) <= SLICE_AXIS_ALIGNMENT_TOLERANCE * (y_range or 1.0)
107 is_vertical = abs(x1 - x0) <= SLICE_AXIS_ALIGNMENT_TOLERANCE * (x_range or 1.0)
109 if is_horizontal and not is_vertical:
110 axis_kind = 'x'
111 axis_values = np.linspace(x0, x1, n_points)
112 sample_x, sample_y = axis_values, np.full(n_points, y0)
113 elif is_vertical and not is_horizontal:
114 axis_kind = 'y'
115 axis_values = np.linspace(y0, y1, n_points)
116 sample_x, sample_y = np.full(n_points, x0), axis_values
117 else:
118 axis_kind = 'distance'
119 sample_x = np.linspace(x0, x1, n_points)
120 sample_y = np.linspace(y0, y1, n_points)
121 axis_values = np.sqrt((sample_x - x0) ** 2 + (sample_y - y0) ** 2)
123 interpolator = RegularGridInterpolator(
124 (y_grid, x_grid), z_grid, method='linear', bounds_error=False, fill_value=np.nan,
125 )
126 z_values = interpolator(np.column_stack([sample_y, sample_x]))
128 return {'axis_values': axis_values, 'axis_kind': axis_kind, 'z_values': z_values}