AMATERASS Science Lab

Using geostationary satellite data,explore how solar radiation changes

Use real AMATERASS Japan-area data at 1 km resolution and 10-minute intervals to explore satellite data analysis with Linux and Java.

FIELD: HIMAWARI msm.1km / 1 km / 10 min
Example solar-radiation map
Example of AMATERASS Japan-area 1 km data (20 Aug 2016, 03:00 UTC)

Today's research question

How does solar radiation change with time and location? Using real AMATERASS data, you will move step by step through observation, processing, comparison, and interpretation. Start with LEVEL 0 to check your Ubuntu environment, then continue through seven missions.

1

Check the mission

First, make sure you know what you are going to investigate.

2

Run the analysis

Use the provided scripts and programs to process real data.

3

Check the result

At the end of each step, check the result and understand what happened before moving on.

Build the analysis one level at a time

LEVEL 1Track changes over time
LEVEL 2Compare days
LEVEL 3Zoom in on a region
LEVEL 4Look at one month
LEVEL 5Difference from monthly mean
LEVEL 6Point time series
LEVEL 7Compare PV output
LEVEL 0 / SYSTEM CHECK

MISSION 0: Prepare your analysis environment

MISSION GOALFirst, check that Java, GNU Wget 1.x, bzip2, GNU date, ImageMagick, and Gnuplot are available on Ubuntu.
Working directory: Do all work in ~/amaterass_lab. Move to that directory first, then use wget to download each required file directly into it.
0

Check the required commands

Before starting the analysis, check that all required commands are available.

1. Download the required files in the working directory
mkdir -p ~/amaterass_lab
cd ~/amaterass_lab
wget -O system_check.sh https://amaterass.science/science-lab/system_check.sh
Check this layout before running the command
~/amaterass_lab/
└── system_check.sh
2. Run
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
If you see NG: That item is not ready yet. Install or configure the required software and make sure every item shows OK before continuing.
LEVEL 0 CLEAR
When SYSTEM READY appears, continue to LEVEL 1.

Go one level deeper: what is running underneath?

If something does not work

If you see SYSTEM NOT READY, first identify the line marked NG. The check_cmd function tests each required command independently, so the missing tool is explicit.

./system_check.sh
Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Find required commands
STEP 2Evaluate OK / NG
STEP 3SYSTEM READY
system_check.sh Shell
What does it do? Checks Java, GNU Wget 1.x, bzip2, GNU date, ImageMagick, and Gnuplot, and prints SYSTEM READY only when every required command is available.
#!/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: How does solar radiation change every 10 minutes?

MISSION GOALRead the AMATERASS Japan-area 1 km data yourself, then expand from a single map to a GIF animation over consecutive times. Before that, confirm that the data value, latitude, longitude, and actual observation time all correspond to the same pixel.
STEP 1Prepare Tmap
STEP 2Four data files
STEP 3Read one pixel
STEP 4–6Make the frames
STEP 7Build the GIF

Read each AMATERASS pixel as value + location + time

Japan-area msm.1km is a 2521-row × 3001-column grid. At the same [row, column] index, solar radiation, latitude, longitude, and time describe the same observed pixel.

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

Prepare Tmap

To map solar radiation, prepare Tmap ver.3.0 C² and the helper script draw_map.sh.

