AMATERASS Science Lab

静止衛星データで日射量の変化を追ってみよう

AMATERASSの日本域1 km・10分間隔の実データを使い、LinuxとJavaで衛星データ解析に挑戦します。

FIELD: HIMAWARI msm.1km / 1 km / 10 min
日射量マップの例
AMATERASS 日本域1 kmデータの例(2016年8月20日 03:00 UTC)

今日の研究テーマ

日射量は、時間や場所によってどのように変化するのでしょうか。AMATERASSの実データを使って、観測・処理・比較・考察を順に進めます。最初にLEVEL 0でUbuntu環境を確認し、その後7つのMISSIONへ進みます。

1

MISSIONを確認

まず、これから何を調べるのかを確認します。

2

解析を実行

用意されたスクリプトやプログラムを使って、実データを処理します。

3

結果をCHECK

各STEPの最後に結果を確認し、何が起きたかを見て次へ進みます。

7つのLEVELで、解析を1段ずつ進めます

LEVEL 1時間変化を追う
LEVEL 2日ごとに比べる
LEVEL 3地域にズーム
LEVEL 41か月で見る
LEVEL 5月平均との差
LEVEL 61地点の時系列
LEVEL 7PV出力と比較
LEVEL 0 / SYSTEM CHECK

MISSION 0:解析環境を準備しよう

MISSION GOALUbuntuでこのScience Labに必要なJava、GNU Wget 1.x、bzip2、GNU date、ImageMagick、Gnuplotが使えるか、最初にまとめて確認します。
作業場所: このScience Labでは、すべての作業を ~/amaterass_lab で行います。必要なファイルは、そのディレクトリに移動してから wget で取得します。
0

必要なコマンドを一括CHECK

解析を始める前に、必要なコマンドが使えるか確認します。

1. 作業場所で必要なファイルを取得
mkdir -p ~/amaterass_lab
cd ~/amaterass_lab
wget -O system_check.sh https://amaterass.science/ja/science-lab/system_check.sh
実行前にこの配置を確認
~/amaterass_lab/
└── system_check.sh
2. 実行
cd ~/amaterass_lab
chmod +x system_check.sh
./system_check.sh
java OK
javac OK
GNU Wget 1.x OK
bzip2 OK
GNU date OK
ImageMagick OK
Gnuplot OK
SYSTEM READY
NGが出たら: その項目の準備がまだできていません。必要なソフトウェアを確認し、すべて OK になってから次へ進みます。
LEVEL 0 CLEAR
SYSTEM READY が出たらLEVEL 1へ。

もう一段深く:このLEVELの中身

うまくいかないとき

SYSTEM NOT READY になったら、まず NG の行を確認します。ソースの check_cmd が各コマンドを順番に検査しているので、どの道具が不足しているかを切り分けられます。

./system_check.sh
このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1必要なコマンドを探す
流れ 2OK / NGを判定
流れ 3SYSTEM READY
system_check.sh Shell
何をしている? Java、GNU Wget 1.x、bzip2、GNU date、ImageMagick、Gnuplotが使えるかを順番に調べ、すべてそろったときだけ SYSTEM READY を返します。
#!/bin/sh
set -u
ok=1
check_cmd(){ label=$1; cmd=$2; if command -v "$cmd" >/dev/null 2>&1; then printf '%-16s OK\n' "$label"; else printf '%-16s NG\n' "$label"; ok=0; fi; }
check_cmd java java
check_cmd javac javac
if command -v wget1 >/dev/null 2>&1; then printf '%-16s OK\n' 'GNU Wget 1.x'; elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then printf '%-16s OK\n' 'GNU Wget 1.x'; else printf '%-16s NG\n' 'GNU Wget 1.x'; ok=0; fi
check_cmd bzip2 bzip2
if date -u -d '2016-08-20 08:00 +0900' +%Y%m%d%H%M >/dev/null 2>&1; then printf '%-16s OK\n' 'GNU date'; else printf '%-16s NG\n' 'GNU date'; ok=0; fi
if command -v magick >/dev/null 2>&1 || command -v convert >/dev/null 2>&1; then printf '%-16s OK\n' 'ImageMagick'; else printf '%-16s NG\n' 'ImageMagick'; ok=0; fi
check_cmd Gnuplot gnuplot
if [ "$ok" -eq 1 ]; then echo 'SYSTEM READY'; exit 0; fi
echo 'SYSTEM NOT READY'; exit 1
LEVEL 1 / 50 min

MISSION 1:日射量は10分ごとにどう変化する?

MISSION GOALAMATERASS日本域1 kmデータを自分で読み、1枚の地図から約70時刻のGIFアニメーションへ展開します。その前に、データ値・緯度・経度・実観測時刻が同じピクセルで対応していることも確認します。
STEP 1Tmap準備
STEP 24種類のデータ
STEP 3同じpixelを読む
STEP 4–6約70枚を作る
STEP 7GIF完成

AMATERASSの1ピクセルは「値・場所・時刻」で読む

日本域 msm.1km は2521行×3001列。同じ [row, column] の日射量・緯度・経度・時刻が、同じ観測ピクセルを表します。

.tar.gz.java.class.sh.txt.bin / .bz2 / .b.png.gif
1

Tmapを準備する

日射量を地図にするため、Tmap ver.3.0 C² と補助スクリプト draw_map.sh を準備します。

