前回の記事《雪:更快速更流畅的气象数据动画》(中国語)では、予報変数を単一ファイルのコンテナ .xue にまとめ、ブラウザがインデックスを使ってフレームごとに読み込んでいた。 GFS の同じ時次の 2 変数について、フレームごとに着色済みの PMTiles(zoom 0–4)は合計 3,205.87 MB、元の GRIB2 は 137.73 MB、 .xue では約 65 MB まで小さくなった。サイズの問題は解決した。独自コンテナの問題は、自前のデコーダでしか読めないことにある。

2026-09-14、このフォーマットを Pangeo のフォーラムに投稿した。最初の返信は Tom Nicholas からで、質問はこうだった:性能を生んでいるのはこのバイナリフォーマットではなく、可視化に向いた Zarr の chunk 形状を選び、ブラウザ側で WASM デコードすれば足りるのではないか。

答えはその通りである。高速化は二つの要素から来ている。uint8 の単一バイト平面への量子化(圧縮前で 4 分の 1、デコード結果をそのまま WebGL2 の R8 テクスチャとしてアップロードできる)と、再生に合わせて切った chunk である。どちらも標準の Zarr v3 で表現できる。

2026-09-15 から .xue はオンラインでの公開をやめた。すべての時次、ローリングウィンドウの各ラウンド、過去事例はすべて Zarr v3 store のみで公開し、静的な STAC をデータの入口としている。 .xue の元のエンコード・デコードのコードは残してあり、今後エンコード方式を改良する際の参考にする。

現在のパイプラインは、データの流れに沿って次の段階に分かれる:

  1. 生成:GitHub Actions が定期的に上流データを取得し、量子化して Zarr store にエンコードし、公開する;
  2. カタログ:静的な STAC カタログが各データソースの現在の時次を列挙する;
  3. 配信:Cloudflare R2 にデータを置き、CDN がキャッシュし、クライアントは HTTP Range で必要な分を読む。ほかに実験的な読み取り専用の JSON API がある;
  4. 描画:ブラウザ内の Rust WebAssembly がデコードし、WebGL2 が投影と着色を行う;
  5. 分析:Python スクリプトが store を直接読み、synoptic-analysis Skill が天気概況レポートを生成する。

データソース

ソース格子と間隔
NOAA GFS0.25°、F120 まで毎時、以降 F240 まで 3 時間ごと
NOAA GFS Sflux約 13 km のガウス格子、時間軸は GFS と同じ
ECMWF IFS オープンデータ0.25°、F144 まで 3 時間ごと、F240 まで 6 時間ごと
ECMWF IFS オープンデータ 0.1°0.1°、時間軸は上と同じ、0.25° 版より約 2 時間遅れ
ECMWF AIFS Single0.25°、F360 まで 6 時間ごと
ECMWF IFS HRES(Open-Meteo 経由)0.1°、F090 まで毎時、F144 まで 3 時間、F360 まで 6 時間、00Z と 12Z のみ
NOAA HRRR0.03° 米国本土、F18 まで毎時、毎時初期値
DWD ICON-EU0.0625° 欧州、F78 まで毎時、F120 まで 3 時間ごと
Météo-France ARPEGE0.25°、F48 まで毎時、F102 まで 3 時間ごと
NOAA GEFS-Aerosols0.25°、F120 まで 3 時間ごと
CMA-CW/DUST 黄砂予報0.15° アジア、F72 まで毎時、F168 まで 3 時間ごと、00Z と 12Z のみ
NCEP CFSv2地上 0.9375°、上空 1°、F6552(39 週)まで 6 時間ごと、00Z と 12Z のみ
NOAA MRMS レーダー0.02° 米国本土、2 分ごと、直近 4 時間
気象庁 高解像度降水ナウキャスト0.005° 日本、5 分ごと、直近 3 時間
Himawari-90.04°、10 分ごと、直近 6 時間
Himawari-9 日本域0.01°、24–46°N・122–148°E、10 分ごと、直近 3 時間
GOES-19 / GOES-180.04°、10 分ごと、直近 6 時間
Meteosat-120.04°、毎時、直近 24 時間
NOAA SWPC オーロラ1°、5 分ごと、直近 12 時間

