0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

線状降水帯予測プロトタイプを作ってみる【r001】Step 2 ー 1時間降水量から3時間積算雨量を作る

0
Posted at

線状降水帯予測プロトタイプを作ってみる【r001】Step 2 ー 1時間降水量から3時間積算雨量を作る

はじめに

前回のStep 1では、Open-MeteoとDIASという異なる取得元からJMA MSMを読み込み、ForecastRunという共通形式へそろえるところまでを作りました。

Step 1の最後に、DIASの実MSM地上面GPVを使って、次のところまで確認しています。

run_id       run_20260612T2100Z
shape        (39, 505, 481)
time_start   2026-06-12 22:00 UTC
time_end     2026-06-14 12:00 UTC
interval     1時間
nan_count    0

ここからStep 2へ進みます。

全体構成の記事では、Step 2の完了条件を次の3点にしていました。

  • 指定した時間窓で積算雨量を作れる
  • 欠測を検出できる
  • PNGまたはGeoTIFFで確認できる

今回は、この条件に合わせて3時間移動積算雨量を実装します。

参考:


Step 2でやること

処理の流れは次のようにしました。

20260921_s01.png

Step 2では、Open-MeteoやDIAS固有の取得処理は書きません。

入力はStep 1で作った、次の共通データだけです。

ForecastRun
├─ time
├─ latitude
├─ longitude
└─ precipitation [mm]

取得元の違いはStep 1で吸収しているため、Step 2ではどちらも同じ1時間降水量Gridとして扱えます。


1. 3時間積算雨量をどう定義するか

Step 1のDIASデータでは、時刻は1時間ごとです。Open-MeteoのJMA APIでも、precipitationは表示時刻までの直前1時間の積算値として扱います。

例えば、

22:00  21:00~22:00の1時間降水量
23:00  22:00~23:00の1時間降水量
00:00  23:00~00:00の1時間降水量

と考えます。

この3つを足すと、

21:00~00:00 の3時間積算雨量

になります。

出力時刻には積算区間の終了時刻である00:00を持たせます。同時に、積算開始時刻として、

accumulation_start_time = 21:00

も保存します。

この2つを残しておけば、後から「どの3時間を積算した値なのか」を確認できます。


2. 「3サンプルを足す」とは決め打ちしない

今回のMSMは1時間間隔なので、3時間積算は3サンプルです。

ただし、将来扱うデータの時間間隔が変わる可能性があります。例えば30分間隔なら、

3時間 / 30分 = 6サンプル

です。

そのためStep 2では、サンプル数を固定せず、

時間窓 = 3時間

を指定し、入力時刻の間隔から必要なサンプル数を求めます。

window_samples = window_duration / input_interval

という考え方です。

これなら、解析処理の中に「3サンプル」という条件を直接埋め込まずに済みます。


3. 欠測は0 mmとして扱わない

ここはStep 2で特に注意したところです。

例えば、

22:00
23:00
01:00

となっていた場合、00:00がありません。

これを、

00:00 = 0 mm

として勝手に補うことはしません。

Step 2では、

22:00 → 23:00 = 1時間
23:00 → 01:00 = 2時間

となった時点で停止します。

降水量Gridの中にNaNやInfが含まれている場合も同様です。

欠測 ≠ 0 mm

として扱い、計算を続けません。


4. Step 2のDockerはネットワークを使わない

Step 2は、Step 1で保存したZarrを入力にしてローカル計算を行います。Open-MeteoやDIASへ通信する必要はありません。

そのため、Composeのstep2サービスは次の構成にしました。

services:
  step2:
    network_mode: none

当初は通常のCompose networkを使っていましたが、開発中にDockerのaddress poolが枯渇し、

all predefined address pools have been fully subnetted

というエラーが出ました。

Step 2自体にはネットワークが不要なので、ここはnetwork_mode: noneへ変更しました。これにより、Step 2を実行するたびにbridge networkを増やさずに済みます。

Step 1のOpen-Meteo取得やDIASダウンロードは外部通信が必要なので、Step 1側のネットワークは残しています。


5. 計算はまずNumPyで実装する

以前はGPUやCuPyも候補として考えましたが、r001のStep 2では使いません。

まずNumPyで実装します。

3時間積算では、格子点ごとにPythonループを回す必要はありません。時間軸方向の累積和を使います。

