経路断面の内挿

ClimCanvas v1.02.1 対応

鉛直断面図の「向き」で「内挿」と付くもの (等緯度線に沿う・等経度線に沿う・2 点間の大円に沿う) を選ぶと、ClimCanvas は格子点の値をそのまま並べるのではなく、経路の上に点を取って各点の値を周りの格子点から双一次内挿します。この章は、その内挿で具体的に何をしているかを説明します。経緯度が 2 次元の格子 (2 次元座標。領域モデルのランベルト格子、WRF、海洋モデルの格子など) を中心に書きますが、経緯度が 1 次元の通常の格子の「2 点間の大円に沿う」断面も同じ仕組みです。内挿は値を変える処理なので、使ったことは図の下の「この図に適用した処理」に必ず出ます。

なぜ内挿が要るのか

経緯度が 2 次元の格子では、格子の行・列は緯線・経線と一致しません。たとえば ClimCORE のランベルト格子 (817 × 661、5 km) では、1 本の行の中で緯度が 30.3〜35.2°N と約 5° 変わり、1 本の列の中で経度が 10° 近く変わります。「格子の行に沿う」断面は内挿なしで値が正確ですが、「35°N の断面」の代わりにはなりません。

格子の行と緯線と大円
ClimCORE と同じ投影 (標準緯線 30°N・60°N、中心経度 140°E) の合成格子 (60 × 50、100 km)。青の格子の行は両端で緯度が 5° 下がり、橙の緯線とは中央でしか交わりません。緑の大円は格子とは無関係に引いた経路で、経路断面ではこの線の上の点で値を内挿します。

緯線や任意の 2 点間に沿った断面を描くには、経路の上の各点について「その点が格子のどこにあるか」を求め、周りの格子点の値から内挿する必要があります。経緯度が 1 次元の格子なら緯度・経度の軸ごとに 1 次元で位置が求まりますが、2 次元座標の格子では格子線が曲がっているので、経緯度から格子番号への逆算が要ります。これがこの章の中心です。

全体の流れ — 3 段階

内挿は「経路の点 → 小数の格子番号 → 値」の 3 段階で、どの段階も地図投影の知識を使いません。格子の経度・緯度の値 (と変数の値) だけから計算するので、投影パラメータを持たないファイルでも、どの投影の格子でも同じように動きます。

段階 すること 再現スクリプトに埋め込まれる関数
1. 経路の上に点を取る 等緯度線・等経度線は経度 (緯度) を等分、大円は球面上で等間隔の点を取る great_circle_points
2. 点の位置を格子番号で表す 各点が格子のどのマスのどこにあるかを小数の格子番号 (fj, fi) で表す grid_fractional_indices (2 次元) / grid_fractional_indices_1d (1 次元)
3. 周りの 4 格子点から内挿する マスの 4 つの角の値の重み付き平均 (双一次内挿) sample_bilinear
経路の標本化
(a) 経路 (緑) の上に格子間隔ほどの間隔で点を取り、点ごとにそれを含むマス (薄い緑) を見つけます。(b) 同じ点を格子番号の空間で見ると、曲がった格子が正方形のマスの並びになり、各点は小数の格子番号 (fj, fi) で表せます。(c) 点 P を含むマスの中の位置 (a, b)。(d) P の値は 4 角の値の重み付き平均で、各角の重みは P をはさんで反対側にある長方形の面積です (値は例)。

小数の格子番号とは、格子点 (行 j, 列 i) の番号を連続量に広げたものです。図の P の (fj, fi) = (26.27, 27.53) なら、行 26〜27・列 27〜28 のマスの中で、行方向に 0.27、列方向に 0.53 の位置にあることを表します (整数部分がマス、小数部分がマスの中の位置)。

1. 経路の上に点を取る

p(t) = [ sin((1 − t) ω) p1 + sin(t ω) p2 ] / sin ω (t = 0 … 1)
ω = atan2(|p1 × p2|, p1 · p2) (2 点のなす角)
始点からの距離 = t ω R (R = 6371 km)

2. 点の位置を格子番号で表す

経緯度が 2 次元の格子

格子の経度・緯度の 2 次元配列だけを使って、各点の小数の格子番号を求めます。手順は 3 段で、図は点 P (真の格子番号は投影から作った (23.37, 31.62)) の例です。

小数の格子番号を求める手順
(a) 間引いた格子点 (青) と P の球面上の距離を比べて粗い最寄りを探す。(b) 粗い最寄りを中心にした窓の中で本当の最寄りの格子点を探し、窓の中心が最寄りになるまで窓を動かす。(c) 格子番号の空間ではマスは正方形で、P は最寄りの格子点から 1 回の反復でマスの中の位置に収束する。(d) P で球に接する平面に写すと、同じマスは四辺形になる。この平面の上で解く。