ほかに地点プロダクトがある:高層観測(1 地点 1 回の放球)、空港(METAR と TAF)、熱帯低気圧(嵐ごとの経路と各機関・各モデルの予報)、地上観測(アメダス、DWD、香港天文台、10 分ごとに集約)、大気質観測局(AirNow、香港環境保護署、30 分ごと)、気象警報(米国 NWS、気象庁、カナダ ECCC、5 分ごとのスナップショット)、NASA FIRMS の火災検知点(直近 7 日間)。地点プロダクトは JSON で、地点または嵐ごとに Range で 1 行を取得し、ラスターのフォーマットには入らない。例外は NEXRAD の単一サイトレーダーで、最低仰角の反射強度とドップラー速度には緯度経度格子がないため、ビームとレンジビンの極座標のまま Zarr store に入れ、直近 3 時間を保持する。ページのリアルタイム表示は NOAA のバケットにある Level 3 の元オブジェクトを直接読む。

オンラインには各ソースの最新時次だけを残す。過去の気象事例はショーケースとして恒久的に公開し、事例の領域と期間に切り出して、同じエンコーダと store 形式で書く。事例ではリアルタイム公開していないソースも使える:ERA5 再解析、CAMS エーロゾル解析、EFAS の河川流量、富士山上空 500 m の WRF ネスト計算。

データソースは二種類に分かれる。予報ソースは初期時刻ごとに公開され、1 時次 1 ディレクトリで、新しい時次が届くと古いものと置き換わる。観測ソース(レーダー、静止衛星、オーロラ予報)はローリングウィンドウで、数分ごとに 1 ラウンドをビルドし、ウィンドウを前に進め、取得済みのフレームは再ダウンロードしない。最初から最後まで同じ間隔のソースはないため、時間軸は単位と各フレームのオフセットを直接宣言し、等間隔を前提にしない。

ここでいうリアルタイムは、継続的に更新される予報と準リアルタイムの観測を指す:予報は初期時刻の 4–8 時間後に揃い、観測ウィンドウの末尾は上流の公開遅延に応じて、実時間から数分から 30 分ほど遅れる。

ソースごとの違いは取得段階で吸収する。静止衛星は静止衛星投影から緯度経度格子へ、IFS HRES は縮約ガウス格子から、HRRR はランベルト正角円錐図法の格子から、いずれも取得側で一度だけ再投影・再サンプリングする。その後の量子化、グループ化、store の書き出しはすべてのソースで同じ経路を通る。

MRMS を例にとると、プロダクトは 2 分ごとに 1 フレームを出すが、レーダーのボリュームスキャンは 1 周 4–6 分かかるため、新しい低仰角スキャンが届くまで、そのレーダーの範囲内では前のフレームが値ごとそのまま繰り返される。見た目としては、フレームレートは高いのに合成図上のエコーの動きが途切れ途切れになる。エコーの色の塊は全体としては同じ方向に動いているが、細部ではばらばらに動き、飛び飛びに見える。

フォーマットと量子化

アクセスのパターンは、全球予報 1 時次分の連続再生とタイムラインの自由なシークであり、この制約がフォーマットの選択を決めている。

各変数は 1 バイトに量子化する:2 m 気温は −60 から 50 °C を 0.5 °C 刻みで表し、最大誤差は 0.25 °C;降水は対数のコードブックで上限 128 mm/h とし、弱い雨の段階を保つ。量子化後もデータは数値としての意味を保つので、ブラウザは着色して再生するだけでなく、格子点の値を読んだりモデルを比較したりできる。