cumulative = np.cumsum(rainfall, axis=0)

累積和を、

0
1時間目
1+2時間目
1+2+3時間目
...

として持っておけば、3時間窓の和は、

累積値の後端 - 累積値の前端

で求められます。

実装では先頭に0を1枚追加して、

accumulated = cumulative[window:] - cumulative[:-window]

としています。

実際の実装では累積和をfloat64で計算し、長い時間列での丸め誤差を抑えています。出力は設定に応じてfloat32またはfloat64へ変換します。


6. なぜまだNumbaを使わないのか

Numbaを使わないと決めたわけではありません。

Step 2のソースには、将来追加できるようperformanceのoptional dependencyを残しています。

performance = [
  "numba>=0.67,<0.68",
]

ただ、今回の主要処理は、

np.cumsum
配列差分

です。

すでにNumPy内部でまとめて処理されるため、先にNumbaへ移しても効果があるとは限りません。

そこで、

まずNumPy
    ↓
実MSMで計測
    ↓
遅い箇所を確認
    ↓
必要ならNumba

という順番にしました。

ベンチマーク用のCLIも用意しています。

docker compose run --rm step2 \
  rainband-step2 benchmark \
  --manifest \
  /workspace/data/manifests/dias_msm_archive/run_20260612T2100Z.json \
  --repeat 5

Zarrの読込は先に済ませ、NumPyの積算処理部分だけを計測します。


7. Step 1完成版からデータを引き継ぐ

今回のStep 2ソースには、Step 1のコードも同梱しています。

すでにfix012で作った実データがある場合は、再取得する必要はありません。

Step 2のZIPを置いてありますので、参考にしてください。

Step 1のデータをStep 2へコピーします。

cp -a \
  ../linear_rainband_r001_step1_fix012/data/. \
  data/

これでStep 1で作成した、

Raw GRIB2
ForecastRun Zarr
manifest

をStep 2側でもそのまま使えます。


8. Dockerをビルドする

環境変数ファイルを作ります。

cp .env.example .env

sed -i "s/^LOCAL_UID=.*/LOCAL_UID=$(id -u)/" .env
sed -i "s/^LOCAL_GID=.*/LOCAL_GID=$(id -g)/" .env

ビルドします。

docker compose build --no-cache

Step 2でもUbuntu 24.04を基準にしています。

同じimageにStep 1とStep 2の両方を入れているため、必要ならStep 1のCLIも再実行できます。

rainband-step1
rainband-step2

9. ソースを検証する

Step 1と同じく、配布前検査をまとめて実行します。

docker compose run --rm step2 \
  ./scripts/validate_package.sh

検査内容は、

dependency consistency
Shell / Python syntax
Ruff format
Ruff check
mypy
pytest
Step 1 / Step 2 CLI
source contract
package source files

です。

今回、Step 2本体についてはUbuntu 24.04 + Docker実機で、

Ruff format          PASS
Ruff check           PASS
mypy                 PASS
pytest               PASS
source contract      PASS
package source check PASS

まで確認しています。

テストでは、1時間雨量を、

1 mm
2 mm
3 mm
4 mm

とした場合、3時間積算が、

1 + 2 + 3 = 6 mm
2 + 3 + 4 = 9 mm

になることを確認します。

さらに、

  • 時刻が1時間飛んでいる
  • NaNが含まれる
  • Infが含まれる
  • 積算に必要な時刻数が足りない
  • 30分間隔なら6サンプルを使う

といったケースもテストしています。


10. DIAS MSMから3時間積算雨量を作る

Step 1で作成済みのmanifestを指定します。

docker compose run --rm step2 \
  rainband-step2 accumulate \
  --manifest \
  /workspace/data/manifests/dias_msm_archive/run_20260612T2100Z.json \
  --config /workspace/config/step2.yaml

Step 1の実データは39時刻あります。

3時間積算では最初の2時刻はまだ3時間分がそろわないため、

39 - 3 + 1 = 37時刻

になります。

実機での結果は次のとおりでした。

run_id         run_20260612T2100Z
window_hours   3
window_samples 3
shape          (37, 505, 481)
max_mm         219.719

期待していた(37, 505, 481)と一致しています。

最大3時間積算雨量は、この予報Runでは219.719 mmになりました。


11. 出力するデータ

Step 2では3種類の成果物を残します。

3時間積算雨量Zarr