tmap_v3.0cc_
20260815d.tar.gz
Tmap
📁
tmap/Java program
sh
draw_map.shScience Lab helper
draw_map.sh passes the grid settings needed for this Science Lab to Tmap.
1. Download the required files in the working directory
cd ~/amaterass_lab
wget -c https://downloads.amaterass.science/tmap_v3.0cc_20260815d.tar.gz
wget -O draw_map.sh https://amaterass.science/science-lab/draw_map.sh
Check this layout before running the command
~/amaterass_lab/
├── tmap_v3.0cc_20260815d.tar.gz
└── draw_map.sh
2. Run
cd ~/amaterass_lab
tar xzf tmap_v3.0cc_20260815d.tar.gz
cd tmap
javac *.java
cd ..
chmod +x draw_map.sh
CHECK
If ls tmap/*.class lists tmap.class and other class files, Tmap is ready.
2

First time step: prepare four files

For 03:00 UTC on August 20, 2016 (12:00 JST), prepare the solar-radiation data, per-pixel observation time, latitude, and longitude files.

LATLatitude
LNGLongitude
TIMEActual pixel observation time
One file per time
SOLARSolar radiation W/m²
One file per time
All four files use the same 2521 × 3001 grid. data[row,column] ↔ lat[row,column], lng[row,column], time[row,column]
1. Download the required files in the working directory
cd ~/amaterass_lab
wget -O download_reference.sh https://amaterass.science/science-lab/download_reference.sh
wget -O download_one.sh https://amaterass.science/science-lab/download_one.sh
Check this layout before running the command
~/amaterass_lab/
├── download_reference.sh
└── download_one.sh
2. Run
cd ~/amaterass_lab
chmod +x download_reference.sh download_one.sh
./download_reference.sh
./download_one.sh 2016 08 20 03 00
CHECK
After decompression, each of the four files should be 30,262,084 bytes. Check with ls -l data/reference/*.bin data/sample/*.bin.
3

Read value, location, and time for the same pixel

Prepare the Science Lab analysis programs, find the pixel nearest a chosen location from LAT/LNG, and read solar radiation and actual observation time from the same array index.

What TIME means: The 03:00 in the filename is the nominal time. Because the satellite scans the Earth, the actual observation time differs slightly from pixel to pixel. The TIME file stores elapsed time from 00:00 UTC on that date, in units of days.
1. Download the required files in the working directory
cd ~/amaterass_lab
wget -O analysis_msm1km.tar.gz https://amaterass.science/science-lab/analysis_msm1km.tar.gz
Check this layout before running the command
~/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. Run
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
If pixel, latitude, longitude, solar flux, and actual UTC are displayed, you have linked all four files at the same pixel.
4

Make one solar-radiation map

Now turn the Float32 numerical grid into an image with Tmap.

Float32
Solar-radiation BIN2521 × 3001
Java
TmapValue → color + coastline
PNG
MapInspect the result
Check this layout before running the command
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/sample/
    └── 201608200300.dwn.sw.flx.sfc.msm.1km.bin
2. Run
cd ~/amaterass_lab
./draw_map.sh data/sample/201608200300.dwn.sw.flx.sfc.msm.1km.bin 1400 0 7 "W/m²"
CHECK
Open data/sample/201608200300.dwn.sw.flx.sfc.msm.1km.bin.png and observe the spatial pattern.
5

Expand to consecutive times

To follow daytime in Japan, download solar-radiation data every 10 minutes from 23:00 UTC on August 19 (08:00 JST on August 20) through 10:40 UTC on August 20 (19:40 JST). Times with no data are skipped automatically.

23:0023:1023:2010:2010:3010:40
The available times will be used in chronological order.
1. Download the required files in your work directory
cd ~/amaterass_lab
wget -O download_animation_data.sh https://amaterass.science/science-lab/download_animation_data.sh
Check this layout before running the command
~/amaterass_lab/
└── download_animation_data.sh
2. Run
cd ~/amaterass_lab
chmod +x download_animation_data.sh
./download_animation_data.sh 2016 08 20 08 00 19 40
CHECK
When Available frames: is shown, the download step is complete.
6

Render the PNG images

Render the available solar-radiation files with Tmap in chronological order. The color scale is also supplied as command arguments.

1. Download the required file in your work directory
cd ~/amaterass_lab
wget -O render_animation.sh https://amaterass.science/science-lab/render_animation.sh
Check this layout before running the command
~/amaterass_lab/
├── render_animation.sh
├── draw_map.sh
├── data/animation/20160820/
│   ├── timestamps.txt
│   └── 201608....msm.1km.bin
└── tmap/
    └── tmap.class
2. Run
cd ~/amaterass_lab
chmod +x render_animation.sh
./render_animation.sh 2016 08 20 1400 0 7 "W/m²"
CHECK
When PNG frames ready: is shown, the frames are ready.
7

Turn the PNG images into a GIF

Combine the available PNG images in time order. Here the GIF width is set to 1200 px and the ImageMagick delay value to 5.

1. Download the required file in your work directory
cd ~/amaterass_lab
wget -O make_animation.sh https://amaterass.science/science-lab/make_animation.sh
Check this layout before running the command
~/amaterass_lab/
├── make_animation.sh
└── data/animation/20160820/
    ├── timestamps.txt
    └── 201608....msm.1km.bin.png
2. Run
cd ~/amaterass_lab
chmod +x make_animation.sh
./make_animation.sh 2016 08 20 1200 5
LEVEL 1 CLEAR
Open amaterass_20160820.gif and observe the changes over time.
OBSERVATION: Observe how solar radiation changes over time and how the pattern differs from place to place.

Go one level deeper: what is running underneath?

If something does not work

If the first timestamp is incomplete, inspect the sample/reference directories and rerun the same download. Complete files are reused and incomplete binaries are replaced.

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

You can also rerun download_animation_data.sh if too few animation frames were obtained.

Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Download data
STEP 2Match LAT/LNG/TIME
STEP 3Map & animate with Tmap
download_reference.sh Shell
What does it do? Downloads the fixed LAT/LNG grids shared by all timestamps and validates the decompressed file sizes.
#!/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
What does it do? Downloads solar radiation and TIME for one timestamp. An incomplete .bin is not trusted: it is removed unless its size is exactly 30,262,084 bytes.
#!/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
What does it do? A thin wrapper that passes the binary field and grid information to Tmap. It keeps numerical processing separate from visualization.
#!/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
What does it do? Converts the requested JST interval to UTC and downloads 10-minute fields. Network transfer remains single-stream while bzip2 decompression runs with up to four jobs by default.
#!/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
What does it do? Uses Tmap to render each downloaded field as a PNG frame.
#!/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
What does it do? Uses ImageMagick to combine the rendered PNG frames into one GIF animation.
#!/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
What does it do? Shared Java helper for reading and writing headerless Big-endian Float32 grids consistently across the analysis programs.
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
What does it do? Finds the grid pixel nearest the requested latitude/longitude, reads solar radiation and TIME at the same index, and reconstructs the actual UTC observation time for that pixel.
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 / DAILY MEAN

MISSION 2: How different is solar radiation from day to day?

MISSION GOALCreate daily-mean solar-radiation fields for August 19, 20, and 21, 2016 (UTC) from 10-minute data, then map all three with the same color scale. For each UTC hour with solar-radiation files, average the available 10-minute values. Hours with no solar-radiation files at night are treated as 0 W/m², and the daily mean is calculated over all 24 hours.
STEP 1Analysis tools
STEP 2Prepare three days
STEP 3Compute daily means
STEP 4Map all three
GOALCompare the days
1

Check the analysis tools from LEVEL 1

Confirm that the programs needed for daily averaging are ready.

Check this layout before running the command
~/amaterass_lab/
└── analysis.msm1km/
    ├── pixelinfo.java
    ├── dailymean.java
    ├── crop.java
    ├── monthlymean.java
    ├── anomaly.java
    └── *.class
CHECK
If ls analysis.msm1km/*.class lists the class files, continue.
2

Prepare 10-minute data for three days

Download the 10-minute data for August 19, 20, and 21, 2016.

1. Download the required file in your work directory
cd ~/amaterass_lab
wget -O download_day.sh https://amaterass.science/science-lab/download_day.sh
Check this layout before running the command
~/amaterass_lab/
└── download_day.sh
2. Run
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
Continue when data/daily_raw/20160819/, 20160820/, and 20160821/ have been created.
3

Create a daily mean for each of the three days

For each UTC hour with data, average the available 10-minute values. Nighttime hours with no solar-radiation files are treated as 0 W/m², and the daily mean is calculated over all 24 hours.

Before you calculate: Which of August 19, 20, or 21 do you expect to show the largest areas of high solar radiation?
1. Download the required file in your work directory
cd ~/amaterass_lab
wget -O make_daily_mean.sh https://amaterass.science/science-lab/make_daily_mean.sh
Check this layout before running the command
~/amaterass_lab/
├── make_daily_mean.sh
├── analysis.msm1km/
│   └── dailymean.class
└── data/daily_raw/
    ├── 20160819/
    ├── 20160820/
    └── 20160821/
2. Run
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
This step is complete when three daily-mean files appear in data/daily/.
4

Map the three daily means with the same scale

Draw the three daily means with the same color scale so they can be compared directly.

Check this layout before running the command
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/daily/
    ├── 20160819.dailymean....bin
    ├── 20160820.dailymean....bin
    └── 20160821.dailymean....bin
2. Run
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
Place the three maps side by side and compare the daily differences.

Go one level deeper: what is running underneath?

If something does not work

If the daily mean is not created, first count complete source fields.

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

Some fully dark hours have no solar-radiation files; this is normal. dailymean.java contributes 0 W/m² for those hours. If needed, rerun:

./download_day.sh 2016 08 20
./make_daily_mean.sh 2016 08 20
Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Download one day
STEP 2Hourly means
STEP 324-hour daily mean
download_day.sh Shell
What does it do? Checks the 144 nominal 10-minute slots in a day and downloads available solar-radiation files. Wget stays single-stream; decompression uses up to four jobs and incomplete binaries are removed automatically.
#!/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
What does it do? Prepares paths, recompiles Java when needed, and runs 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
What does it do? Averages the available 10-minute fields within each UTC hour, then averages 24 hourly means. Fully dark hours with no solar-radiation files contribute 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 / Crop a region

MISSION 3: Crop a region

MISSION GOALDownload the 03:00 UTC 10-minute solar-radiation fields for July 19, 20, and 21, 2016, then crop only 137–140°E and 34–37°N from the 3001 × 2521 Japan-area grid. No averaging is performed here: only the spatial extent of the original data is reduced.
STEP 1Check the region
STEP 2Download 3 fields
STEP 3Crop all 3
STEP 4Compare maps
1

Check the latitude/longitude range

For this example, use 137°E–140°E and 34°N–37°N.

3001
×
2521
Japan areaAbout 30 MB
300
×
300
137–140°E
34–37°N
About 0.35 MB
This keeps only the region of interest from the same solar-radiation field. No temporal averaging is performed.
Cropping with LAT/LNG: The crop bounds are passed as arguments to crop_solar.sh. crop.java first derives the available domain from the LAT / LNG grids and proceeds only when the requested rectangle is completely inside it. A partly out-of-range request is rejected instead of being silently clipped.
2

Download 03:00 UTC on July 19, 20, and 21

Use the same time on all three days. 03:00 UTC is 12:00 JST.

1. Download the required script
cd ~/amaterass_lab
wget -O download_solar.sh https://amaterass.science/science-lab/download_solar.sh
Check this layout before running
~/amaterass_lab/
└── download_solar.sh
2. Run
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/ should contain the solar-radiation files for 201607190300, 201607200300, and 201607210300.
3

Crop the same region from all three fields

Use LAT/LNG to crop 137–140°E and 34–37°N from each field.

1. Download the required script
cd ~/amaterass_lab
wget -O crop_solar.sh https://amaterass.science/science-lab/crop_solar.sh
Check this layout before running
~/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. Run
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
Three 300 × 300 cropped files should appear in data/crop/.
4

Map the three cropped fields

Render the same 03:00 UTC time, the same region, and the same color scale on all three days.

Check this layout before running
~/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. Run
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
You cropped and compared the same time on three different days without changing the data values by averaging.

Go one level deeper: what is running underneath?

If something does not work

If no crop is produced, verify that both the source solar field and LAT/LNG grids exist.

ls data/crop_source/
ls data/reference/

If you see ERROR: Requested crop is outside the available grid., part of the requested latitude/longitude range lies outside the msm.1km domain. Choose new bounds inside the displayed Available longitude / latitude range.

When the Java source is updated, crop_solar.sh automatically recompiles it if crop.java is newer than the class file.

Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Search LAT/LNG bounds
STEP 2Crop the same indices
STEP 3Compare with Tmap
download_solar.sh Shell
What does it do? Downloads one solar-radiation field into the crop-source directory.
#!/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
What does it do? Passes the geographic bounds and source field to crop.java. If crop.java has been updated, it recompiles automatically, then crops the same [row,column] positions from LAT/LNG and solar radiation.
#!/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
What does it do? Derives the available domain from the LAT/LNG grids, verifies that the requested rectangle is fully inside it, and only then extracts the matching rows and columns. Out-of-range requests are rejected rather than silently clipped.
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 / MONTHLY MEAN

MISSION 4: What appears when you average one month?

MISSION GOALPrepare all 31 daily means for August 2016 on the full msm.1km Japan grid (3001 × 2521), then create a monthly-mean solar-radiation map for the full domain. The regional crop from LEVEL 3 is not used here.
STEP 1Prepare 31 full-grid daily means
STEP 2Compute the full-grid monthly mean
STEP 3View the full Japan domain
1

Prepare 31 full-grid daily means

Prepare the 3001 × 2521 daily-mean files for August 1 through August 31 in data/daily/.

Check this layout before running the command
~/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
CHECK
ls data/daily/201608*.dailymean.dwn.sw.flx.sfc.msm.1km.bin | wc -l should return 31.
If you do not have all 31 daily means

prepare_month.sh processes August one day at a time: download the 10-minute data, make the full-grid daily mean, delete that day's 10-minute files, and continue to the next day.

Download the required scripts
cd ~/amaterass_lab
wget -O download_day.sh https://amaterass.science/science-lab/download_day.sh
wget -O make_daily_mean.sh https://amaterass.science/science-lab/make_daily_mean.sh
wget -O prepare_month.sh https://amaterass.science/science-lab/prepare_month.sh
Check this layout before running the command
~/amaterass_lab/
├── download_day.sh
├── make_daily_mean.sh
├── prepare_month.sh
└── analysis.msm1km/
    └── dailymean.class
Run
cd ~/amaterass_lab
chmod +x download_day.sh make_daily_mean.sh prepare_month.sh
./prepare_month.sh 2016 08
2

Create the monthly mean from 31 daily means

Average the corresponding cells of the 31 daily means on the same 3001 × 2521 grid.

Daily mean × 31Full Japan domain
AverageCell by cell
BIN
Monthly mean × 1Full Japan domain
1. Download the required file in the working directory
cd ~/amaterass_lab
wget -O make_monthly_mean.sh https://amaterass.science/science-lab/make_monthly_mean.sh
Check this layout before running the command
~/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. Run
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
CHECK
If Days used : 31 is displayed, the monthly mean was created from all 31 days of August.
3

View the monthly mean over the full Japan domain

Map the full msm.1km Japan domain and look for spatial differences that remain after averaging a month.

THINK: What regional differences in monthly-mean solar radiation can you see across the full Japan domain? Compared with a single daily map, which broad features remain and which fine-scale variations are smoothed out?
Check this layout before running the command
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/monthly/
    └── 201608.monthlymean.dwn.sw.flx.sfc.msm.1km.bin
2. Run
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
You created and viewed a monthly mean over the full Japan domain.

Go one level deeper: what is running underneath?

If something does not work

If the 31 daily files are not ready, count them first:

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

If the result is not 31, rerun prepare_month.sh. Complete days are reported as Already exists: and only missing days are processed.

./prepare_month.sh 2016 08

If the Java source itself is missing, extract and compile the analysis package:

tar xzf analysis_msm1km.tar.gz
javac analysis.msm1km/*.java
Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Build each daily mean
STEP 2Check required days
STEP 3Monthly mean
prepare_month.sh Shell
What does it do? Determines the number of days in the month and runs download_day.sh → make_daily_mean.sh for each day. Complete existing daily means are reused.
#!/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
What does it do? Sets the monthly input/output paths, recompiles Java if necessary, and calls 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
What does it do? Checks that the expected number of daily means exists (31 for August), then averages the grids pixel by pixel.
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 / ANOMALY

MISSION 5: How did August 20 differ from the August 2016 mean across the full Japan domain?

MISSION GOALSubtract the full-domain August monthly mean from the full-domain daily mean for August 20, 2016. On the 3001 × 2521 grid, map where solar radiation was higher or lower than the August 2016 mean.
STEP 1Think about the difference
STEP 2Compute the full-grid anomaly
STEP 3View the full Japan domain
1

Calculate daily mean − monthly mean

Both fields use the same 3001 × 2521 msm.1km Japan grid, so corresponding grid cells can be subtracted directly.

August 20 daily meanFull Japan domain
August monthly meanFull Japan domain
±
AnomalyDifference from mean
Positive values mean more solar radiation than the August 2016 mean; negative values mean less.
Anomaly: Here the reference is the monthly mean for the same August 2016, not a climatological normal.
2

Create the full-domain anomaly file

Subtract corresponding cells of the August 20 daily mean and the August monthly mean on the full 3001 × 2521 grid.

Before you calculate: Which parts of the Japan domain do you expect to have more solar radiation than the August 2016 mean, and which parts less?
1. Download the required file in the working directory
cd ~/amaterass_lab
wget -O make_anomaly.sh https://amaterass.science/science-lab/make_anomaly.sh
Check this layout before running the command
~/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. Run
cd ~/amaterass_lab
chmod +x make_anomaly.sh
./make_anomaly.sh 2016 08 20
CHECK
data/anomaly/20160820.anomaly.dwn.sw.flx.sfc.msm.1km.bin should be created.
3

Map the anomaly over the full Japan domain

Use a symmetric range around zero so positive and negative anomalies can be compared directly.

Negative anomalyBelow August mean
0Near August mean
Positive anomalyAbove August mean
First confirm that 0 W/m² is at the center of the color scale.
Final question: Across the full Japan domain, where was solar radiation higher than the August 2016 mean on August 20, and where was it lower? What large-scale pattern can you see in the anomaly?
Check this layout before running the command
~/amaterass_lab/
├── draw_map.sh
├── tmap/
│   └── tmap.class
└── data/anomaly/
    └── 20160820.anomaly.dwn.sw.flx.sfc.msm.1km.bin
2. Run
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
You mapped how the August 20 daily mean differed from the August 2016 mean across the full Japan domain.

Go one level deeper: what is running underneath?

If something does not work

If the anomaly cannot be created, confirm that both required inputs exist: the selected daily mean and the monthly mean.

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
Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Read daily mean
STEP 2Read monthly mean
STEP 3Calculate difference
make_anomaly.sh Shell
What does it do? Passes the selected daily mean and the corresponding monthly mean to anomaly.java and prepares the output path.
#!/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
What does it do? Calculates daily mean − monthly mean at every grid point. Positive values are above the monthly mean; negative values are below it.
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: Follow one location through a day

MISSION GOALSelect one location by latitude and longitude from the 3001 × 2521 grid and extract a one-day solar-radiation time series. The horizontal axis uses the actual observation time calculated from the TIME value at the same pixel, not simply the timestamp in the filename.
STEP 1Download one day
STEP 2Select one grid cell
STEP 3Read time from TIME
STEP 4Plot with Gnuplot
1

Download one day of solar radiation and TIME

Download the 10-minute data corresponding to August 20, 2016 in JST. For each available solar-radiation file, the matching TIME file is downloaded as well.

Download the helper scripts
cd ~/amaterass_lab
wget -O download_point_day.sh https://amaterass.science/science-lab/download_point_day.sh
wget -O pickup_day.sh https://amaterass.science/science-lab/pickup_day.sh
wget -O plot_solar_day.sh https://amaterass.science/science-lab/plot_solar_day.sh
wget -O analysis_msm1km.tar.gz https://amaterass.science/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
Run
./download_point_day.sh 2016 08 20
CHECK
Solar-radiation and TIME files should appear in data/timeseries/20160820/.
2

Select the nearest grid cell from latitude and longitude

For this example, specify 35.0°N, 138.0°E. The LAT/LNG grids are searched to find the closest pixel.

Run
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
Why use TIME? Himawari does not observe every pixel of the full image at exactly the same instant. The timestamps in solar.dat are therefore calculated from the TIME value at the selected pixel and converted to actual observation time in JST.
3

Plot the daily variation with Gnuplot

Instead of a map, plot actual observation time on the x-axis and downward shortwave flux [W/m²] on the y-axis.

Run
cd ~/amaterass_lab
./plot_solar_day.sh 2016 08 20
LEVEL 6 CLEAR
data/timeseries/20160820/solar_timeseries.png should be created. You can now see the satellite-derived physical quantity as a numerical time series.

Go one level deeper: what is running underneath?

If something does not work

If the time series cannot be produced, check that solar and TIME files exist for matching timestamps and that the point-series source is available.

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.sh can refresh the analysis package automatically when an older working directory does not contain pointseries.java.

Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Lat/lon → nearest pixel
STEP 2Read solar + TIME
STEP 3Gnuplot time series
download_point_day.sh Shell
What does it do? Converts one JST day to UTC and downloads available solar-radiation fields together with their matching TIME fields, because the time series reads both at the same pixel.
#!/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
What does it do? Passes LAT/LNG plus the solar/TIME files to pointseries.java and creates solar.dat. It can also refresh the analysis source in an older working directory.
#!/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/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
What does it do? Uses Gnuplot to plot actual observation time in JST against solar radiation. The default y-range is 0–1400 W/m² and solar radiation is orange.
#!/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
What does it do? Finds the nearest grid point once, then reads the physical value and TIME from that same index for each file. TIME is converted from UTC to JST and only samples belonging to the requested local date are written.
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: How does solar radiation connect to photovoltaic output?

MISSION GOALExtract AMATERASS estimated PV output for the same location and day as LEVEL 6. Compare solar radiation [W/m²] with PV output [kW/kWp], then integrate both time series to obtain daily solar energy and estimated energy yield per 1 kWp.
STEP 1Download PV output
STEP 2Compare two time series
STEP 3Integrate one day
1

Download AMATERASS estimated PV output

Use unit.pvp.tc028.ac945.sfc, the estimated output per 1 kWp [kW/kWp] calculated with a temperature coefficient of −0.28 %/°C and inverter efficiency of 94.5%.

Download the helper scripts and run
cd ~/amaterass_lab
wget -O download_pv_day.sh https://amaterass.science/science-lab/download_pv_day.sh
wget -O pickup_pv_day.sh https://amaterass.science/science-lab/pickup_pv_day.sh
wget -O plot_solar_pv.sh https://amaterass.science/science-lab/plot_solar_pv.sh
wget -O daily_energy.sh https://amaterass.science/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
Reading unit.pvp: A value of 0.8 means an estimated output of about 0.8 kW from a 1 kWp PV system at that time and grid cell.
2

Compare solar radiation and PV output with Gnuplot

Because the quantities have different units, plot them in two panels with the same time axis. Both use actual observation times derived from TIME.

Run
cd ~/amaterass_lab
./plot_solar_pv.sh 2016 08 20
CHECK
Open data/timeseries/20160820/solar_pv_timeseries.png and compare how the two physical quantities rise and fall through the day.
3

Convert instantaneous values into daily energy

Integrate along the actual observation times to convert the time series into daily totals.

Run
cd ~/amaterass_lab
./daily_energy.sh 2016 08 20
Solar energy : ... kWh/m^2
PV energy : ... kWh/kWp
Samples : solar=... pv=...
Think: Solar energy [kWh/m²] and PV energy yield [kWh/kWp] are not the same physical quantity. How did the shape of the time series and the daily totals change as solar radiation was converted into estimated PV output?
ALL LEVELS CLEAR
You moved beyond satellite images to extract physical quantities at one point, follow them in time, and connect solar radiation to photovoltaic energy.

Go one level deeper: what is running underneath?

If something does not work

If the PV comparison is missing, first check the extracted text data.

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

If PV fields are missing, download and extract them again for the same timestamps used in LEVEL 6.

./download_pv_day.sh 2016 08 20
./pickup_pv_day.sh 2016 08 20 35.0 138.0
Read the programs used in this LEVEL

These are the actual sources downloaded and executed by this page. You do not need to understand every line; start by tracing the inputs, the physical-data processing, and the outputs.

STEP 1Download PV at same times
STEP 2Compare solar and PV
STEP 3Integrate to kWh
download_pv_day.sh Shell
What does it do? Uses the solar-radiation timestamps already present from LEVEL 6 to download matching unit.pvp.tc028.ac945.sfc fields. Network transfer stays single-stream and decompression uses up to four jobs.
#!/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
What does it do? Extracts both solar radiation and PV output at the same latitude/longitude, producing solar.dat and 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/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
What does it do? Plots solar radiation (orange, 0–1400 W/m²) and PV output (blue, 0–1.2 kW/kWp) in two Gnuplot panels sharing the same time axis.
#!/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
What does it do? Feeds solar.dat and pv.dat to dailyenergy.java and saves the calculated daily totals to daily_energy.txt while also showing them on screen.
#!/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/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
What does it do? Integrates adjacent time-series samples with the trapezoidal rule, converting W/m² to kWh/m² and kW/kWp to kWh/kWp. This is the step where the time series becomes an integrated physical quantity.
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: Explore further

This Science Lab focuses on msm.1km solar-radiation and PV-output data. The same ideas—averaging, cropping, differences, time series, and integration—can be applied to other AMATERASS products and regions.

AMATERASS Product Guide →   Product Download →

Download all helper files: A Science Lab helper-files ZIP is also available. Tmap itself is not included. Normally, follow each step and use the shown wget command to place the required files directly in ~/amaterass_lab.

Notes and disclaimer

Please review the following notes before using this Science Lab.

About this material

This material is intended for education and research. Care has been taken to ensure accuracy, but operation in every environment, completeness of the content, and continued future availability are not guaranteed.

About analysis results

Images and numerical results produced here are for learning and analysis. Do not use them as the sole basis for decisions involving life, property, disaster response, equipment operation, or safety management.

Data and network access

AMATERASS data and external distribution servers may be temporarily unavailable because of maintenance, network problems, or specification changes. Check the AMATERASS data-use and download information for applicable terms.

Software and third-party data

Tmap is released under the MIT License. Third-party data included with Tmap are subject to separate licenses; see the license documents in the distribution for details.

AMATERASS Data Use & Download →   Tmap & Licenses →