データはモデルの元の格子のまま置き、再投影も事前着色もしない。投影とカラーパレットは GPU に任せる。時間方向は 6 フレームで 1 chunk とする。フレーム間差分(xue.delta codec)は後述の時系列 store でだけ既定で有効にし、再生用の store には差分をかけないので、汎用の Zarr クライアントは追加の codec を登録しなくてよい。降水はどの store でも元の値のまま保存する。降水域は気象システムとともに移動するため、フレーム間差分のほうがかえって大きくなるからである。空間方向はタイルに切り、タイルの大きさはソースごとに設定する(GFS と ECMWF は 52 × 48)。ブラウザはビューポートが覆うタイルだけを取得する。

レイアウト

bundle は 1 つのレイヤーが使う変数の組で、例えば wind10m は ugrd10m と vgrd10m の 2 変数からなる。 10-23 以降、1 時次の 1 つの解像度階層に属するすべての bundle を 1 つの Zarr group、つまり共有 store に書く:フル解像度は bundles.zarr、格子を 1 つおきに間引いたものは bundles.half.zarr、静止衛星にはさらに 3 つおきの bundles.quarter.zarr がある。 group の中には変数ごとに uint8 の配列が 1 つずつあり、変数 id で名前を付け、形状は [フレーム, 緯度, 経度]。時間・緯度・経度の座標配列は store 全体で一度だけ書き、時間軸や格子が異なる bundle は自前の座標配列を持つ。各 bundle のメタデータは group 文書の xue_bundles 属性に入る。内側の chunk は 6 フレーム × 1 タイルで、chunk ごとに独立した Zstandard フレームになっている。

配列ごとに shard は 1 つだけで、時間軸全体と格子全体を覆うため、1 つの配列はフレーム数によらずバケット上の 2 オブジェクト(zarr.json と shard)になる。 11-01 00Z の GFS の時次では、フル解像度の階層に 79 の bundle、93 の変数配列、4 つの座標配列があり、group 文書を含めて 195 オブジェクトである。 zarr-profile 文書の記録によると、bundle ごとに 1 store だったときの GFS の 1 時次は 1,571 オブジェクトで、そのうち shard は 190 だけだった。残りの大半は store ごとに重複して書かれた group 文書と座標配列である。 shard のインデックスはオブジェクトの末尾にあり、クライアントは HTTP のサフィックス range 1 回でそれを取得し、以降のすべてのフレームとビューポートはそのインデックスで位置を特定する。

公開する codec チェーンは標準の [bytes, zstd] で、どの Zarr クライアントもプラグインなしで読める。 store には各 bundle のメタデータと CF の scale_factor / add_offset が含まれ、xarray.open_zarr で読むとそのまま物理量になる。

もう一つのアクセスパターンは、1 格子点の時系列全体の取得である。 GFS の tmp2m、prate、wind10m、および HRRR と sflux のいくつかの地上変数は、時系列 store(<bundle>.series.zarr)も公開している:内側の chunk は時間軸全体 × 8 × 8 格子点で、xue.delta が既定で有効になっている。 zarr-profile 文書が gfs.2026090918(161 フレーム)で測った結果では、tmp2m の時系列 store は 37.2 MB、再生用の store は 37.0 MB で、1 格子点の時系列全体は 1 chunk、中央値 1.9 KB である。

ローリングウィンドウは数分ごとに再ビルドされ、連続する 2 ラウンドの差は最新の数フレームだけである。これらのソースの store は時刻で区切ったセグメントに分ける:フレーム間隔が 1 時間未満のフィードは 1 時間ごと、毎時以上の間隔のものは 6 時間ごと(00、06、12、18 UTC 起点)。各セグメントはそれ自体が共有 store で、パスは <ソース>.segments/<セグメント開始時刻>/<ダイジェスト>/bundles.zarr、ダイジェストはセグメントのメタデータとコード値に対する SHA-256 の先頭 8 桁である。完全なセグメントはそれを含むどのラウンドでもバイト単位で同一なので、新しいラウンドは以前のラウンドが公開したセグメントを参照し、内容が変わったものだけを書く。ブラウザにキャッシュされた古いセグメントもそのまま使える。 11-01 01 UTC から始まる Himawari-9 のラウンドは、1 時間ごとの 6 セグメントからなる。