data/processed/accumulation/3h/
└─ dias_msm_archive/
   └─ run_20260612T2100Z.zarr

確認用PNG

data/preview/accumulation/3h/
└─ dias_msm_archive/
   └─ run_20260612T2100Z_max.png

PNGは、全予報時刻について各格子の最大3時間積算雨量を取った確認用画像です。

これはStep 6のWebGISではなく、Step 2の計算結果を目視確認するための簡易出力です。

Step 2 manifest

data/manifests/step2_accumulation/3h/
└─ dias_msm_archive/
   └─ run_20260612T2100Z.json

manifestには、

元のStep 1 manifest
元ZarrのSHA-256
積算時間
入力時間間隔
積算サンプル数
出力Zarr
確認用PNG
各SHA-256

を残します。


12. 保存後にもう一度読み直す

Step 1と同じ考え方で、

ファイルを書けた

だけでは完了にしません。

計算
 ↓
Zarr保存
 ↓
別Datasetとして再読込
 ↓
時刻一致
 ↓
積算雨量一致

まで確認します。

保存前後の積算雨量はnp.testing.assert_allclose()で比較しています。


13. manifestから再検証する

Step 2の成果物も後から検証できます。

docker compose run --rm step2 \
  rainband-step2 verify \
  --manifest \
  /workspace/data/manifests/step2_accumulation/3h/dias_msm_archive/run_20260612T2100Z.json

実機では、

status PASS

まで確認できました。

このverifyでは、

Step 1 source manifest SHA-256
Step 1 ForecastRun verify
Step 2 Zarr SHA-256
Step 2 PNG SHA-256
Step 2 Zarr再読込
積算時間の整合
欠測の有無

を確認します。

元のStep 1データが途中で変わっていた場合も、そのまま処理を続けないようにしています。


14. NumPy版の実測結果

実DIAS MSMでNumPy版を5回計測しました。

input_shape  (39, 505, 481)
output_shape (37, 505, 481)
repeat       5
median_ms    95.184
min_ms       88.170
max_ms       132.585

今回の規模では、3時間積算だけなら中央値で約95 msでした。

少なくともr001のMSM処理では、この部分を急いでNumba化する必要はなさそうです。

まずNumPy版を基準実装として使い、LFM 1 kmやLEPSへ進んでデータ量が増えた段階で改めて見直します。


15. Open-MeteoのForecastRunでも同じコードを使う

DIAS専用の積算処理にはしていません。

Step 1でOpen-Meteo側のmanifestが作られていれば、同じaccumulateをそのまま使えます。

<run_id>のような説明用文字列をそのままシェルへ渡すと、Bashでは<がリダイレクトとして扱われるため、ここでは最新manifestを自動で取得します。

OPEN_METEO_MANIFEST=$(
  find data/manifests/openmeteo_jma_msm \
    -maxdepth 1 \
    -type f \
    -name '*.json' \
    -printf '%T@ %p\n' \
  | sort -nr \
  | head -1 \
  | cut -d' ' -f2-
)

echo "$OPEN_METEO_MANIFEST"

今回の環境では、例えば、

data/manifests/openmeteo_jma_msm/latest_20260920T005852Z.json

が作成されています。

3時間積算は次のように実行します。

docker compose run --rm step2 \
  rainband-step2 accumulate \
  --manifest "/workspace/$OPEN_METEO_MANIFEST" \
  --config /workspace/config/step2.yaml

Open-Meteo側のStep 1データが12時刻なら、期待する出力は、

input  (12, 9, 9)
output (10, 9, 9)

です。

続いてStep 2 manifestを検証します。

RUN_ID=$(basename "$OPEN_METEO_MANIFEST" .json)

OPEN_METEO_STEP2_MANIFEST="/workspace/data/manifests/step2_accumulation/3h/openmeteo_jma_msm/${RUN_ID}.json"

docker compose run --rm step2 \
  rainband-step2 verify \
  --manifest "$OPEN_METEO_STEP2_MANIFEST"

最後にNumPyベンチマークも同じように実行できます。

docker compose run --rm step2 \
  rainband-step2 benchmark \
  --manifest "/workspace/$OPEN_METEO_MANIFEST" \
  --repeat 5

Open-Meteo経路も実データで確認しました。結果は次のとおりです。

run_id         latest_20260920T005852Z
window_hours   3
window_samples 3
shape          (10, 9, 9)
max_mm         11.500
verify         PASS