手順 1 — 間引いた格子で最寄りを粗く探す (a)。経度・緯度を単位球上の 3 次元ベクトルにすると、2 点の球面上の距離は内積が大きいほど短くなります。そこで、格子を行・列とも間引いた点 (間引き幅 = 格子の大きい方の辺の点数 ÷ 100 を切り上げ) と P の内積をとり、最大のものを粗い最寄りとします。ClimCORE (817 × 661) では間引き幅 9、91 × 74 = 6,734 点との内積で済みます。3 次元ベクトルで比べるので、経度の規約 (0〜360 / −180〜180)・日付変更線・極の影響を受けません。

手順 2 — 窓を動かして本当の最寄りの格子点へ (b)。粗い最寄りを中心に、間引き幅の範囲の窓 (中心 ± 間引き幅) の全格子点と内積をとります。窓の中の最寄りが中心と違えばそこへ窓を動かしてやり直し、中心が窓の中の最寄りになったら確定します。図の例では粗い最寄り (25, 30) から最寄りの格子点 (23, 32) へ 1 回動いて止まります。

手順 3 — P で接する平面の上でニュートン法 (c・d)。最寄りの格子点の周りの格子点を、P で球に接する平面へ心射図法で写します。単位ベクトル v の写る先は次のとおりで、p は P の単位ベクトル、e・n は P での東向き・北向きの単位ベクトルです。この平面では P が原点に来ます。

w = v / (v · p), x = w · e, y = w · n

格子番号の空間で正方形のマス (c) は、平面の上では四辺形になります (d)。マスの 4 つの角の平面座標 x00, x01, x10, x11 (y も同様) から、マスの中の位置 (a, b) (行方向 a、列方向 b、どちらも 0〜1) を平面の上の点へ写す双一次の写像を考え、X = Y = 0 (= P の位置) となる (a, b) をニュートン法で解きます。

X(a, b) = x00 (1 − a)(1 − b) + x01 (1 − a) b + x10 a (1 − b) + x11 a b (Y も同様)

経緯度が 1 次元の格子

経緯度が 1 次元の通常の格子では、緯度・経度の軸ごとに独立に求まります。座標の値を昇順に並べ替えてから、点の値に対応する番号を線形に内挿し (numpy の interp)、範囲外は欠損にします。

格子 点 列番号 fi
0〜357.5°、2.5° 間隔 (144 列) 10° 4.0
同上 −1° (= 359°) 143.6 (357.5° の列と 0° の列の間)
100〜160° の領域格子 −200° (= 160°) 24.0 (東端)
同上 170° 欠損 (領域の外)

2 次元の格子に展開して上の方法を使っても、ほぼ同じ番号になります (2.5° 格子で差 2.7e-3 マス)。完全には一致しないので、通常の格子では 1 次元の方法を使います。

3. 周りの 4 格子点から双一次内挿する

小数の格子番号 (fj, fi) = (j0 + a, i0 + b) の位置の値を、マスの 4 つの角の値から重み付き平均で求めます。f00 は角 (j0, i0)、f01 は (j0, i0 + 1)、f10 は (j0 + 1, i0)、f11 は (j0 + 1, i0 + 1) の値です。

f = f00 (1 − a)(1 − b) + f01 (1 − a) b + f10 a (1 − b) + f11 a b

各角の重みは、点をはさんで反対側にある長方形の面積に等しくなります (下の図 a)。点が角の 1 つに重なればその角の値そのもの、マスの辺の上なら辺の両端の値の線形内挿になります。

双一次内挿の重みと欠損の規則
(a) 各角の重みは、点をはさんで反対側の長方形の面積。(b) 欠損の格子点 (×) が内挿を欠損にする範囲 (橙) は、その点を角に持つ 4 マスの内側と、その点を通る格子線の上だけ。4 マスの外周 (欠損の角の重みが 0) では値が出ます。

制約と注意

再現スクリプトでの確認

内挿に使う関数は、再現スクリプトの冒頭に本文がそのまま埋め込まれます (使うものだけ: 大円なら great_circle_points、2 次元座標の格子なら grid_fractional_indices、1 次元の格子なら grid_fractional_indices_1d、経路の断面では常に sample_bilinear、目盛の経緯度の併記では lonlat_tick_label)。アプリの描画と再現スクリプトは文字どおり同じコードで内挿するので図は一致し、スクリプトは numpy と xarray だけで動きます。2 次元座標の格子で大円の断面を描いたときの該当部分は次のようになります。

path_lon, path_lat, path_x = great_circle_points((118.0, 27.0), (160.0, 44.0), 60)
path_fj, path_fi = grid_fractional_indices(ds0['lon'].transpose('y', 'x').values,
                                           ds0['lat'].transpose('y', 'x').values,
                                           path_lon, path_lat)
da_fill = ds0['t'].sel(time='2024-01-01T06:00:00')
da_fill = sample_bilinear(da_fill, 'y', 'x', path_fj, path_fi, 'path', wrap_x=False)
da_fill = da_fill.assign_coords({'path': path_x, 'path_lon': ('path', path_lon),
                                 'path_lat': ('path', path_lat)})

始点・終点や点の数はリテラルで書かれるので、スクリプトの側で書き換えて断面を動かせます。関数の中身を読めば、この章の手順をそのまま追えます。

検証していること

ClimCanvas のテストでは、この内挿について次を自動で確かめています (tests/test_section_path.py ほか)。