カタログ

各時次で manifest.json を書き、その横に派生した STAC 1.1.0 カタログを置く:ルートの Catalog の下に、データソースごとに 1 つの Collection、時次ごとに 1 つの Item、ローリングウィンドウはラウンドごとに 1 つの Item、地点プロダクトも同じ三層構成で、ショーケースにも Collection が 1 つあり、事例ごとに 1 つの Item がある。カタログは manifest とソースのレジストリから生成し、タイムスタンプもホスト名も含まないので、どの文書もディスク上の成果物から再生成できる。リンクはすべて相対パスである。

バケットには各ソースの最新時次だけを残すため、各 Collection には現在の時次を指す固定パスの item.json がある。

読む側では既存のツールを使う:pystac がカタログをたどり、xpystac / odc-stac が Item の Zarr アセットを xarray で開く。時次の Item の変数アセットはすべて同じ bundles.zarr を指すので、開いたあと変数名で配列を選ぶ。 group の文書にはインラインの consolidated metadata が入っているので、ディレクトリ一覧を取得できない素の HTTP バケットでも xr.open_zarr(url) でそのまま開ける。

配信

サイトは Cloudflare Pages 上の静的ページで、データは公開 R2 バケットにあり、間に CDN がある。ページは API 層を通さずにデータを読む。 R2 の送信トラフィックもエッジでのヒットも無料なので、リクエスト数の多寡は主に遅延に影響し、請求額にはほとんど影響しない。

クライアントは必要な chunk を HTTP Range で読み、同じオブジェクト内の隣接する range は 1 回のリクエストにまとめる。全球表示の 1 フレームは時間ブロックごとに 1 リクエスト、ビューポートはタイル行ごとに 1 リクエスト、1 格子点の時系列全体は、時系列 store のある変数では 1 chunk、それ以外は時間ブロックごとに 1 chunk である。再生位置の前後のフレームは、バイト数で予算を決めたキャッシュに先読みする。

新しい時次を公開する前にまず CDN をウォームアップし、その後でポインタを切り替える。最初の訪問者もエッジキャッシュから配信される。

計算はビルド段階に集中させ、ページは静的ファイルだけを読む。空間集計や時次をまたぐ連結といった計算は、エンコーダがあらかじめ成果物として計算しておく。

読み取り専用の JSON データ API xue-api.ringsaturn.me もある。実験的という扱いで、互換性、可用性、鮮度は保証しない。 STAC カタログと同じ経路(Collection → ポインタ → manifest)で時次を解決し、range で store を読み、ページの WASM デコーダと同じ xue crate でデコードして、物理量で返す: /v1/point は緯度経度で 1 地点の時系列を、/v1/cell は指定した時次の格子点の時系列を行と列で返し、地点プロダクトにはそれぞれ地点一覧と地点ごとの記録のエンドポイントがある。ページはこの API に依存せず、API が止まってもサイトとカタログはそのまま使える。

描画

ブラウザ内では Rust WebAssembly の worker が必要に応じて chunk をデコードし、 WebGL2 のフラグメントシェーダがピクセルごとに逆 Web Mercator 投影を行ってからカラーパレットを引く。タイルは格子空間の矩形なので、平面へ戻すのは texSubImage2D 1 回で済み、まだ届いていないタイルはシェーダ内でスキップする。隣接する 2 フレームは GPU 上でブレンドされ、再生時の遷移は連続的になる。

等値線は同じシェーダ内で描き、2 回目の描画パスは要らない。風は GPU のパーティクルレイヤーで描く。 WebCodecs の H.264 経路(?use_h264=true)は同じコード値を可逆圧縮の動画で運ぶ。10-29 以降どのソースもこの動画を生成しなくなったので、この経路は動画を持つ古い時次にだけ使われる。再生中の画面は GIF または MP4 として書き出せ、MP4 はブラウザ内で WebCodecs がエンコードする。