Step 1のOpen-Meteo入力は(12, 9, 9)だったので、3時間移動積算後の(10, 9, 9)は期待どおりです。
Zarr、PNG、Step 2 manifestも生成され、verifyまで正常に通りました。

これで、DIASとOpen-Meteoのどちらを入力にしても、同じStep 2処理を通せることを実データで確認できました。

性能計測については、実データサイズが大きいDIAS MSMを代表ケースとして採用しています。Open-Meteo側は今回9 × 9格子と小さいため、データ源ごとのbenchmarkをStep 2完了条件にはしていません。


16. DIASとOpen-Meteoの実機結果を並べる

最終的な実機確認結果をまとめると、次のようになりました。

入力 入力shape 出力shape 最大3時間雨量 verify
DIAS MSM (39, 505, 481) (37, 505, 481) 219.719 mm PASS
Open-Meteo JMA MSM (12, 9, 9) (10, 9, 9) 11.500 mm PASS

データの取得方法や格子サイズは違いますが、Step 2ではどちらも同じForecastRunを入力にして同じ処理を通しています。
ここで、Step 1でデータ取得部分を分離した効果を実データでも確認できました。


17. 実データサイズで考える

Step 1で取得したDIAS MSMは、

39 × 505 × 481

です。

float32の入力降水量だけなら約36 MBです。

今回の累積和は丸め誤差を抑えるためfloat64で計算するので、先頭の0面を含む累積配列は約74 MBになります。

3時間積算の出力は、

37 × 505 × 481

で、float32なら約34 MBです。

xarrayやZarrの管理情報を除いても、r001のMSM段階では通常のPCで十分扱える規模でした。

LFM 1 kmやLEPS 21メンバーへ進んだ段階で、必要なら計算方法やメモリ管理を見直します。


18. Step 2の完了条件

Step 2では、次をすべて通したら完了とします。

[✓] Docker build
[✓] Ruff format / check
[✓] mypy
[✓] pytest
[✓] Step 1 manifest verify
[✓] 時刻連続性検査
[✓] 欠測Grid検査
[✓] DIAS 3時間積算
[✓] DIAS 39時刻 → 37時刻
[✓] DIAS Zarr / PNG / manifest生成
[✓] DIAS Step 2 verify
[✓] NumPy benchmark(DIAS代表ケース)
[✓] Open-Meteo 3時間積算
[✓] Open-Meteo 12時刻 → 10時刻
[✓] Open-Meteo Zarr / PNG / manifest生成
[✓] Open-Meteo Step 2 verify

全体構成の記事で決めた、

指定した時間窓で積算雨量を作れる
欠測を検出できる
PNGまたはGeoTIFFで確認できる

という条件を、DIASとOpen-Meteoの両方で確認できました。

性能については、大きな実データであるDIAS MSMを代表ケースとして5回計測し、中央値95.184 msを記録しています。
Open-Meteo側は小さな9 × 9格子なので、個別benchmarkはStep 2完了条件から外しました。

以上をもって、r001 Step 2は正式にCOMPLETEとします。


19. 次は強雨域候補を抽出する

Step 2が完成すると、次はStep 3です。

20260921_s02.png

Step 3では、今回作ったprecipitation_3hを入力にします。

ここから初めて、

強い雨がどこにまとまっているか

を調べる処理に入ります。


まとめ

Step 2では、Step 1で苦労した取得元の違いを意識する必要がなくなりました。

処理対象は、

ForecastRunの1時間降水量

だけです。

そのうえで、

時刻の穴を検出
欠測値を検出
3時間積算
Zarr保存
PNG確認
manifest
verify

までを一つの流れにしました。

実DIAS MSMでは、

39 × 505 × 481
      ↓
37 × 505 × 481

の3時間積算を正常に作成でき、最大3時間雨量は219.719 mmでした。

NumPy処理部分の中央値は95.184 msで、r001のMSM段階では十分軽いことも確認できました。

高速化を先に目的にせず、まずNumPy版を基準にした判断は、この段階では妥当だったと考えています。

Open-Meteo側も同じ共通処理で(12, 9, 9) → (10, 9, 9)、最大3時間雨量11.500 mm、verify PASSまで確認できました。

これでStep 2は正式に完了です。次はStep 3の強雨域候補抽出へ進みます。


参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?