tmap_v3.0cc_
20260815d.tar.gz
Tmap本体
📁
tmap/Javaプログラム
sh
draw_map.shScience Lab補助
draw_map.sh を使って、Science Labで必要な格子情報をTmapへ渡します。
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -c https://downloads.amaterass.science/tmap_v3.0cc_20260815d.tar.gz
wget -O draw_map.sh https://amaterass.science/ja/science-lab/draw_map.sh
実行前にこの配置を確認
~/amaterass_lab/
├── tmap_v3.0cc_20260815d.tar.gz
└── draw_map.sh
2. 実行
cd ~/amaterass_lab
tar xzf tmap_v3.0cc_20260815d.tar.gz
cd tmap
javac *.java
cd ..
chmod +x draw_map.sh
CHECK
ls tmap/*.classtmap.class などが表示されれば準備完了です。
2

最初の1時刻:4種類のファイルをそろえる

2016年8月20日03:00 UTC(12:00 JST)を例に、日射量本体・ピクセル実観測時刻・緯度・経度をそろえます。

LAT緯度
LNG経度
TIMEピクセル実観測時刻
時刻ごと
SOLAR日射量 W/m²
時刻ごと
4ファイルはすべて2521×3001の同じ格子です。data[row,column] ↔ lat[row,column], lng[row,column], time[row,column]
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O download_reference.sh https://amaterass.science/ja/science-lab/download_reference.sh
wget -O download_one.sh https://amaterass.science/ja/science-lab/download_one.sh
実行前にこの配置を確認
~/amaterass_lab/
├── download_reference.sh
└── download_one.sh
2. 実行
cd ~/amaterass_lab
chmod +x download_reference.sh download_one.sh
./download_reference.sh
./download_one.sh 2016 08 20 03 00
CHECK
4つとも展開後サイズは 30,262,084 bytels -l data/reference/*.bin data/sample/*.bin で確認します。
3

同じピクセルの「値・場所・時刻」を読む

Science Labの解析プログラムを準備し、任意地点に最も近いピクセルをLAT/LNGから探して、日射量と実観測時刻を同じ添字から読みます。

TIMEの意味: ファイル名の03:00は基準時刻です。衛星は地球を走査して観測するため、実際の観測時刻はピクセルごとに少し異なります。TIMEファイルは、その日の00:00 UTCからの経過時間を「日」単位で格納しています。
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O analysis_msm1km.tar.gz https://amaterass.science/ja/science-lab/analysis_msm1km.tar.gz
実行前にこの配置を確認
~/amaterass_lab/
├── analysis_msm1km.tar.gz
└── data/
    ├── reference/
    │   ├── standard_2521x3001.lat.msm.1km.bin
    │   └── standard_2521x3001.lng.msm.1km.bin
    └── sample/
        ├── 201608200300.dwn.sw.flx.sfc.msm.1km.bin
        └── 201608200300.grd.time.mjd.hms.msm.1km.bin
2. 実行
cd ~/amaterass_lab
tar xzf analysis_msm1km.tar.gz
javac analysis.msm1km/*.java

java -cp analysis.msm1km pixelinfo data/sample/201608200300.dwn.sw.flx.sfc.msm.1km.bin data/reference/standard_2521x3001.lat.msm.1km.bin data/reference/standard_2521x3001.lng.msm.1km.bin data/sample/201608200300.grd.time.mjd.hms.msm.1km.bin 35.66 138.57
CHECK
pixellatitudelongitudesolar fluxactual UTC が表示されれば、4種類のファイルを同じピクセルで結びつけられています。
4

日射量を1枚の地図にする

ここで初めて、Float32の数値格子をTmapで画像にします。

Float32
日射量BIN2521 × 3001
Java
Tmap値 → 色 + 海岸線
PNG
地図結果は自分で確認
実行前にこの配置を確認
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/sample/
    └── 201608200300.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
./draw_map.sh data/sample/201608200300.dwn.sw.flx.sfc.msm.1km.bin 1400 0 7 "W/m²"
CHECK
data/sample/201608200300.dwn.sw.flx.sfc.msm.1km.bin.png を開き、どんな分布が見えるか観察します。
5

連続した時刻へ拡張する

日本時間の朝から夕方までを見るため、2016年8月19日23:00 UTC(20日08:00 JST)から20日10:40 UTC(19:40 JST)まで、10分ごとの日射量を取得します。コマンドでは対象日と開始・終了時刻をJSTで指定します。データが存在しない時刻は自動的に飛ばします。

23:0023:1023:2010:2010:3010:40
取得できた時刻を、そのまま時間順にアニメーションへ使います。
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O download_animation_data.sh https://amaterass.science/ja/science-lab/download_animation_data.sh
実行前にこの配置を確認
~/amaterass_lab/
└── download_animation_data.sh
2. 実行
cd ~/amaterass_lab
chmod +x download_animation_data.sh
./download_animation_data.sh 2016 08 20 08 00 19 40
CHECK
Available frames: が表示されれば取得完了です。
6

PNGを一気に作る

取得できた日射量ファイルを、時刻順にTmapで画像化します。カラースケールも引数で指定します。

1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O render_animation.sh https://amaterass.science/ja/science-lab/render_animation.sh
実行前にこの配置を確認
~/amaterass_lab/
├── render_animation.sh
├── draw_map.sh
├── data/animation/20160820/
│   ├── timestamps.txt
│   └── 201608....msm.1km.bin
└── tmap/
    └── tmap.class
2. 実行
cd ~/amaterass_lab
chmod +x render_animation.sh
./render_animation.sh 2016 08 20 1400 0 7 "W/m²"
CHECK
PNG frames ready: が表示されればGIFの材料がそろいました。
7

PNGをGIFにする

取得できたPNGを時刻順につなぎます。ここでは横幅1200 px、フレーム間隔5(ImageMagickのdelay値)を指定します。

1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O make_animation.sh https://amaterass.science/ja/science-lab/make_animation.sh
実行前にこの配置を確認
~/amaterass_lab/
├── make_animation.sh
└── data/animation/20160820/
    ├── timestamps.txt
    └── 201608....msm.1km.bin.png
2. 実行
cd ~/amaterass_lab
chmod +x make_animation.sh
./make_animation.sh 2016 08 20 1200 5
LEVEL 1 CLEAR
amaterass_20160820.gif を開き、時間変化を観察します。
OBSERVATION: 日射量の時間変化と地域による違いを見てみましょう。

もう一段深く:このLEVELの中身

うまくいかないとき

最初の1時刻がそろわない場合は、sampleとreferenceを確認して同じコマンドを再実行します。正常ファイルは再利用され、不完全なbinは取り直されます。

ls -lh data/sample/
ls -lh data/reference/
./download_one.sh 2016 08 20 03 00

アニメーションのフレームが少ない場合も、download_animation_data.sh を再実行できます。

このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1データを取得
流れ 2LAT/LNG/TIMEを対応
流れ 3Tmapで地図・GIF
download_reference.sh Shell
何をしている? 全時刻で共通に使うLAT/LNG格子を取得し、解凍後のファイルサイズまで確認します。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then
        echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then
        echo wget
    else
        echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2
        exit 1
    fi
}

EXPECTED=30262084
WGET=$(select_wget)
TOOLS=${AMATERASS_TOOLS_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/tools}
DEST=data/reference
mkdir -p "$DEST"
file_is_complete() { FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=
cleanup_pending() { [ -n "$DECOMP_PID" ] || return 0; kill "$DECOMP_PID" 2>/dev/null || true; wait "$DECOMP_PID" 2>/dev/null || true; rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=; }
trap 'cleanup_pending' 0
trap 'cleanup_pending; exit 1' 1 2 15
finish_decompress() {
    [ -n "$DECOMP_PID" ] || return 0
    if ! wait "$DECOMP_PID"; then rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; echo "Decompression failed: $DECOMP_BZ2" >&2; DECOMP_PID=; return 1; fi
    if ! file_is_complete "$DEST/$DECOMP_NAME"; then SIZE=$(wc -c < "$DEST/$DECOMP_NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; echo "Unexpected file size: $DEST/$DECOMP_NAME ($SIZE bytes)" >&2; DECOMP_PID=; return 1; fi
    rm -f "$DEST/$DECOMP_BZ2"
    DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=
}
for NAME in standard_2521x3001.lat.msm.1km.bin standard_2521x3001.lng.msm.1km.bin
do
    BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then rm -f "$DEST/$BZ2"; echo "Already exists: $DEST/$NAME"; continue; fi
    if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; rm -f "$DEST/$NAME"; fi
    (cd "$DEST" && "$WGET" -c "$TOOLS/$BZ2")
    finish_decompress || exit 1
    rm -f "$DEST/$NAME"
    (cd "$DEST" && bzip2 -d "$BZ2") &
    DECOMP_PID=$!; DECOMP_NAME=$NAME; DECOMP_BZ2=$BZ2
done
finish_decompress || exit 1
trap - 0 1 2 15
echo 'LAT/LNG ready.'
download_one.sh Shell
何をしている? 指定した1時刻について、日射量とTIMEを1本ずつ取得します。途中で壊れた .bin が残っていても、正しい30,262,084 byteでなければ取り直します。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then
        echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then
        echo wget
    else
        echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2
        exit 1
    fi
}

[ "$#" -eq 5 ] || { echo "Usage: $0 YYYY MM DD HH MN" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; HH=$4; MN=$5
case "$YYYY" in [0-9][0-9][0-9][0-9]) ;; *) echo 'YYYY must be four digits.' >&2; exit 1;; esac
case "$MM" in 0[1-9]|1[0-2]) ;; *) echo 'MM must be 01-12.' >&2; exit 1;; esac
case "$DD" in 0[1-9]|[12][0-9]|3[01]) ;; *) echo 'DD must be 01-31.' >&2; exit 1;; esac
case "$HH" in [01][0-9]|2[0-3]) ;; *) echo 'HH must be 00-23.' >&2; exit 1;; esac
case "$MN" in 00|10|20|30|40|50) ;; *) echo 'MN must be one of 00,10,20,30,40,50.' >&2; exit 1;; esac
DAY="${YYYY}${MM}${DD}"; TIME="${DAY}${HH}${MN}"; YYYYMM="${YYYY}${MM}"
EXPECTED=30262084
WGET=$(select_wget)
BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}
DEST=data/sample; DIR="$BASE/$YYYYMM/$DAY"; mkdir -p "$DEST"
file_is_complete() { FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=
cleanup_pending() { [ -n "$DECOMP_PID" ] || return 0; kill "$DECOMP_PID" 2>/dev/null || true; wait "$DECOMP_PID" 2>/dev/null || true; rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=; }
trap 'cleanup_pending' 0
trap 'cleanup_pending; exit 1' 1 2 15
finish_decompress() {
    [ -n "$DECOMP_PID" ] || return 0
    if ! wait "$DECOMP_PID"; then rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; echo "Decompression failed: $DECOMP_BZ2" >&2; DECOMP_PID=; return 1; fi
    if ! file_is_complete "$DEST/$DECOMP_NAME"; then SIZE=$(wc -c < "$DEST/$DECOMP_NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$DECOMP_NAME" "$DEST/$DECOMP_BZ2"; echo "Unexpected file size: $DEST/$DECOMP_NAME ($SIZE bytes)" >&2; DECOMP_PID=; return 1; fi
    rm -f "$DEST/$DECOMP_BZ2"; DECOMP_PID=; DECOMP_NAME=; DECOMP_BZ2=
}
for TYPE in dwn.sw.flx.sfc.msm.1km.bin grd.time.mjd.hms.msm.1km.bin
do
    NAME="$TIME.$TYPE"; BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then rm -f "$DEST/$BZ2"; echo "Already exists: $DEST/$NAME"; continue; fi
    if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; rm -f "$DEST/$NAME"; fi
    (cd "$DEST" && "$WGET" -c "$DIR/$BZ2")
    finish_decompress || exit 1
    rm -f "$DEST/$NAME"
    (cd "$DEST" && bzip2 -d "$BZ2") &
    DECOMP_PID=$!; DECOMP_NAME=$NAME; DECOMP_BZ2=$BZ2
done
finish_decompress || exit 1
trap - 0 1 2 15
echo "Sample solar/TIME ready: $TIME"
draw_map.sh Shell
何をしている? 入力binと対応する格子情報をTmapへ渡して地図画像を作ります。データ処理と可視化を分けるための薄いラッパーです。
#!/bin/sh
set -eu

[ "$#" -eq 5 ] || { echo "Usage: $0 file max min color_div unit" >&2; exit 1; }
FILE=$1
MAX=$2
MIN=$3
DIV=$4
UNIT=$5
[ -f "$FILE" ] || { echo "Missing: $FILE" >&2; exit 1; }
[ -f tmap/tmap.class ] || { echo 'Tmap is not compiled. Run: cd tmap && javac *.java' >&2; exit 1; }

GRID="$FILE.grid.txt"
if [ -f "$GRID" ]; then
    getv() { sed -n "s/^$1=//p" "$GRID" | head -n 1; }
    WIDTH=$(getv width)
    HEIGHT=$(getv height)
    NORTH=$(getv north)
    LAT_SPAN=$(getv lat_span)
    WEST=$(getv west)
    LON_SPAN=$(getv lon_span)
    LAT_DIV=$(getv lat_div)
    LON_DIV=$(getv lon_div)
    : "${LAT_DIV:=6}" "${LON_DIV:=6}"
else
    SIZE=$(wc -c < "$FILE" | tr -d ' ')
    if [ "$SIZE" -ne 30262084 ]; then
        echo "No grid metadata for non-standard file: $FILE" >&2
        exit 1
    fi
    WIDTH=3001
    HEIGHT=2521
    NORTH=47.6
    LAT_SPAN=25.2
    LAT_DIV=5
    WEST=120
    LON_SPAN=30
    LON_DIV=6
fi

(
    cd tmap
    java tmap "../$FILE" "$MAX" "$MIN" "$DIV" "$UNIT" auto png \
      "$NORTH" "$LAT_SPAN" "$LAT_DIV" "$WEST" "$LON_SPAN" "$LON_DIV" \
      "$WIDTH" "$HEIGHT"
)
download_animation_data.sh Shell
何をしている? JSTで指定した開始・終了時刻をUTCへ変換して10分間隔のファイルを取得します。通信は1本のまま、bzip2解凍だけ最大4本(既定)まで並列化します。
#!/bin/sh
set -eu
select_wget(){ if command -v wget1 >/dev/null 2>&1; then echo wget1; elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then echo wget; else echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2; exit 1; fi; }
[ "$#" -eq 7 ] || [ "$#" -eq 8 ] || { echo "Usage: $0 YYYY MM DD START_HH START_MN END_HH END_MN [DECOMP_JOBS]" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; START_HH=$4; START_MN=$5; END_HH=$6; END_MN=$7; JOBS=${8:-4}; DAY="${YYYY}${MM}${DD}"
case "$JOBS" in ''|*[!0-9]*) echo 'DECOMP_JOBS must be a positive integer.' >&2; exit 1;; esac
CHECK=$(date -u -d "${YYYY}-${MM}-${DD}" +%Y%m%d 2>/dev/null || true); [ "$CHECK" = "$DAY" ] || { echo 'Invalid calendar date.' >&2; exit 1; }
START=$(date -u -d "${YYYY}-${MM}-${DD} ${START_HH}:${START_MN} +0900" +%s); END=$(date -u -d "${YYYY}-${MM}-${DD} ${END_HH}:${END_MN} +0900" +%s); [ "$END" -ge "$START" ] || { echo 'End time must not be earlier than start time.' >&2; exit 1; }
EXPECTED=30262084; WGET=$(select_wget); BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}; DEST="data/animation/$DAY"; LIST="$DEST/timestamps.txt"; mkdir -p "$DEST"; : > "$LIST"
file_is_complete(){ FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
QUEUE=; RUNNING=0
cleanup_queue(){ for JOB in $QUEUE; do PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}; kill "$PID" 2>/dev/null || true; wait "$PID" 2>/dev/null || true; rm -f "$DEST/$NAME" "$DEST/$BZ2"; done; QUEUE=; RUNNING=0; }
trap 'cleanup_queue' 0; trap 'cleanup_queue; exit 1' 1 2 15
finish_oldest(){ [ "$RUNNING" -gt 0 ] || return 0; set -- $QUEUE; JOB=$1; shift; QUEUE="$*"; PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}; if ! wait "$PID"; then rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Decompression failed: $BZ2" >&2; exit 1; fi; if ! file_is_complete "$DEST/$NAME"; then SIZE=$(wc -c < "$DEST/$NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Unexpected file size: $DEST/$NAME ($SIZE bytes)" >&2; exit 1; fi; rm -f "$DEST/$BZ2"; echo "Ready: $NAME"; RUNNING=$((RUNNING-1)); }
start_decompress(){ NAME=$1; BZ2=$2; rm -f "$DEST/$NAME"; (cd "$DEST" && bzip2 -d "$BZ2") & PID=$!; QUEUE="${QUEUE}${QUEUE:+ }${PID}|${NAME}|${BZ2}"; RUNNING=$((RUNNING+1)); [ "$RUNNING" -lt "$JOBS" ] || finish_oldest; }
T=$START; TOTAL=0
while [ "$T" -le "$END" ]; do
    TIME=$(date -u -d "@$T" +%Y%m%d%H%M); printf '%s\n' "$TIME" >> "$LIST"; TOTAL=$((TOTAL+1)); YYYYMM=$(printf '%s' "$TIME"|cut -c1-6); YYYYMMDD=$(printf '%s' "$TIME"|cut -c1-8); NAME="$TIME.dwn.sw.flx.sfc.msm.1km.bin"; BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then rm -f "$DEST/$BZ2"; else if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME"|tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; rm -f "$DEST/$NAME"; fi; URL="$BASE/$YYYYMM/$YYYYMMDD/$BZ2"; if (cd "$DEST" && "$WGET" -q -c "$URL"); then start_decompress "$NAME" "$BZ2"; else rm -f "$DEST/$BZ2"; fi; fi
    T=$((T+600))
done
while [ "$RUNNING" -gt 0 ]; do finish_oldest; done
trap - 0 1 2 15
READY=$(while IFS= read -r TIME; do file_is_complete "$DEST/$TIME.dwn.sw.flx.sfc.msm.1km.bin" && echo 1; done < "$LIST" | wc -l | tr -d ' ')
[ "$READY" -gt 1 ] || { echo 'Not enough frames were downloaded.' >&2; exit 1; }
echo "Available frames: $READY / $TOTAL (max $JOBS decompression jobs)"; echo "Timestamp list: $LIST"
render_animation.sh Shell
何をしている? 取得した各時刻のbinをTmapで1枚ずつPNGへ変換します。
#!/bin/sh
set -eu

[ "$#" -eq 7 ] || { echo "Usage: $0 YYYY MM DD MAX MIN COLOR_DIV UNIT" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; MAX=$4; MIN=$5; DIV=$6; UNIT=$7
DAY="${YYYY}${MM}${DD}"
DEST="data/animation/$DAY"
LIST="$DEST/timestamps.txt"
[ -f "$LIST" ] || { echo "Missing: $LIST" >&2; exit 1; }
COUNT=0
while IFS= read -r TIME; do
    [ -n "$TIME" ] || continue
    FILE="$DEST/$TIME.dwn.sw.flx.sfc.msm.1km.bin"
    [ -f "$FILE" ] || continue
    ./draw_map.sh "$FILE" "$MAX" "$MIN" "$DIV" "$UNIT"
    COUNT=$((COUNT + 1))
done < "$LIST"
[ "$COUNT" -gt 1 ] || { echo 'Not enough frames to render.' >&2; exit 1; }
echo "PNG frames ready: $COUNT"
make_animation.sh Shell
何をしている? ImageMagickを使って複数のPNGを1本のGIFアニメーションへまとめます。
#!/bin/sh
set -eu

[ "$#" -eq 5 ] || { echo "Usage: $0 YYYY MM DD WIDTH DELAY" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; WIDTH=$4; DELAY=$5
DAY="${YYYY}${MM}${DD}"
DEST="data/animation/$DAY"
LIST="$DEST/timestamps.txt"
OUT="amaterass_${DAY}.gif"
[ -f "$LIST" ] || { echo "Missing: $LIST" >&2; exit 1; }
case "$WIDTH" in ''|*[!0-9]*) echo 'WIDTH must be a positive integer.' >&2; exit 1;; esac
case "$DELAY" in ''|*[!0-9]*) echo 'DELAY must be a non-negative integer.' >&2; exit 1;; esac
[ "$WIDTH" -gt 0 ] || { echo 'WIDTH must be greater than zero.' >&2; exit 1; }
set --
while IFS= read -r TIME; do
    [ -n "$TIME" ] || continue
    PNG="$DEST/$TIME.dwn.sw.flx.sfc.msm.1km.bin.png"
    [ -f "$PNG" ] || continue
    set -- "$@" "$PNG"
done < "$LIST"
[ "$#" -gt 1 ] || { echo 'Not enough PNG files for an animation.' >&2; exit 1; }
if command -v magick >/dev/null 2>&1; then
    magick -delay "$DELAY" -loop 0 "$@" -resize "${WIDTH}x" "$OUT"
elif command -v convert >/dev/null 2>&1; then
    convert -delay "$DELAY" -loop 0 "$@" -resize "${WIDTH}x" "$OUT"
else
    echo 'ImageMagick is required.' >&2
    exit 1
fi
echo "Created: $OUT ($# frames, ${WIDTH} px wide)"
FloatGrid.java Java
何をしている? Java側でBig-endian Float32格子を読むための共通部品です。各解析プログラムが同じ読み書き方法を使えるようにしています。
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.*;

final class FloatGrid {
    static long floatCount(Path p) throws IOException {
        long size = Files.size(p);
        if ((size & 3L) != 0) throw new IOException("File size is not a multiple of 4: " + p);
        return size / 4L;
    }

    static void requireSameSize(Path... ps) throws IOException {
        long s = Files.size(ps[0]);
        for (Path p : ps) if (Files.size(p) != s) throw new IOException("Grid size mismatch: " + p);
    }

    static MappedByteBuffer map(Path p) throws IOException {
        try (FileChannel ch = FileChannel.open(p, StandardOpenOption.READ)) {
            MappedByteBuffer b = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size());
            b.order(ByteOrder.BIG_ENDIAN);
            return b;
        }
    }

    static float readAt(Path p, long index) throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(p.toFile(), "r")) {
            raf.seek(index * 4L);
            return raf.readFloat();
        }
    }

    static void writeFloats(Path out, float[] data) throws IOException {
        Files.createDirectories(out.toAbsolutePath().getParent());
        try (DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(out)))) {
            for (float v : data) dos.writeFloat(v);
        }
    }

    static Path gridMetaPath(Path bin) {
        return Paths.get(bin.toString() + ".grid.txt");
    }

    static boolean hasGridMeta(Path bin) {
        return Files.isRegularFile(gridMetaPath(bin));
    }

    static Properties readGridMeta(Path bin) throws IOException {
        Path p = gridMetaPath(bin);
        Properties props = new Properties();
        try (Reader r = Files.newBufferedReader(p)) { props.load(r); }
        return props;
    }

    static void copyGridMeta(Path fromBin, Path toBin) throws IOException {
        Path from = gridMetaPath(fromBin);
        Path to = gridMetaPath(toBin);
        if (Files.isRegularFile(from)) {
            Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
        } else {
            Files.deleteIfExists(to);
        }
    }

    static boolean sameGridMeta(Path a, Path b) throws IOException {
        boolean ah = hasGridMeta(a);
        boolean bh = hasGridMeta(b);
        if (!ah && !bh) return true;
        if (ah != bh) return false;
        return readGridMeta(a).equals(readGridMeta(b));
    }
}
pixelinfo.java Java
何をしている? 指定緯度経度に最も近いピクセルをLAT/LNGから探索し、同じindexの日射量とTIMEを読みます。TIMEからそのピクセルの実観測UTCを復元します。
import java.nio.*;
import java.nio.file.*;
import java.time.*;
import java.time.format.*;

public class pixelinfo {
    public static void main(String[] args) throws Exception {
        if (args.length != 6) {
            System.err.println("Usage: pixelinfo solar lat lng time target_lat target_lon");
            System.exit(1);
        }
        Path solar = Paths.get(args[0]), latf = Paths.get(args[1]), lngf = Paths.get(args[2]), timef = Paths.get(args[3]);
        double targetLat = Double.parseDouble(args[4]);
        double targetLon = Double.parseDouble(args[5]);
        FloatGrid.requireSameSize(solar, latf, lngf, timef);
        long n = FloatGrid.floatCount(solar);
        if (n > Integer.MAX_VALUE) throw new IllegalArgumentException("Grid too large");

        MappedByteBuffer lat = FloatGrid.map(latf);
        MappedByteBuffer lng = FloatGrid.map(lngf);
        double best = Double.POSITIVE_INFINITY;
        int bestIndex = -1;
        float bestLat = Float.NaN, bestLon = Float.NaN;
        for (int i = 0; i < (int)n; i++) {
            float la = lat.getFloat(i * 4);
            float lo = lng.getFloat(i * 4);
            if (!Float.isFinite(la) || !Float.isFinite(lo)) continue;
            double dlat = la - targetLat;
            double dlon = (lo - targetLon) * Math.cos(Math.toRadians(targetLat));
            double d2 = dlat*dlat + dlon*dlon;
            if (d2 < best) { best = d2; bestIndex = i; bestLat = la; bestLon = lo; }
        }
        if (bestIndex < 0) throw new IllegalStateException("No valid coordinate found");

        float flux = FloatGrid.readAt(solar, bestIndex);
        float days = FloatGrid.readAt(timef, bestIndex);
        String name = solar.getFileName().toString();
        if (name.length() < 8) throw new IllegalArgumentException("Filename must begin with YYYYMMDD");
        LocalDate date = LocalDate.parse(name.substring(0,8), DateTimeFormatter.BASIC_ISO_DATE);
        long millis = Math.round(days * 86400.0 * 1000.0);
        Instant instant = date.atStartOfDay(ZoneOffset.UTC).toInstant().plusMillis(millis);
        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS 'UTC'").withZone(ZoneOffset.UTC);

        int nx = 3001;
        int row = bestIndex / nx;
        int col = bestIndex % nx;
        System.out.printf("pixel       : row=%d column=%d (0-based)%n", row, col);
        System.out.printf("latitude    : %.6f deg%n", bestLat);
        System.out.printf("longitude   : %.6f deg%n", bestLon);
        System.out.printf("solar flux  : %.3f W/m^2%n", flux);
        System.out.printf("actual UTC  : %s%n", fmt.format(instant));
    }
}
LEVEL 2 / 日平均

MISSION 2:日によって日射量はどれくらい違う?

MISSION GOAL2016年8月19日・20日・21日(UTC)の10分値から3枚の日平均日射量データを作り、同じカラースケールで画像化して比べます。各UTC時間で日射量ファイルがある場合は、その時間に存在する10分値を平均して時間平均を作ります。夜間で日射量ファイルがない時間は0 W/m²とし、24時間の平均から日平均を作ります。
STEP 1解析ツール
STEP 23日間のデータ
STEP 3日平均を計算
STEP 43枚を画像化
GOAL日ごとの差を観察
1

LEVEL 1で準備した解析ツールを確認する

日平均計算に使うプログラムが準備できているか確認します。

実行前にこの配置を確認
~/amaterass_lab/
└── analysis.msm1km/
    ├── pixelinfo.java
    ├── dailymean.java
    ├── crop.java
    ├── monthlymean.java
    ├── anomaly.java
    └── *.class
CHECK
ls analysis.msm1km/*.class で各クラスが表示されれば次へ進みます。
2

3日間の10分値を準備する

2016年8月19日・20日・21日の10分値を、それぞれ取得します。

1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O download_day.sh https://amaterass.science/ja/science-lab/download_day.sh
実行前にこの配置を確認
~/amaterass_lab/
└── download_day.sh
2. 実行
cd ~/amaterass_lab
chmod +x download_day.sh
./download_day.sh 2016 08 19
./download_day.sh 2016 08 20
./download_day.sh 2016 08 21
CHECK
data/daily_raw/20160819/20160820/20160821/ ができれば次へ進みます。
3

日平均を3日分作る

各UTC時間にファイルがあれば、その時間に存在する10分値を平均します。日射量ファイルがない夜間は0 W/m²として、24時間の日平均を作ります。

MISSION予想: 8月19日・20日・21日のうち、日射量の高い地域が最も広がるのはどの日だと思いますか。
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O make_daily_mean.sh https://amaterass.science/ja/science-lab/make_daily_mean.sh
実行前にこの配置を確認
~/amaterass_lab/
├── make_daily_mean.sh
├── analysis.msm1km/
│   └── dailymean.class
└── data/daily_raw/
    ├── 20160819/
    ├── 20160820/
    └── 20160821/
2. 実行
cd ~/amaterass_lab
chmod +x make_daily_mean.sh
./make_daily_mean.sh 2016 08 19
./make_daily_mean.sh 2016 08 20
./make_daily_mean.sh 2016 08 21
CHECK
data/daily/ に日平均ファイルが3個できれば成功です。
4

作った3枚の日平均を同じ条件で画像化する

3枚を同じカラースケールで地図にして比べます。

実行前にこの配置を確認
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/daily/
    ├── 20160819.dailymean....bin
    ├── 20160820.dailymean....bin
    └── 20160821.dailymean....bin
2. 実行
cd ~/amaterass_lab
./draw_map.sh data/daily/20160819.dailymean.dwn.sw.flx.sfc.msm.1km.bin 400 0 4 "W/m²"
./draw_map.sh data/daily/20160820.dailymean.dwn.sw.flx.sfc.msm.1km.bin 400 0 4 "W/m²"
./draw_map.sh data/daily/20160821.dailymean.dwn.sw.flx.sfc.msm.1km.bin 400 0 4 "W/m²"
LEVEL 2 CLEAR
3枚を並べ、日ごとの違いを観察します。

もう一段深く:このLEVELの中身

うまくいかないとき

日平均ができない場合は、まず取得済みの日射量ファイル数を確認します。

find data/daily_raw/20160820 -type f -name '*.bin' -size 30262084c | wc -l

夜間は日射量ファイル自体が作られない時間があります。これは異常ではなく、dailymean.java がその時間を 0 W/m² として24時間平均に入れます。必要なら次を再実行します。

./download_day.sh 2016 08 20
./make_daily_mean.sh 2016 08 20
このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 11日分を取得
流れ 2時間平均
流れ 324時間の日平均
download_day.sh Shell
何をしている? 1日144候補(24時間×6)を順に確認して日射量ファイルを取得します。wgetは1本、解凍は最大4ジョブで進め、不完全なbinは自動的に除去します。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then echo wget
    else echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2; exit 1
    fi
}
[ "$#" -eq 3 ] || [ "$#" -eq 4 ] || { echo "Usage: $0 YYYY MM DD [DECOMP_JOBS]" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; JOBS=${4:-4}
case "$JOBS" in ''|*[!0-9]*) echo 'DECOMP_JOBS must be a positive integer.' >&2; exit 1;; esac
[ "$JOBS" -ge 1 ] || { echo 'DECOMP_JOBS must be at least 1.' >&2; exit 1; }
DAY="${YYYY}${MM}${DD}"; YYYYMM="${YYYY}${MM}"; EXPECTED=30262084
WGET=$(select_wget); BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}; DEST="data/daily_raw/$DAY"
mkdir -p "$DEST"
file_is_complete(){ FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
QUEUE=; RUNNING=0
cleanup_queue(){ for JOB in $QUEUE; do PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}; kill "$PID" 2>/dev/null || true; wait "$PID" 2>/dev/null || true; rm -f "$DEST/$NAME" "$DEST/$BZ2"; done; QUEUE=; RUNNING=0; }
trap 'cleanup_queue' 0
trap 'cleanup_queue; exit 1' 1 2 15
finish_oldest(){
    [ "$RUNNING" -gt 0 ] || return 0
    set -- $QUEUE; JOB=$1; shift; QUEUE="$*"
    PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}
    if ! wait "$PID"; then rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Decompression failed: $BZ2" >&2; exit 1; fi
    if ! file_is_complete "$DEST/$NAME"; then SIZE=$(wc -c < "$DEST/$NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Unexpected file size: $DEST/$NAME ($SIZE bytes)" >&2; exit 1; fi
    rm -f "$DEST/$BZ2"; echo "Ready: $NAME"; RUNNING=$((RUNNING-1))
}
start_decompress(){ NAME=$1; BZ2=$2; rm -f "$DEST/$NAME"; (cd "$DEST" && bzip2 -d "$BZ2") & PID=$!; QUEUE="${QUEUE}${QUEUE:+ }${PID}|${NAME}|${BZ2}"; RUNNING=$((RUNNING+1)); [ "$RUNNING" -lt "$JOBS" ] || finish_oldest; }
for HH in 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23; do
  for MN in 00 10 20 30 40 50; do
    TIME="$DAY$HH$MN"; NAME="$TIME.dwn.sw.flx.sfc.msm.1km.bin"; BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then rm -f "$DEST/$BZ2"; continue; fi
    if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; rm -f "$DEST/$NAME"; fi
    URL="$BASE/$YYYYMM/$DAY/$BZ2"
    if (cd "$DEST" && "$WGET" -q -c "$URL"); then start_decompress "$NAME" "$BZ2"; else rm -f "$DEST/$BZ2"; fi
  done
done
while [ "$RUNNING" -gt 0 ]; do finish_oldest; done
trap - 0 1 2 15
COUNT=$(find "$DEST" -maxdepth 1 -type f -name "$DAY*.dwn.sw.flx.sfc.msm.1km.bin" -size 30262084c | wc -l | tr -d ' ')
[ "$COUNT" -gt 0 ] || { echo "No solar-radiation files were downloaded for $DAY. Check the date or network connection." >&2; exit 1; }
echo "Ready: $DAY ($COUNT files, max $JOBS decompression jobs)"
make_daily_mean.sh Shell
何をしている? 入力ディレクトリと出力先を準備し、必要ならJavaを再コンパイルして dailymean.java を実行します。
#!/bin/sh
set -eu
[ "$#" -eq 3 ] || { echo "Usage: $0 YYYY MM DD" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; DAY="${YYYY}${MM}${DD}"; EXPECTED=30262084
SRC=analysis.msm1km/dailymean.java; CLS=analysis.msm1km/dailymean.class
[ -f "$SRC" ] || { echo 'Missing dailymean.java. Extract analysis_msm1km.tar.gz first.' >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
IN="data/daily_raw/$DAY"; OUT="data/daily/$DAY.dailymean.dwn.sw.flx.sfc.msm.1km.bin"; [ -d "$IN" ] || { echo "Missing: $IN" >&2; exit 1; }; mkdir -p data/daily; rm -f "$OUT"; java -cp analysis.msm1km dailymean "$IN" "$OUT"; SIZE=$(wc -c < "$OUT"|tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ] || { rm -f "$OUT"; echo "Unexpected daily-mean size: $OUT ($SIZE bytes)" >&2; exit 1; }
dailymean.java Java
何をしている? 各UTC時について存在する10分値を平均して時間平均を作り、24個の時間平均をさらに平均します。完全な夜間でファイルが0個の時間は0 W/m²として扱います。
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;

public class dailymean {
    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: dailymean input_day_directory output.bin");
            System.exit(1);
        }
        Path dir = Paths.get(args[0]);
        Path out = Paths.get(args[1]);
        if (!Files.isDirectory(dir)) throw new IOException("Missing directory: " + dir);

        List<Path> all;
        try (Stream<Path> s = Files.list(dir)) {
            all = s.filter(p -> p.getFileName().toString().matches("\\d{12}\\.dwn\\.sw\\.flx\\.sfc\\.msm\\.1km\\.bin"))
                   .sorted().collect(Collectors.toList());
        }
        if (all.isEmpty()) throw new IOException("No 10-minute files in " + dir);
        long nLong = FloatGrid.floatCount(all.get(0));
        if (nLong > Integer.MAX_VALUE) throw new IOException("Grid too large");
        int n = (int)nLong;
        for (Path p : all) if (FloatGrid.floatCount(p) != nLong) throw new IOException("Size mismatch: " + p);

        float[] daily = new float[n];
        float[] hour = new float[n];
        for (int hh = 0; hh < 24; hh++) {
            final String hourKey = String.format("%02d", hh);
            List<Path> files = all.stream().filter(p -> {
                String x = p.getFileName().toString();
                return x.substring(8,10).equals(hourKey);
            }).collect(Collectors.toList());
            if (files.isEmpty()) {
                // AMATERASS solar-radiation files are not produced during fully dark hours.
                // For a 24-hour daily mean, those hours contribute 0 W/m^2.
                System.out.printf("hour %02d: 0 files -> 0 W/m^2%n", hh);
                continue;
            }
            Arrays.fill(hour, 0f);
            for (Path p : files) {
                MappedByteBuffer b = FloatGrid.map(p);
                for (int i=0;i<n;i++) hour[i] += b.getFloat(i*4);
            }
            float inv = 1.0f / files.size();
            for (int i=0;i<n;i++) daily[i] += hour[i] * inv;
            System.out.printf("hour %02d: %d files%n", hh, files.size());
        }
        for (int i=0;i<n;i++) daily[i] /= 24.0f;
        FloatGrid.writeFloats(out, daily);
        System.out.println("Created: " + out);
    }
}
LEVEL 3 / 部分切り出し

MISSION 3:地域を切り出してみよう

MISSION GOAL2016年7月19日・20日・21日の03:00 UTCの日射量10分値を取得し、日本域3001×2521の格子から137–140°E・34–37°Nだけを切り出します。平均処理はせず、同じ時刻の元データから空間範囲だけを小さくします。
STEP 1範囲を確認
STEP 23時刻を取得
STEP 33枚を切り出す
STEP 4地図で比較
1

切り出す緯度・経度を確認する

ここでは例として、137°E〜140°E34°N〜37°N の範囲を使います。

3001
×
2521
日本域約30 MB
300
×
300
137–140°E
34–37°N
約0.35 MB
同じ日射量データから、必要な地域だけを取り出します。値を平均したり時間方向にまとめたりはしません。
LAT/LNGを使った切り出し: crop_solar.sh の引数で切り出す範囲を指定します。crop.javaLAT / LNG ファイルから利用可能な領域を確認し、指定範囲が完全に領域内にある場合だけ対応する [row,column] を日射量データから切り出します。領域外にはみ出した指定は、勝手に切り詰めずエラーにします。
2

7月19日・20日・21日の03:00 UTCを取得する

3日とも同じ時刻の10分値を使います。03:00 UTCは日本時間12:00です。

1. 作業場所で必要なスクリプトを取得
cd ~/amaterass_lab
wget -O download_solar.sh https://amaterass.science/ja/science-lab/download_solar.sh
実行前にこの配置を確認
~/amaterass_lab/
└── download_solar.sh
2. 実行
cd ~/amaterass_lab
chmod +x download_solar.sh
./download_solar.sh 2016 07 19 03 00
./download_solar.sh 2016 07 20 03 00
./download_solar.sh 2016 07 21 03 00
CHECK
data/crop_source/201607190300201607200300201607210300 の日射量ファイルがそろえば次へ進みます。
3

3枚を同じ範囲に切り出す

LAT/LNGを使って、3枚それぞれから137–140°E・34–37°Nを切り出します。

1. 作業場所で必要なスクリプトを取得
cd ~/amaterass_lab
wget -O crop_solar.sh https://amaterass.science/ja/science-lab/crop_solar.sh
実行前にこの配置を確認
~/amaterass_lab/
├── crop_solar.sh
├── analysis.msm1km/
│   └── crop.class
├── data/reference/
│   ├── standard_2521x3001.lat.msm.1km.bin
│   └── standard_2521x3001.lng.msm.1km.bin
└── data/crop_source/
    ├── 201607190300.dwn.sw.flx.sfc.msm.1km.bin
    ├── 201607200300.dwn.sw.flx.sfc.msm.1km.bin
    └── 201607210300.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
chmod +x crop_solar.sh
./crop_solar.sh 2016 07 19 03 00 137 140 34 37
./crop_solar.sh 2016 07 20 03 00 137 140 34 37
./crop_solar.sh 2016 07 21 03 00 137 140 34 37
source index : column=1701..2000 row=1060..1359 (0-based)
grid : 300 x 300
grid centers : 137.005E..139.995E / 36.995N..34.005N
CHECK
data/crop/ に300×300の切り出しファイルが3個できれば成功です。
4

切り出した3枚を画像化する

3日とも同じ03:00 UTC・同じ地域・同じカラースケールで地図にします。

実行前にこの配置を確認
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/crop/
    ├── 201607190300.dwn.sw.flx.sfc.msm.1km.crop.bin
    ├── 201607200300.dwn.sw.flx.sfc.msm.1km.crop.bin
    └── 201607210300.dwn.sw.flx.sfc.msm.1km.crop.bin
2. 実行
cd ~/amaterass_lab
./draw_map.sh data/crop/201607190300.dwn.sw.flx.sfc.msm.1km.crop.bin 1400 0 7 "W/m²"
./draw_map.sh data/crop/201607200300.dwn.sw.flx.sfc.msm.1km.crop.bin 1400 0 7 "W/m²"
./draw_map.sh data/crop/201607210300.dwn.sw.flx.sfc.msm.1km.crop.bin 1400 0 7 "W/m²"
LEVEL 3 CLEAR
同じ時刻の3日間について、必要な地域だけを切り出して比較できました。

もう一段深く:このLEVELの中身

うまくいかないとき

切り出しファイルができない場合は、元の日射量とLAT/LNGがあるか確認します。

ls data/crop_source/
ls data/reference/

ERROR: Requested crop is outside the available grid. と表示された場合は、指定した緯度経度の一部が msm.1km の領域外です。エラー表示の Available longitude / latitude に収まるように範囲を指定し直してください。

Javaソースを更新した場合、crop_solar.shcrop.java がclassより新しければ自動的に再コンパイルします。

このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1LAT/LNGで範囲検索
流れ 2同じindexを切り出す
流れ 3Tmapで比較
download_solar.sh Shell
何をしている? 指定した1時刻の日射量だけをcrop用の入力ディレクトリへ取得します。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then
        echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then
        echo wget
    else
        echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2
        exit 1
    fi
}

[ "$#" -eq 5 ] || { echo "Usage: $0 YYYY MM DD HH MN" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; HH=$4; MN=$5
case "$YYYY" in [0-9][0-9][0-9][0-9]) ;; *) echo 'YYYY must be four digits.' >&2; exit 1;; esac
case "$MM" in 0[1-9]|1[0-2]) ;; *) echo 'MM must be 01-12.' >&2; exit 1;; esac
case "$DD" in 0[1-9]|[12][0-9]|3[01]) ;; *) echo 'DD must be 01-31.' >&2; exit 1;; esac
case "$HH" in [01][0-9]|2[0-3]) ;; *) echo 'HH must be 00-23.' >&2; exit 1;; esac
case "$MN" in 00|10|20|30|40|50) ;; *) echo 'MN must be one of 00,10,20,30,40,50.' >&2; exit 1;; esac
DAY="${YYYY}${MM}${DD}"; TIME="${DAY}${HH}${MN}"; YYYYMM="${YYYY}${MM}"
EXPECTED=30262084
WGET=$(select_wget)
BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}
DEST=data/crop_source; NAME="$TIME.dwn.sw.flx.sfc.msm.1km.bin"; BZ2="$NAME.bz2"; URL="$BASE/$YYYYMM/$DAY/$BZ2"
mkdir -p "$DEST"
file_is_complete() { FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
if file_is_complete "$DEST/$NAME"; then
    rm -f "$DEST/$BZ2"
else
    if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; fi
    rm -f "$DEST/$NAME"
    (cd "$DEST" && "$WGET" -c "$URL")
    (cd "$DEST" && bzip2 -d "$BZ2")
fi
if ! file_is_complete "$DEST/$NAME"; then SIZE=$(wc -c < "$DEST/$NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Unexpected file size: $DEST/$NAME ($SIZE bytes)" >&2; exit 1; fi
rm -f "$DEST/$BZ2"
echo "Ready: $DEST/$NAME"
crop_solar.sh Shell
何をしている? 緯度経度範囲と入力ファイルを crop.java に渡します。crop.java が更新されていれば自動再コンパイルし、LAT/LNGと日射量の同じ[row,column]を使って切り出します。
#!/bin/sh
set -eu

[ "$#" -eq 9 ] || { echo "Usage: $0 YYYY MM DD HH MN WEST EAST SOUTH NORTH" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; HH=$4; MN=$5; WEST=$6; EAST=$7; SOUTH=$8; NORTH=$9
TIME="${YYYY}${MM}${DD}${HH}${MN}"
SRC=analysis.msm1km/crop.java; CLS=analysis.msm1km/crop.class
[ -f "$SRC" ] || { echo 'Missing crop.java. Extract analysis_msm1km.tar.gz first.' >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
LAT=data/reference/standard_2521x3001.lat.msm.1km.bin
LNG=data/reference/standard_2521x3001.lng.msm.1km.bin
[ -f "$LAT" ] && [ -f "$LNG" ] || { echo 'LAT/LNG files are required.' >&2; exit 1; }
IN="data/crop_source/$TIME.dwn.sw.flx.sfc.msm.1km.bin"
OUT="data/crop/$TIME.dwn.sw.flx.sfc.msm.1km.crop.bin"
[ -f "$IN" ] || { echo "Missing: $IN" >&2; exit 1; }
mkdir -p data/crop
java -cp analysis.msm1km crop "$IN" "$LAT" "$LNG" "$WEST" "$EAST" "$SOUTH" "$NORTH" "$OUT"
crop.java Java
何をしている? LAT/LNG格子から利用可能な領域を求め、指定範囲が完全に格子内にあることを確認してから対応する行・列を切り出します。領域外はエラーにし、黙ってクリップしません。
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.util.*;

public class crop {
    static final int NX = 3001;
    static final int NY = 2521;
    static final double RANGE_EPS = 1.0e-5;

    public static void main(String[] args) throws Exception {
        if (args.length != 8) {
            System.err.println("Usage: crop input lat lng west east south north output");
            System.exit(1);
        }
        Path in=Paths.get(args[0]), latf=Paths.get(args[1]), lngf=Paths.get(args[2]), out=Paths.get(args[7]);
        double west=Double.parseDouble(args[3]), east=Double.parseDouble(args[4]);
        double south=Double.parseDouble(args[5]), north=Double.parseDouble(args[6]);

        // Never leave an old crop behind when the current request fails.
        Files.deleteIfExists(out);
        Files.deleteIfExists(Paths.get(out.toString()+".grid.txt"));

        FloatGrid.requireSameSize(in, latf, lngf);
        if (FloatGrid.floatCount(in) != (long)NX*NY) throw new IOException("Expected 2521x3001 msm.1km grid");

        if (!Double.isFinite(west) || !Double.isFinite(east) ||
            !Double.isFinite(south) || !Double.isFinite(north)) {
            failInvalidBounds(west, east, south, north,
                    "All crop bounds must be finite numbers.");
        }
        if (west >= east || south >= north) {
            failInvalidBounds(west, east, south, north,
                    "WEST must be smaller than EAST, and SOUTH must be smaller than NORTH.");
        }

        MappedByteBuffer lat = FloatGrid.map(latf);
        MappedByteBuffer lng = FloatGrid.map(lngf);

        // Derive the available geographic domain from the coordinate grids.
        float firstLat=lat.getFloat(0);
        float lastLat=lat.getFloat(((NY-1)*NX+(NX-1))*4);
        float firstLon=lng.getFloat(0);
        float lastLon=lng.getFloat(((NY-1)*NX+(NX-1))*4);
        double latStep=Math.abs(lat.getFloat(NX*4)-firstLat);
        double lonStep=Math.abs(lng.getFloat(4)-firstLon);
        double gridNorth=Math.max(firstLat,lastLat)+latStep/2.0;
        double gridSouth=Math.min(firstLat,lastLat)-latStep/2.0;
        double gridWest=Math.min(firstLon,lastLon)-lonStep/2.0;
        double gridEast=Math.max(firstLon,lastLon)+lonStep/2.0;

        // The requested rectangle must be completely inside the data domain.
        // Do not silently clip a partly out-of-range request.
        if (west < gridWest-RANGE_EPS || east > gridEast+RANGE_EPS ||
            south < gridSouth-RANGE_EPS || north > gridNorth+RANGE_EPS) {
            failOutsideGrid(west,east,south,north,gridWest,gridEast,gridSouth,gridNorth);
        }

        int c0=-1,c1=-1,r0=-1,r1=-1;
        for (int c=0;c<NX;c++) {
            float lo=lng.getFloat(c*4);
            if (lo >= west && lo < east) { if(c0<0)c0=c; c1=c; }
        }
        for (int r=0;r<NY;r++) {
            float la=lat.getFloat((r*NX)*4);
            if (la < north && la >= south) { if(r0<0)r0=r; r1=r; }
        }
        if (c0<0 || r0<0) {
            System.err.println("ERROR: Requested crop contains no grid-cell centers.");
            System.err.printf(Locale.ROOT,"Requested longitude: %.3f .. %.3f E%n",west,east);
            System.err.printf(Locale.ROOT,"Requested latitude : %.3f .. %.3f N%n",south,north);
            System.err.printf(Locale.ROOT,"Grid spacing       : %.3f deg (lon), %.3f deg (lat)%n",lonStep,latStep);
            System.exit(2);
        }

        int width=c1-c0+1, height=r1-r0+1;
        Files.createDirectories(out.toAbsolutePath().getParent());
        try (FileChannel src=FileChannel.open(in, StandardOpenOption.READ);
             FileChannel dst=FileChannel.open(out, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
            ByteBuffer row=ByteBuffer.allocate(width*4);
            for(int r=r0;r<=r1;r++) {
                row.clear();
                long pos=((long)r*NX+c0)*4L;
                int need=width*4;
                while(row.position()<need) {
                    int x=src.read(row,pos+row.position());
                    if(x<0) throw new EOFException(in.toString());
                }
                row.flip();
                while(row.hasRemaining()) dst.write(row);
            }
        }

        firstLat=lat.getFloat((r0*NX+c0)*4);
        lastLat=lat.getFloat((r1*NX+c0)*4);
        firstLon=lng.getFloat((r0*NX+c0)*4);
        lastLon=lng.getFloat((r0*NX+c1)*4);
        latStep = height>1 ? Math.abs(lat.getFloat(((r0+1)*NX+c0)*4)-firstLat) : latStep;
        lonStep = width>1 ? Math.abs(lng.getFloat((r0*NX+c0+1)*4)-firstLon) : lonStep;
        double northBound=cleanBound(Math.max(firstLat,lastLat)+latStep/2.0);
        double southBound=cleanBound(Math.min(firstLat,lastLat)-latStep/2.0);
        double westBound=cleanBound(Math.min(firstLon,lastLon)-lonStep/2.0);
        double eastBound=cleanBound(Math.max(firstLon,lastLon)+lonStep/2.0);

        Properties p=new Properties();
        p.setProperty("width", Integer.toString(width));
        p.setProperty("height", Integer.toString(height));
        p.setProperty("north", String.format(Locale.ROOT,"%.6f",northBound));
        p.setProperty("lat_span", String.format(Locale.ROOT,"%.6f",northBound-southBound));
        p.setProperty("lat_div", "6");
        p.setProperty("west", String.format(Locale.ROOT,"%.6f",westBound));
        p.setProperty("lon_span", String.format(Locale.ROOT,"%.6f",eastBound-westBound));
        p.setProperty("lon_div", "6");
        Path meta=Paths.get(out.toString()+".grid.txt");
        try(Writer w=Files.newBufferedWriter(meta)) {
            for(String k: new String[]{"width","height","north","lat_span","lat_div","west","lon_span","lon_div"})
                w.write(k+"="+p.getProperty(k)+System.lineSeparator());
        }
        System.out.printf("source index : column=%d..%d row=%d..%d (0-based)%n",c0,c1,r0,r1);
        System.out.printf("grid         : %d x %d%n",width,height);
        System.out.printf(Locale.ROOT,"grid centers : %.3fE..%.3fE / %.3fN..%.3fN%n",firstLon,lastLon,firstLat,lastLat);
        System.out.printf(Locale.ROOT,"grid bounds  : %.3fE..%.3fE / %.3fN..%.3fN%n",westBound,eastBound,southBound,northBound);
        System.out.println("Created      : " + out);
    }

    static double cleanBound(double value) {
        return Math.rint(value*10000.0)/10000.0;
    }

    static void failInvalidBounds(double west,double east,double south,double north,String reason) {
        System.err.println("ERROR: Invalid crop bounds.");
        System.err.println(reason);
        System.err.printf(Locale.ROOT,"Requested longitude: %.3f .. %.3f E%n",west,east);
        System.err.printf(Locale.ROOT,"Requested latitude : %.3f .. %.3f N%n",south,north);
        System.exit(2);
    }

    static void failOutsideGrid(double west,double east,double south,double north,
                                double gridWest,double gridEast,double gridSouth,double gridNorth) {
        System.err.println("ERROR: Requested crop is outside the available grid.");
        System.err.printf(Locale.ROOT,"Requested longitude: %.3f .. %.3f E%n",west,east);
        System.err.printf(Locale.ROOT,"Requested latitude : %.3f .. %.3f N%n",south,north);
        System.err.printf(Locale.ROOT,"Available longitude: %.3f .. %.3f E%n",gridWest,gridEast);
        System.err.printf(Locale.ROOT,"Available latitude : %.3f .. %.3f N%n",gridSouth,gridNorth);
        System.exit(2);
    }
}
LEVEL 4 / 月平均

MISSION 4:1か月平均すると何が見えてくる?

MISSION GOAL2016年8月の日平均を31日分、msm.1km 日本域全体(3001×2521)のままそろえ、日本域全体の月平均日射量マップを作ります。LEVEL 3の小領域クロップはここでは使いません。
STEP 1全領域の日平均を31日分そろえる
STEP 2全領域の月平均を計算
STEP 3日本域全体を見る
1

日本域全体の日平均を31日分そろえる

data/daily/ に、8月1日から31日までの3001×2521の日平均ファイルをそろえます。

実行前にこの配置を確認
~/amaterass_lab/
└── data/
    └── daily/
        ├── 20160801.dailymean.dwn.sw.flx.sfc.msm.1km.bin
        ├── 20160802.dailymean.dwn.sw.flx.sfc.msm.1km.bin
        ├── ...
        ├── 20160820.dailymean.dwn.sw.flx.sfc.msm.1km.bin
        ├── ...
        └── 20160831.dailymean.dwn.sw.flx.sfc.msm.1km.bin
ここまでできればOK
ls data/daily/201608*.dailymean.dwn.sw.flx.sfc.msm.1km.bin | wc -l31 になれば準備完了です。
31日分のファイルがない場合

prepare_month.sh を使うと、8月1日から31日まで1日ずつ「取得 → 日平均」を行います。各日の日平均を作った後、その日の10分値は削除して次の日へ進みます。

必要なスクリプトを取得
cd ~/amaterass_lab
wget -O download_day.sh https://amaterass.science/ja/science-lab/download_day.sh
wget -O make_daily_mean.sh https://amaterass.science/ja/science-lab/make_daily_mean.sh
wget -O prepare_month.sh https://amaterass.science/ja/science-lab/prepare_month.sh
実行前にこの配置を確認
~/amaterass_lab/
├── download_day.sh
├── make_daily_mean.sh
├── prepare_month.sh
└── analysis.msm1km/
    └── dailymean.class
実行
cd ~/amaterass_lab
chmod +x download_day.sh make_daily_mean.sh prepare_month.sh
./prepare_month.sh 2016 08
2

31個の日平均から月平均を作る

2016年8月の31個の日平均について、3001×2521の同じ格子位置どうしを平均します。

日平均 × 31日本域全体
平均同じ格子ごと
BIN
月平均 × 1日本域全体
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O make_monthly_mean.sh https://amaterass.science/ja/science-lab/make_monthly_mean.sh
実行前にこの配置を確認
~/amaterass_lab/
├── make_monthly_mean.sh
├── analysis.msm1km/
│   └── monthlymean.class
└── data/daily/
    ├── 20160801.dailymean.dwn.sw.flx.sfc.msm.1km.bin
    ├── ...
    └── 20160831.dailymean.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
chmod +x make_monthly_mean.sh
./make_monthly_mean.sh 2016 08
read: 20160801...
...
Days used : 31
Created : data/monthly/201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin
ここまでできればOK
最後に Days used : 31 と表示されれば、8月の31日分を使って月平均を作れています。
3

日本域全体の月平均マップを見る

msm.1km 日本域全体を画像化し、1か月平均したときに残る空間的な違いを見ます。

THINK: 日本域全体で、月平均日射量にはどのような地域差が見えるでしょうか。1日だけの地図と比べると、どのような特徴が残り、どのような細かな変化がならされているでしょうか。
実行前にこの配置を確認
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/monthly/
    └── 201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
./draw_map.sh data/monthly/201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin 400 0 4 "W/m²"
LEVEL 4 CLEAR
日本域全体について、日ごとの変化を1か月平均した空間分布を見ることができました。

もう一段深く:このLEVELの中身

うまくいかないとき

「31日分がそろわない」ときは、最初に日平均の個数を数えます。

ls data/daily/201608*.dailymean.dwn.sw.flx.sfc.msm.1km.bin | wc -l

31 でなければ prepare_month.sh をもう一度実行します。正常な日は Already exists: として飛ばし、足りない日だけ処理します。

./prepare_month.sh 2016 08

Javaソースが無いと言われた場合だけ、解析プログラムを展開して再コンパイルします。

tar xzf analysis_msm1km.tar.gz
javac analysis.msm1km/*.java
このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1日平均を各日作る
流れ 2必要日数を確認
流れ 3月平均
prepare_month.sh Shell
何をしている? その月の日数を判定し、日ごとに download_day.sh → make_daily_mean.sh を実行します。正常な日平均が既にあれば Already exists として再利用します。
#!/bin/sh
set -eu
[ "$#" -eq 2 ] || [ "$#" -eq 3 ] || { echo "Usage: $0 YYYY MM [DECOMP_JOBS]" >&2; exit 1; }
YYYY=$1; MM=$2; JOBS=${3:-4}; MONTH="${YYYY}${MM}"; EXPECTED=30262084
case "$JOBS" in ''|*[!0-9]*) echo 'DECOMP_JOBS must be a positive integer.' >&2; exit 1;; esac
SRC=analysis.msm1km/dailymean.java; CLS=analysis.msm1km/dailymean.class
[ -f "$SRC" ] || { echo 'Missing dailymean.java. Extract analysis_msm1km.tar.gz first.' >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
mkdir -p data/daily_raw data/daily
case "$MM" in
  01|03|05|07|08|10|12) LAST=31 ;;
  04|06|09|11) LAST=30 ;;
  02) if [ $((YYYY % 400)) -eq 0 ] || { [ $((YYYY % 4)) -eq 0 ] && [ $((YYYY % 100)) -ne 0 ]; }; then LAST=29; else LAST=28; fi ;;
  *) echo 'MM must be 01-12.' >&2; exit 1 ;;
esac
N=1
while [ "$N" -le "$LAST" ]; do
    DD=$(printf '%02d' "$N"); DAY="${YYYY}${MM}${DD}"; RAW="data/daily_raw/$DAY"; DAILY="data/daily/$DAY.dailymean.dwn.sw.flx.sfc.msm.1km.bin"; COMPLETE=0
    if [ -f "$DAILY" ]; then SIZE=$(wc -c < "$DAILY" | tr -d ' '); if [ "$SIZE" -eq "$EXPECTED" ]; then COMPLETE=1; echo "Already exists: $DAILY"; else echo "Removing incomplete daily mean: $DAILY ($SIZE bytes)"; rm -f "$DAILY"; fi; fi
    if [ "$COMPLETE" -eq 0 ]; then ./download_day.sh "$YYYY" "$MM" "$DD" "$JOBS"; ./make_daily_mean.sh "$YYYY" "$MM" "$DD"; fi
    rm -rf "$RAW"; N=$((N + 1))
done
echo "$LAST full-grid daily-mean files ready in data/daily/."
make_monthly_mean.sh Shell
何をしている? 月平均の入力・出力を決め、必要ならJavaを再コンパイルして monthlymean.java を呼び出します。
#!/bin/sh
set -eu
[ "$#" -eq 2 ] || { echo "Usage: $0 YYYY MM" >&2; exit 1; }
YYYY=$1; MM=$2; MONTH="${YYYY}${MM}"
SRC=analysis.msm1km/monthlymean.java; CLS=analysis.msm1km/monthlymean.class
[ -f "$SRC" ] || { echo 'Missing monthlymean.java. Extract analysis_msm1km.tar.gz first.' >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
mkdir -p data/monthly
java -cp analysis.msm1km monthlymean data/daily "$MONTH" "data/monthly/$MONTH.monthlymean.dwn.sw.flx.sfc.msm.1km.bin"
monthlymean.java Java
何をしている? 指定月に必要な日平均ファイル数(8月なら31)を確認してから、全日の日平均格子をピクセルごとに平均します。
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.stream.*;

public class monthlymean {
    public static void main(String[] args) throws Exception {
        if(args.length!=3){System.err.println("Usage: monthlymean input_dir YYYYMM output.bin");System.exit(1);}
        Path dir=Paths.get(args[0]); String month=args[1]; Path out=Paths.get(args[2]);
        YearMonth ym;
        try {
            ym=YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyyMM"));
        } catch(DateTimeException e) {
            throw new IOException("YYYYMM must be a valid month: "+month, e);
        }
        String suffix=".dailymean.dwn.sw.flx.sfc.msm.1km.bin";
        List<Path> files;
        try(Stream<Path>s=Files.list(dir)){
            files=s.filter(p->p.getFileName().toString().startsWith(month))
                   .filter(p->p.getFileName().toString().endsWith(suffix))
                   .sorted().collect(Collectors.toList());
        }
        int expected=ym.lengthOfMonth();
        if(files.size()!=expected) throw new IOException("Expected "+expected+" daily files, found "+files.size());
        long nLong=FloatGrid.floatCount(files.get(0)); if(nLong>Integer.MAX_VALUE)throw new IOException("Grid too large"); int n=(int)nLong;
        float[] sum=new float[n];
        for(Path p:files){
            if(FloatGrid.floatCount(p)!=nLong)throw new IOException("Size mismatch: "+p);
            if(!FloatGrid.sameGridMeta(files.get(0),p))throw new IOException("Grid metadata mismatch: "+p);
            MappedByteBuffer b=FloatGrid.map(p);
            for(int i=0;i<n;i++)sum[i]+=b.getFloat(i*4);
            System.out.println("read: "+p.getFileName());
        }
        for(int i=0;i<n;i++)sum[i]/=files.size();
        FloatGrid.writeFloats(out,sum); FloatGrid.copyGridMeta(files.get(0),out);
        System.out.println("Days used : "+files.size());
        System.out.println("Created   : "+out);
    }
}
LEVEL 5 / 偏差

MISSION 5:8月20日は、日本域全体で「2016年8月の平均」とどう違った?

MISSION GOAL2016年8月20日の日本域全体の日平均から、LEVEL 4で作った日本域全体の8月月平均を引きます。3001×2521の各格子点で「2016年8月の平均より多かったか、少なかったか」を地図で調べます。
STEP 1平均との差を考える
STEP 2全領域で偏差を計算
STEP 3日本域全体を見る
1

日平均 − 月平均を計算する

日平均と月平均はどちらも同じ3001×2521のmsm.1km 日本域格子なので、同じ位置の値を1つずつ引き算できます。

8/20 日平均日本域全体
8月 月平均日本域全体
±
偏差平均との差
正の値なら2016年8月平均より日射量が多く、負の値なら少ないことを表します。
偏差(anomaly): ここでは2016年8月20日の日平均と、同じ2016年8月の月平均との差を調べます。
2

日本域全体の偏差ファイルを作る

8月20日の日平均と8月の月平均について、3001×2521の同じ格子位置どうしを引き算します。

MISSION予想: 8月20日は、日本域のどの地域で2016年8月平均より日射量が多く、どの地域で少なかったでしょうか。
1. 作業場所で必要なファイルを取得
cd ~/amaterass_lab
wget -O make_anomaly.sh https://amaterass.science/ja/science-lab/make_anomaly.sh
実行前にこの配置を確認
~/amaterass_lab/
├── make_anomaly.sh
├── analysis.msm1km/
│   └── anomaly.class
├── data/daily/
│   └── 20160820.dailymean.dwn.sw.flx.sfc.msm.1km.bin
└── data/monthly/
    └── 201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
chmod +x make_anomaly.sh
./make_anomaly.sh 2016 08 20
ここまでできればOK
data/anomaly/20160820.anomaly.dwn.sw.flx.sfc.msm.1km.bin ができれば計算完了です。
3

日本域全体の偏差を地図で見る

偏差なので0を中心に、負から正まで同じ幅で表示します。

負の偏差8月平均より少ない
08月平均と同程度
正の偏差8月平均より多い
地図を開いたら、0 W/m²がカラースケールの中央にあることを確認してから読み取ります。
最後の問い: 8月20日は、日本域全体のどこで2016年8月の平均より日射量が多く、どこで少なかったでしょうか。偏差の分布にはどのような広がりが見えるでしょうか。
実行前にこの配置を確認
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/anomaly/
    └── 20160820.anomaly.dwn.sw.flx.sfc.msm.1km.bin
2. 実行
cd ~/amaterass_lab
./draw_map.sh data/anomaly/20160820.anomaly.dwn.sw.flx.sfc.msm.1km.bin 200 -200 8 "W/m²"
LEVEL 5 CLEAR
日本域全体について、8月20日の日平均と2016年8月平均の差を地図で確認できました。

もう一段深く:このLEVELの中身

うまくいかないとき

偏差が作れない場合は、入力となる「その日の日平均」と「その月の月平均」の両方を確認します。

ls -lh data/daily/20160820.dailymean.dwn.sw.flx.sfc.msm.1km.bin
ls -lh data/monthly/201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin
このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1日平均を読む
流れ 2月平均を読む
流れ 3差を計算
make_anomaly.sh Shell
何をしている? 指定日の日平均と同じ月の月平均を anomaly.java に渡し、偏差ファイルの保存先を作ります。
#!/bin/sh
set -eu
[ "$#" -eq 3 ] || { echo "Usage: $0 YYYY MM DD" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; DAY="${YYYY}${MM}${DD}"; MONTH="${YYYY}${MM}"
SRC=analysis.msm1km/anomaly.java; CLS=analysis.msm1km/anomaly.class
[ -f "$SRC" ] || { echo 'Missing anomaly.java. Extract analysis_msm1km.tar.gz first.' >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
DAILY="data/daily/$DAY.dailymean.dwn.sw.flx.sfc.msm.1km.bin"; MONTHLY="data/monthly/$MONTH.monthlymean.dwn.sw.flx.sfc.msm.1km.bin"; OUT="data/anomaly/$DAY.anomaly.dwn.sw.flx.sfc.msm.1km.bin"
[ -f "$DAILY" ] || { echo "Missing: $DAILY" >&2; exit 1; }; [ -f "$MONTHLY" ] || { echo "Missing: $MONTHLY" >&2; exit 1; }; mkdir -p data/anomaly; java -cp analysis.msm1km anomaly "$DAILY" "$MONTHLY" "$OUT"
anomaly.java Java
何をしている? 同じ格子位置について「日平均 − 月平均」を計算します。正なら月平均より多く、負なら少なかったことを表します。
import java.io.*;
import java.nio.*;
import java.nio.file.*;

public class anomaly {
    public static void main(String[] args) throws Exception {
        if(args.length!=3){System.err.println("Usage: anomaly daily.bin monthly.bin output.bin");System.exit(1);}
        Path daily=Paths.get(args[0]), monthly=Paths.get(args[1]), out=Paths.get(args[2]);
        FloatGrid.requireSameSize(daily,monthly);
        if(!FloatGrid.sameGridMeta(daily,monthly))throw new IOException("Grid metadata mismatch");
        long nLong=FloatGrid.floatCount(daily); if(nLong>Integer.MAX_VALUE)throw new IOException("Grid too large"); int n=(int)nLong;
        MappedByteBuffer a=FloatGrid.map(daily), b=FloatGrid.map(monthly); float[] d=new float[n];
        for(int i=0;i<n;i++) d[i]=a.getFloat(i*4)-b.getFloat(i*4);
        FloatGrid.writeFloats(out,d); FloatGrid.copyGridMeta(daily,out);
        System.out.println("Created: "+out);
    }
}
LEVEL 6 / POINT TIME SERIES

MISSION 6:緯度・経度を指定して、1地点の1日を追ってみよう

MISSION GOALこれまで地図として見てきた3001×2521格子から、緯度・経度で1地点を選び、その地点の日射量を1日分の時系列として取り出します。横軸の時刻はファイル名ではなく、同じピクセルのTIMEデータから求めた実観測時刻を使います。
STEP 1時系列用データを取得
STEP 21地点を選ぶ
STEP 3TIMEから観測時刻を読む
STEP 4Gnuplotで描く
1

1日分の日射量とTIMEを取得する

2016年8月20日(JST)に対応する10分値を取得します。日射量ファイルが存在する時刻について、対応するTIMEファイルも同時にそろえます。

必要なスクリプトを取得
cd ~/amaterass_lab
wget -O download_point_day.sh https://amaterass.science/ja/science-lab/download_point_day.sh
wget -O pickup_day.sh https://amaterass.science/ja/science-lab/pickup_day.sh
wget -O plot_solar_day.sh https://amaterass.science/ja/science-lab/plot_solar_day.sh
wget -O analysis_msm1km.tar.gz https://amaterass.science/ja/science-lab/analysis_msm1km.tar.gz
tar xzf analysis_msm1km.tar.gz
chmod +x download_point_day.sh pickup_day.sh plot_solar_day.sh
実行
./download_point_day.sh 2016 08 20
CHECK
data/timeseries/20160820/ に日射量とTIMEの10分値がそろえば準備完了です。
2

緯度・経度から最寄りの格子点を選ぶ

例として35.0°N、138.0°Eを指定します。LAT/LNG格子を調べ、指定地点に最も近い1ピクセルを選びます。

実行
cd ~/amaterass_lab
./pickup_day.sh 2016 08 20 35.0 138.0
selected grid : ... N, ... E (row=... column=...)
samples : ...
Created : data/timeseries/20160820/solar.dat
TIMEを使う理由: ひまわりは画像全体を完全に同時刻に観測するわけではありません。solar.dat の時刻は、各10分値のファイル名ではなく、選んだピクセルのTIME値から計算したJSTの実観測時刻です。
3

Gnuplotで1日の変化を見る

地図ではなく、横軸を実観測時刻、縦軸を日射量[W/m²]にして時系列を描きます。LEVEL 7と比較しやすいよう、日射量の縦軸は0〜1400 W/m²に固定します。日射量の線はオレンジで統一します。

実行
cd ~/amaterass_lab
./plot_solar_day.sh 2016 08 20
LEVEL 6 CLEAR
data/timeseries/20160820/solar_timeseries.png ができれば成功です。日射量が1日の中でどのように増減するか、数値の時間変化として確認できます。

もう一段深く:このLEVELの中身

うまくいかないとき

時系列が作れない場合は、日射量とTIMEが同じ時刻でそろっているか、そして解析ソースがあるかを確認します。

ls data/timeseries/20160820/*dwn.sw.flx.sfc* | head
ls data/timeseries/20160820/*grd.time.mjd.hms* | head
ls analysis.msm1km/pointseries.java

pickup_day.shpointseries.java が無い古い作業環境なら解析プログラムを自動更新します。

このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1緯度経度→最寄りpixel
流れ 2日射量+TIMEを読む
流れ 3Gnuplot時系列
download_point_day.sh Shell
何をしている? JSTの1日をUTCへ変換し、存在する日射量と対応TIMEを取得します。地点時系列ではTIMEも同じピクセルから読むため、両方をそろえます。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then
        echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then
        echo wget
    else
        echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2
        exit 1
    fi
}

[ "$#" -eq 3 ] || [ "$#" -eq 4 ] || { echo "Usage: $0 YYYY MM DD [DECOMP_JOBS]" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; JOBS=${4:-4}
case "$JOBS" in ''|*[!0-9]*) echo 'DECOMP_JOBS must be a positive integer.' >&2; exit 1;; esac
[ "$JOBS" -ge 1 ] || { echo 'DECOMP_JOBS must be at least 1.' >&2; exit 1; }
DAY="${YYYY}${MM}${DD}"
CHECK=$(date -u -d "${YYYY}-${MM}-${DD}" +%Y%m%d 2>/dev/null || true)
[ "$CHECK" = "$DAY" ] || { echo 'Invalid calendar date.' >&2; exit 1; }
EXPECTED=30262084
WGET=$(select_wget)
BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}
DEST="data/timeseries/$DAY"
mkdir -p "$DEST"

file_is_complete() {
    FILE=$1
    [ -f "$FILE" ] || return 1
    SIZE=$(wc -c < "$FILE" | tr -d ' ')
    [ "$SIZE" -eq "$EXPECTED" ]
}

QUEUE=
RUNNING=0
cleanup_queue() {
    for JOB in $QUEUE; do
        PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}
        kill "$PID" 2>/dev/null || true
        wait "$PID" 2>/dev/null || true
        rm -f "$DEST/$NAME" "$DEST/$BZ2"
    done
    QUEUE=; RUNNING=0
}
trap 'cleanup_queue' 0
trap 'cleanup_queue; exit 1' 1 2 15

finish_oldest() {
    [ "$RUNNING" -gt 0 ] || return 0
    set -- $QUEUE
    JOB=$1; shift; QUEUE="$*"
    PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}
    if ! wait "$PID"; then
        rm -f "$DEST/$NAME" "$DEST/$BZ2"
        echo "Decompression failed: $BZ2" >&2
        exit 1
    fi
    if ! file_is_complete "$DEST/$NAME"; then
        SIZE=$(wc -c < "$DEST/$NAME" 2>/dev/null | tr -d ' ' || echo 0)
        rm -f "$DEST/$NAME" "$DEST/$BZ2"
        echo "Unexpected file size: $DEST/$NAME ($SIZE bytes)" >&2
        exit 1
    fi
    rm -f "$DEST/$BZ2"
    echo "Ready: $NAME"
    RUNNING=$((RUNNING - 1))
}

start_decompress() {
    NAME=$1; BZ2=$2
    rm -f "$DEST/$NAME"
    (cd "$DEST" && bzip2 -d "$BZ2") &
    PID=$!
    QUEUE="${QUEUE}${QUEUE:+ }${PID}|${NAME}|${BZ2}"
    RUNNING=$((RUNNING + 1))
    if [ "$RUNNING" -ge "$JOBS" ]; then finish_oldest; fi
}

fetch_file() {
    NAME=$1; URL=$2
    BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then
        rm -f "$DEST/$BZ2"
        return 0
    fi
    if [ -e "$DEST/$NAME" ]; then
        SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' ')
        echo "Removing incomplete file: $NAME ($SIZE bytes)"
        rm -f "$DEST/$NAME"
    fi
    if (cd "$DEST" && "$WGET" -q -c "$URL"); then
        start_decompress "$NAME" "$BZ2"
        return 0
    fi
    rm -f "$DEST/$BZ2"
    return 1
}

START=$(date -u -d "${YYYY}-${MM}-${DD} 00:00 +0900" +%s)
END=$(date -u -d "${YYYY}-${MM}-${DD} 23:50 +0900" +%s)
T=$START
FOUND=0
while [ "$T" -le "$END" ]; do
    STAMP=$(date -u -d "@$T" +%Y%m%d%H%M)
    YYYYMM=$(printf '%s' "$STAMP" | cut -c1-6)
    YYYYMMDD=$(printf '%s' "$STAMP" | cut -c1-8)
    DIR="$BASE/$YYYYMM/$YYYYMMDD"
    SOLAR="$STAMP.dwn.sw.flx.sfc.msm.1km.bin"
    TIME="$STAMP.grd.time.mjd.hms.msm.1km.bin"
    if file_is_complete "$DEST/$SOLAR" || fetch_file "$SOLAR" "$DIR/$SOLAR.bz2"; then
        FOUND=$((FOUND + 1))
        file_is_complete "$DEST/$TIME" || fetch_file "$TIME" "$DIR/$TIME.bz2" || { echo "Missing TIME file for $STAMP" >&2; exit 1; }
    fi
    T=$((T + 600))
done
while [ "$RUNNING" -gt 0 ]; do finish_oldest; done
trap - 0 1 2 15
COUNT=$(find "$DEST" -maxdepth 1 -type f -name '*.dwn.sw.flx.sfc.msm.1km.bin' -size 30262084c | wc -l | tr -d ' ')
[ "$COUNT" -gt 1 ] || { echo "Not enough solar-radiation files for $DAY." >&2; exit 1; }
echo "Time-series source files ready: $DAY ($COUNT solar files, max $JOBS decompression jobs)"
pickup_day.sh Shell
何をしている? LAT/LNGと日射量・TIMEを pointseries.java に渡して、指定地点の solar.dat を作ります。Javaソースが古い作業環境では解析プログラムを取り直す処理も含みます。
#!/bin/sh
set -eu
[ "$#" -eq 5 ] || { echo "Usage: $0 YYYY MM DD LAT LON" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; LAT=$4; LON=$5; DAY="${YYYY}${MM}${DD}"
DIR="data/timeseries/$DAY"; LATF=data/reference/standard_2521x3001.lat.msm.1km.bin; LNGF=data/reference/standard_2521x3001.lng.msm.1km.bin
[ -d "$DIR" ] || { echo "Missing: $DIR" >&2; exit 1; }
[ -f "$LATF" ] && [ -f "$LNGF" ] || { echo 'Missing LAT/LNG reference files. Run ./download_reference.sh first.' >&2; exit 1; }
SRC=analysis.msm1km/pointseries.java; CLS=analysis.msm1km/pointseries.class
if [ ! -f "$SRC" ]; then
    echo "Refreshing analysis programs..."
    wget -q -O analysis_msm1km.tar.gz https://amaterass.science/ja/science-lab/analysis_msm1km.tar.gz
    tar xzf analysis_msm1km.tar.gz
fi
[ -f "$SRC" ] || { echo "Could not prepare $SRC" >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
java -cp analysis.msm1km pointseries "$DIR" "$LATF" "$LNGF" "$DAY" "$LAT" "$LON" dwn.sw.flx.sfc.msm.1km.bin "$DIR/solar.dat"
plot_solar_day.sh Shell
何をしている? solar.datをGnuplotへ渡し、実観測時刻(JST)を横軸、日射量を縦軸にして描きます。縦軸は既定0–1400 W/m²、日射量はオレンジです。
#!/bin/sh
set -eu
[ "$#" -ge 3 ] && [ "$#" -le 4 ] || { echo "Usage: $0 YYYY MM DD [SOLAR_MAX]" >&2; exit 1; }
SOLAR_MAX="${4:-1400}"
DAY="${1}${2}${3}"; DIR="data/timeseries/$DAY"; DAT="$DIR/solar.dat"; OUT="$DIR/solar_timeseries.png"
[ -f "$DAT" ] || { echo "Missing: $DAT" >&2; exit 1; }
command -v gnuplot >/dev/null 2>&1 || { echo 'Gnuplot is required.' >&2; exit 1; }
gnuplot <<EOF_GP
set terminal pngcairo size 1200,650 enhanced font "sans,16"
set output "$OUT"
set xdata time
set timefmt "%Y-%m-%dT%H:%M:%S"
set format x "%H:%M"
set xlabel "Time (JST)"
set ylabel "Downward shortwave flux (W/m^2)"
set grid
set key off
set yrange [0:$SOLAR_MAX]
plot "$DAT" using 1:2 with lines linewidth 2 linecolor rgb "#E69F00"
EOF_GP
echo "Created: $OUT"
pointseries.java Java
何をしている? LAT/LNGから最寄り格子点を一度決め、各時刻について同じindexの物理量とTIMEを読みます。TIMEをUTC→JSTへ変換し、指定したJSTの日だけを時系列として出力します。
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.time.*;
import java.time.format.*;
import java.util.*;
import java.util.regex.*;
import java.util.stream.*;

public class pointseries {
    static final int NX = 3001;
    static final int NY = 2521;
    static final String TIME_SUFFIX = "grd.time.mjd.hms.msm.1km.bin";

    public static void main(String[] args) throws Exception {
        if (args.length != 8) {
            System.err.println("Usage: pointseries input_dir lat.bin lng.bin YYYYMMDD target_lat target_lon data_suffix output.dat");
            System.exit(1);
        }
        Path dir=Paths.get(args[0]), latf=Paths.get(args[1]), lngf=Paths.get(args[2]);
        LocalDate targetDate=LocalDate.parse(args[3],DateTimeFormatter.BASIC_ISO_DATE);
        double targetLat=Double.parseDouble(args[4]), targetLon=Double.parseDouble(args[5]);
        String suffix=args[6]; Path out=Paths.get(args[7]);
        if(!Files.isDirectory(dir))throw new IOException("Missing directory: "+dir);
        FloatGrid.requireSameSize(latf,lngf);
        if(FloatGrid.floatCount(latf)!=(long)NX*NY)throw new IOException("Expected 2521x3001 msm.1km coordinate grid");

        MappedByteBuffer lat=FloatGrid.map(latf), lng=FloatGrid.map(lngf);
        double best=Double.POSITIVE_INFINITY; int bestIndex=-1; float bestLat=Float.NaN,bestLon=Float.NaN;
        for(int i=0;i<NX*NY;i++){
            float la=lat.getFloat(i*4), lo=lng.getFloat(i*4);
            if(!Float.isFinite(la)||!Float.isFinite(lo))continue;
            double dlat=la-targetLat;
            double dlon=(lo-targetLon)*Math.cos(Math.toRadians(targetLat));
            double d2=dlat*dlat+dlon*dlon;
            if(d2<best){best=d2;bestIndex=i;bestLat=la;bestLon=lo;}
        }
        if(bestIndex<0)throw new IOException("No valid coordinate found");

        Pattern pattern=Pattern.compile("\\d{12}\\."+Pattern.quote(suffix));
        List<Path> files;
        try(Stream<Path>s=Files.list(dir)){
            files=s.filter(p->pattern.matcher(p.getFileName().toString()).matches()).sorted().collect(Collectors.toList());
        }
        if(files.isEmpty())throw new IOException("No files for "+suffix+" in "+dir);
        Files.createDirectories(out.toAbsolutePath().getParent());
        ZoneId jst=ZoneId.of("Asia/Tokyo");
        DateTimeFormatter outFmt=DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
        int count=0;
        try(PrintWriter w=new PrintWriter(Files.newBufferedWriter(out))){
            int row=bestIndex/NX, col=bestIndex%NX;
            w.printf(Locale.ROOT,"# target_lat=%.6f target_lon=%.6f%n",targetLat,targetLon);
            w.printf(Locale.ROOT,"# grid_lat=%.6f grid_lon=%.6f row=%d column=%d%n",bestLat,bestLon,row,col);
            w.println("# time_JST value");
            for(Path data:files){
                String name=data.getFileName().toString();
                String stamp=name.substring(0,12);
                Path time=dir.resolve(stamp+"."+TIME_SUFFIX);
                if(!Files.isRegularFile(time))continue;
                FloatGrid.requireSameSize(data,time,latf);
                float value=FloatGrid.readAt(data,bestIndex);
                float days=FloatGrid.readAt(time,bestIndex);
                if(!Float.isFinite(value)||!Float.isFinite(days))continue;
                LocalDate utcDate=LocalDate.parse(stamp.substring(0,8),DateTimeFormatter.BASIC_ISO_DATE);
                long millis=Math.round(days*86400.0*1000.0);
                Instant instant=utcDate.atStartOfDay(ZoneOffset.UTC).toInstant().plusMillis(millis);
                ZonedDateTime local=instant.atZone(jst);
                if(!local.toLocalDate().equals(targetDate))continue;
                w.printf(Locale.ROOT,"%s %.6f%n",outFmt.format(local),value);
                count++;
            }
        }
        if(count<2){Files.deleteIfExists(out);throw new IOException("Not enough samples on "+targetDate+" ("+count+")");}
        System.out.printf(Locale.ROOT,"selected grid : %.6f N, %.6f E (row=%d column=%d)%n",bestLat,bestLon,bestIndex/NX,bestIndex%NX);
        System.out.println("samples       : "+count);
        System.out.println("Created       : "+out);
    }
}
LEVEL 7 / SOLAR TO PV

MISSION 7:日射量は太陽光発電出力にどうつながる?

MISSION GOALLEVEL 6と同じ地点・同じ日のAMATERASS推定PV出力を取り出し、日射量[W/m²]とPV出力[kW/kWp]を時系列で比較します。最後に両方を時間積分し、1日に届いた日射エネルギーと1 kWpあたりの推定発電量を求めます。
STEP 1PV出力を取得
STEP 22つの時系列を比較
STEP 31日分を積算
1

AMATERASSの推定PV出力を取得する

unit.pvp.tc028.ac945.sfc を使います。これは1 kWpの太陽光発電設備について、温度係数−0.28 %/°C、インバータ効率94.5%として推定した出力[kW/kWp]です。

必要なスクリプトを取得して実行
cd ~/amaterass_lab
wget -O download_pv_day.sh https://amaterass.science/ja/science-lab/download_pv_day.sh
wget -O pickup_pv_day.sh https://amaterass.science/ja/science-lab/pickup_pv_day.sh
wget -O plot_solar_pv.sh https://amaterass.science/ja/science-lab/plot_solar_pv.sh
wget -O daily_energy.sh https://amaterass.science/ja/science-lab/daily_energy.sh
chmod +x download_pv_day.sh pickup_pv_day.sh plot_solar_pv.sh daily_energy.sh
./download_pv_day.sh 2016 08 20
./pickup_pv_day.sh 2016 08 20 35.0 138.0
読み方: unit.pvp が0.8なら、その時刻・格子で1 kWpの設備から約0.8 kW出力すると読みます。
2

日射量とPV出力をGnuplotで比べる

単位が異なるので、上下2段のグラフに分けて同じ時刻軸で比較します。両方の時刻はTIMEデータから得た実観測時刻です。日射量はLEVEL 6と同じ0〜1400 W/m²、PV出力は0〜1.2 kW/kWpに固定して、グラフごとの自動スケールによる見え方の違いをなくします。日射量はオレンジ、PV出力は青で表示します。

実行
cd ~/amaterass_lab
./plot_solar_pv.sh 2016 08 20
CHECK
data/timeseries/20160820/solar_pv_timeseries.png を開き、日射量の増減と推定PV出力の増減がどのように対応しているか見てみましょう。
3

1日分のエネルギーにしてみる

実観測時刻に沿って時系列を積分し、瞬間値を1日分の量へ変換します。

実行
cd ~/amaterass_lab
./daily_energy.sh 2016 08 20
Solar energy : ... kWh/m^2
PV energy : ... kWh/kWp
Samples : solar=... pv=...
考えてみよう: 日射エネルギー[kWh/m²]とPV発電量[kWh/kWp]は同じ単位でも同じ物理量でもありません。日射がPV出力へ変換されるとき、時系列の形や1日積算値はどのように変わったでしょうか。
ALL LEVELS CLEAR
衛星データを地図として見るだけでなく、1地点の物理量を時系列として取り出し、日射から太陽光発電までを数値で追うことができました。

もう一段深く:このLEVELの中身

うまくいかないとき

PV比較が作れない場合は、まず solar.datpv.dat を確認します。

ls -lh data/timeseries/20160820/solar.dat
ls -lh data/timeseries/20160820/pv.dat

PVファイルが無ければ、LEVEL 6の同じ時刻に対応するPVをもう一度取得して抽出します。

./download_pv_day.sh 2016 08 20
./pickup_pv_day.sh 2016 08 20 35.0 138.0
このLEVELのプログラムを読んでみる

実際にこのページからダウンロードして実行しているソースです。最初からすべてを理解する必要はありません。「入力をどこで受け取り、どこで物理量を処理し、何を出力しているか」を追ってみましょう。

流れ 1同地点のPVを取得
流れ 2日射とPVを比較
流れ 3時間積分してkWh
download_pv_day.sh Shell
何をしている? LEVEL 6で取得済みの日射量時刻を基準に、同時刻の unit.pvp.tc028.ac945.sfc を取得します。通信1本・解凍最大4ジョブです。
#!/bin/sh
set -eu

select_wget() {
    if command -v wget1 >/dev/null 2>&1; then echo wget1
    elif command -v wget >/dev/null 2>&1 && ! wget --version 2>&1 | grep -q 'Wget2'; then echo wget
    else echo 'Error: GNU Wget 1.x (wget or wget1) is required.' >&2; exit 1
    fi
}
[ "$#" -eq 3 ] || [ "$#" -eq 4 ] || { echo "Usage: $0 YYYY MM DD [DECOMP_JOBS]" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; JOBS=${4:-4}; DAY="${YYYY}${MM}${DD}"
case "$JOBS" in ''|*[!0-9]*) echo 'DECOMP_JOBS must be a positive integer.' >&2; exit 1;; esac
[ "$JOBS" -ge 1 ] || { echo 'DECOMP_JOBS must be at least 1.' >&2; exit 1; }
EXPECTED=30262084; WGET=$(select_wget)
BASE=${AMATERASS_JP_BASE:-ftp://amaterass.cr.chiba-u.ac.jp/quasi-realtime/himawari829/archived/JP}
DEST="data/timeseries/$DAY"
[ -d "$DEST" ] || { echo "Missing: $DEST (run download_point_day.sh first)" >&2; exit 1; }
file_is_complete(){ FILE=$1; [ -f "$FILE" ] || return 1; SIZE=$(wc -c < "$FILE" | tr -d ' '); [ "$SIZE" -eq "$EXPECTED" ]; }
QUEUE=; RUNNING=0
cleanup_queue(){ for JOB in $QUEUE; do PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}; kill "$PID" 2>/dev/null || true; wait "$PID" 2>/dev/null || true; rm -f "$DEST/$NAME" "$DEST/$BZ2"; done; QUEUE=; RUNNING=0; }
trap 'cleanup_queue' 0
trap 'cleanup_queue; exit 1' 1 2 15
finish_oldest(){
    [ "$RUNNING" -gt 0 ] || return 0
    set -- $QUEUE; JOB=$1; shift; QUEUE="$*"
    PID=${JOB%%|*}; REST=${JOB#*|}; NAME=${REST%%|*}; BZ2=${REST#*|}
    if ! wait "$PID"; then rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Decompression failed: $BZ2" >&2; exit 1; fi
    if ! file_is_complete "$DEST/$NAME"; then SIZE=$(wc -c < "$DEST/$NAME" 2>/dev/null | tr -d ' ' || echo 0); rm -f "$DEST/$NAME" "$DEST/$BZ2"; echo "Unexpected file size: $DEST/$NAME ($SIZE bytes)" >&2; exit 1; fi
    rm -f "$DEST/$BZ2"; echo "Ready: $NAME"; RUNNING=$((RUNNING-1))
}
start_decompress(){ NAME=$1; BZ2=$2; rm -f "$DEST/$NAME"; (cd "$DEST" && bzip2 -d "$BZ2") & PID=$!; QUEUE="${QUEUE}${QUEUE:+ }${PID}|${NAME}|${BZ2}"; RUNNING=$((RUNNING+1)); [ "$RUNNING" -lt "$JOBS" ] || finish_oldest; }
COUNT=0
for SOLAR in "$DEST"/*.dwn.sw.flx.sfc.msm.1km.bin; do
    [ -f "$SOLAR" ] || continue
    STAMP=$(basename "$SOLAR" | cut -c1-12); YYYYMM=$(printf '%s' "$STAMP" | cut -c1-6); YYYYMMDD=$(printf '%s' "$STAMP" | cut -c1-8)
    NAME="$STAMP.unit.pvp.tc028.ac945.sfc.msm.1km.bin"; BZ2="$NAME.bz2"
    if file_is_complete "$DEST/$NAME"; then rm -f "$DEST/$BZ2"; COUNT=$((COUNT+1)); continue; fi
    if [ -e "$DEST/$NAME" ]; then SIZE=$(wc -c < "$DEST/$NAME" | tr -d ' '); echo "Removing incomplete file: $NAME ($SIZE bytes)"; rm -f "$DEST/$NAME"; fi
    URL="$BASE/$YYYYMM/$YYYYMMDD/$BZ2"
    if (cd "$DEST" && "$WGET" -q -c "$URL"); then start_decompress "$NAME" "$BZ2"; COUNT=$((COUNT+1)); else rm -f "$DEST/$BZ2"; fi
done
while [ "$RUNNING" -gt 0 ]; do finish_oldest; done
trap - 0 1 2 15
[ "$COUNT" -gt 1 ] || { echo "Not enough PV files for $DAY." >&2; exit 1; }
echo "PV files ready: $DAY ($COUNT files, max $JOBS decompression jobs)"
pickup_pv_day.sh Shell
何をしている? 同じ緯度経度について日射量とPV出力を pointseries.java で抽出し、solar.datとpv.datをそろえます。
#!/bin/sh
set -eu
[ "$#" -eq 5 ] || { echo "Usage: $0 YYYY MM DD LAT LON" >&2; exit 1; }
YYYY=$1; MM=$2; DD=$3; LAT=$4; LON=$5; DAY="${YYYY}${MM}${DD}"
DIR="data/timeseries/$DAY"; LATF=data/reference/standard_2521x3001.lat.msm.1km.bin; LNGF=data/reference/standard_2521x3001.lng.msm.1km.bin
[ -d "$DIR" ] || { echo "Missing: $DIR" >&2; exit 1; }
[ -f "$LATF" ] && [ -f "$LNGF" ] || { echo 'Missing LAT/LNG reference files. Run ./download_reference.sh first.' >&2; exit 1; }
SRC=analysis.msm1km/pointseries.java; CLS=analysis.msm1km/pointseries.class
if [ ! -f "$SRC" ]; then
    echo "Refreshing analysis programs..."
    wget -q -O analysis_msm1km.tar.gz https://amaterass.science/ja/science-lab/analysis_msm1km.tar.gz
    tar xzf analysis_msm1km.tar.gz
fi
[ -f "$SRC" ] || { echo "Could not prepare $SRC" >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ] || [ analysis.msm1km/FloatGrid.java -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
java -cp analysis.msm1km pointseries "$DIR" "$LATF" "$LNGF" "$DAY" "$LAT" "$LON" dwn.sw.flx.sfc.msm.1km.bin "$DIR/solar.dat"
java -cp analysis.msm1km pointseries "$DIR" "$LATF" "$LNGF" "$DAY" "$LAT" "$LON" unit.pvp.tc028.ac945.sfc.msm.1km.bin "$DIR/pv.dat"
plot_solar_pv.sh Shell
何をしている? Gnuplotの上下2段に日射量(オレンジ、0–1400 W/m²)とPV出力(青、0–1.2 kW/kWp)を同じ時刻軸で描きます。
#!/bin/sh
set -eu
[ "$#" -ge 3 ] && [ "$#" -le 5 ] || { echo "Usage: $0 YYYY MM DD [SOLAR_MAX] [PV_MAX]" >&2; exit 1; }
SOLAR_MAX="${4:-1400}"
PV_MAX="${5:-1.2}"
DAY="${1}${2}${3}"; DIR="data/timeseries/$DAY"; SOLAR="$DIR/solar.dat"; PV="$DIR/pv.dat"; OUT="$DIR/solar_pv_timeseries.png"
[ -f "$SOLAR" ] && [ -f "$PV" ] || { echo 'Missing solar.dat or pv.dat.' >&2; exit 1; }
command -v gnuplot >/dev/null 2>&1 || { echo 'Gnuplot is required.' >&2; exit 1; }
gnuplot <<EOF_GP
set terminal pngcairo size 1200,900 enhanced font "sans,16"
set output "$OUT"
set xdata time
set timefmt "%Y-%m-%dT%H:%M:%S"
set format x "%H:%M"
set grid
set multiplot layout 2,1 title "Solar radiation and estimated PV output"
set ylabel "Solar radiation (W/m^2)"
set xlabel ""
set yrange [0:$SOLAR_MAX]
set key off
plot "$SOLAR" using 1:2 with lines linewidth 2 linecolor rgb "#E69F00"
set ylabel "PV output (kW/kWp)"
set xlabel "Time (JST)"
set yrange [0:$PV_MAX]
plot "$PV" using 1:2 with lines linewidth 2 linecolor rgb "#0072B2"
unset multiplot
EOF_GP
echo "Created: $OUT"
daily_energy.sh Shell
何をしている? solar.datとpv.datを dailyenergy.java に渡し、日積算値を計算して画面表示とdaily_energy.txt保存を同時に行います。
#!/bin/sh
set -eu
[ "$#" -eq 3 ] || { echo "Usage: $0 YYYY MM DD" >&2; exit 1; }
DAY="${1}${2}${3}"; DIR="data/timeseries/$DAY"; SOLAR="$DIR/solar.dat"; PV="$DIR/pv.dat"; OUT="$DIR/daily_energy.txt"
[ -f "$SOLAR" ] && [ -f "$PV" ] || { echo 'Missing solar.dat or pv.dat.' >&2; exit 1; }
SRC=analysis.msm1km/dailyenergy.java; CLS=analysis.msm1km/dailyenergy.class
if [ ! -f "$SRC" ]; then
    echo "Refreshing analysis programs..."
    wget -q -O analysis_msm1km.tar.gz https://amaterass.science/ja/science-lab/analysis_msm1km.tar.gz
    tar xzf analysis_msm1km.tar.gz
fi
[ -f "$SRC" ] || { echo "Could not prepare $SRC" >&2; exit 1; }
if [ ! -f "$CLS" ] || [ "$SRC" -nt "$CLS" ]; then javac analysis.msm1km/*.java; fi
java -cp analysis.msm1km dailyenergy "$SOLAR" "$PV" | tee "$OUT"
echo "Saved: $OUT"
dailyenergy.java Java
何をしている? 時系列の隣り合う時刻間を台形積分し、W/m²→kWh/m²、kW/kWp→kWh/kWpへ変換します。単なる画像ではなく、時間方向に物理量を積分している部分です。
import java.io.*;
import java.nio.file.*;
import java.time.*;
import java.time.format.*;
import java.util.*;

public class dailyenergy {
    static class Sample { long t; double v; Sample(long t,double v){this.t=t;this.v=v;} }
    static List<Sample> read(Path p) throws Exception {
        List<Sample>a=new ArrayList<>(); ZoneId jst=ZoneId.of("Asia/Tokyo");
        for(String line:Files.readAllLines(p)){
            line=line.trim(); if(line.isEmpty()||line.startsWith("#"))continue;
            String[]x=line.split("\\s+"); if(x.length<2)continue;
            LocalDateTime ldt=LocalDateTime.parse(x[0],DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
            double v=Double.parseDouble(x[1]); if(!Double.isFinite(v))continue;
            a.add(new Sample(ldt.atZone(jst).toEpochSecond(),v));
        }
        a.sort(Comparator.comparingLong(s->s.t)); return a;
    }
    static double integrate(List<Sample>a,double scale){
        double e=0.0; int used=0;
        for(int i=1;i<a.size();i++){
            Sample p=a.get(i-1), q=a.get(i); double dt=(q.t-p.t)/3600.0;
            if(dt<=0.0||dt>0.25)continue;
            e+=(p.v+q.v)*0.5*dt*scale; used++;
        }
        if(used==0)throw new IllegalArgumentException("No consecutive samples could be integrated");
        return e;
    }
    public static void main(String[]args)throws Exception{
        if(args.length!=2){System.err.println("Usage: dailyenergy solar.dat pv.dat");System.exit(1);}
        List<Sample>solar=read(Paths.get(args[0])), pv=read(Paths.get(args[1]));
        if(solar.size()<2||pv.size()<2)throw new IOException("Not enough time-series samples");
        double solarKWh=integrate(solar,1.0/1000.0);
        double pvKWh=integrate(pv,1.0);
        System.out.printf(Locale.ROOT,"Solar energy : %.3f kWh/m^2%n",solarKWh);
        System.out.printf(Locale.ROOT,"PV energy    : %.3f kWh/kWp%n",pvKWh);
        System.out.printf("Samples      : solar=%d pv=%d%n",solar.size(),pv.size());
    }
}

BONUS MISSION:もっと調べてみたい人へ

このScience Labでは msm.1km の日射量とPV出力を中心に扱いました。ここで使った「平均・切り出し・差・時系列・積算」という考え方は、ほかのAMATERASSプロダクトや別の地域にも応用できます。

AMATERASS プロダクトガイドを見る → プロダクト・ダウンロードを見る →

補助ファイルをまとめて取得: Science Lab補助ファイル一括版(ZIP)も利用できます。Tmap本体は含みません。通常は、各STEPに書かれた wget コマンドで必要なファイルを ~/amaterass_lab に順番に取得してください。

利用上の注意・免責事項

このScience Labを安心して使うために、次の点を確認してください。

教材について

本教材は教育・研究での利用を想定して作成しています。内容の正確性には配慮していますが、すべての環境での動作、内容の完全性、将来にわたる継続提供を保証するものではありません。

解析結果について

ここで得られる画像や数値は学習・解析用です。防災、設備運用、安全管理など、人命・財産に関わる判断の唯一の根拠として使用しないでください。

データ・通信について

AMATERASSデータや外部配布サーバーは、保守、通信障害、仕様変更などにより一時的に利用できない場合があります。データの利用条件はAMATERASSのデータ利用・ダウンロード案内を確認してください。

ソフトウェアと第三者データ

Tmap本体はMIT Licenseで公開されています。Tmapに含まれる第三者データには別のライセンスが適用されるため、詳細は配布物のライセンス文書を確認してください。

AMATERASS データ利用・ダウンロード → Tmap・ライセンス →