地点プロダクトは地図上にマークとして描く:高層観測点、空港、地上観測点、火災検知点。大気質観測局は PM2.5 濃度で色分けし、警報はレベル 1–5 で警報区域を塗る。

分析

データはオープンな CORS の静的ファイルで、キーは不要であり、ラップされた API を経由せずに読める。リポジトリには synoptic-analysis Skill が含まれており、 Python を実行できる AI アシスタントと組み合わせると、STAC カタログから実データを読み、天気概況を分析し、レポートを書く。例えば次のように依頼できる:

東京の今後 3 日間の天気概況を分析し、GFS と ECMWF の降水と風の予報を比較し、最新の衛星、レーダー、高層観測のデータも合わせて、図、データの時刻、不確実性の説明を含む日本語のレポートを作成してください。

Skill の作業手順は、領域と有効時刻を決め、モデルの場を読み、上層のトラフとリッジ、ジェット気流、地上の気圧系、水蒸気輸送、降水の条件を分析し、観測でモデルの挙動を確かめ、モデル間の違いを比較する、というものである。 1 回の分析で使うデータはまずローカルに保存し、それを計算、作図、検証に使うので、レポート内の数値と図は同じデータに基づく。対象領域、分析の重点、レポートの言語、出力形式はいずれも変更できる。 1 地点の値だけが必要な場合は、データ API の /v1/point が各変数の時系列を物理量で返すので、Python を動かす必要はない。

実際に作成したレポートの例として、台風 Dujuan が東京に影響を及ぼす直前の解析がある:東京都台風気象過程解析(PDF)。作成の経緯は《LLM による複数データソースの気象解析》を参照。

セルフホスティング

独立して運用する場合は、上流のオープンデータを選び、対応する処理パイプラインを動かす。ローカルでは 1 つのコマンドで 1 時次をビルドできる:

1
2
make mvp MODEL=gfs    # ecmwf / ifs / aifs / ifshres / sflux / hrrr / iconeu / arpege / gefsaero / cmadust / cfs も可
make serve

依存関係は Python 3.14 以上(uv で管理)、GRIB ドライバ付きの GDAL 3.8 以上、Node.js 22 以上で、WASM デコーダのビルドには Rust ツールチェーンと wasm-pack が要る。ソースによっては eccodes や認証情報も必要になる。フロントエンドのデータの URL、R2 バケット、パスはいずれも設定できる。データ API のコードもリポジトリにあり、自前でデプロイできる。別のオブジェクトストレージや社内ネットワークへ移す場合は、公開フロー、ネットワークアクセス、権限設定を変更する必要があるが、フォーマット、カタログ、デコーダは変わらない。

デプロイ後に調整できる範囲には、データ(1 つのモデルといくつかのよく使う変数から始め、必要に応じて広げる)、画面(配色、レイヤー、特定の地域向けの構成)、分析(Skill の分析ロジックとレポート形式)がある。

コードは Apache 2.0 OR MIT のデュアルライセンスで、どちらか一方を選べる。いずれも商用利用、改変、クローズドソースでの再配布を認め、著作権表示とライセンス表示の保持だけを求める。 docs/ 以下の仕様文書(コンテナ、Zarr プロファイル、STAC カタログ、地点プロダクト、データ API)は CC BY 4.0 である。公開データは各ソースの条件を引き継ぎ、各 STAC Collection の license と providers に記載している。Xue 自身の寄与(レイアウト、量子化、カタログ)は CC BY 4.0 で、引用時はソースのクレジットの後に「via Xue (xue.ringsaturn.me)」を加える。ソースごとの条件:NOAA のプロダクトはパブリックドメイン。ECMWF のオープンデータ、Open-Meteo 経由の IFS HRES、DWD ICON-EU、EUMETSAT の Meteosat 毎正時データは CC BY 4.0 でクレジット表示が必要。Météo-France ARPEGE は Licence Ouverte / Etalab 2.0。高層観測と CMA の黄砂予報は WMO 統一データポリシーのコアデータ。気象庁の降水ナウキャストは出典の明記が必要。ベースマップは © OpenStreetMap contributors。