# Interactive fields: one gzip-compressed binary file per map
field_dir = assets / "fields"
field_dir.mkdir(exist_ok=True)
for old in field_dir.glob("*"):
old.unlink()
flat = np.flatnonzero(mask.transpose("Y", "X").values.ravel()) # row 0 = northernmost pixel row
n = len(flat)
lead_key = {lead: f"dekad{lead}" for lead in leads}
def at_pixels(a):
return np.asarray(a.transpose("Y", "X").values, dtype="float64").ravel()[flat]
def encode(values, kind, scale):
v = np.round(values * scale)
if kind == "i16":
return np.where(np.isfinite(v), np.clip(v, -32767, 32767), -32768).astype("<i2").tobytes()
if kind == "u16":
return np.where(np.isfinite(v), np.clip(v, 0, 65534), 65535).astype("<u2").tobytes()
return np.where(np.isfinite(v), np.clip(v, 0, 254), 255).astype("u1").tobytes()
def write(name, parts):
(field_dir / f"{name}.bin.gz").write_bytes(gzip.compress(b"".join(encode(*p) for p in parts), 9, mtime=0))
(assets / "pixels.bin").write_bytes(flat.astype("<i4").tobytes())
for lead in leads:
for m in ["CCA", "PCR"]:
write(f"skill-{m}-pearson-{lead_key[lead]}", [(at_pixels(clean(results[lead, m].skill_pearson)), "i16", 1000)])
write(f"skill-{m}-twoafc-{lead_key[lead]}", [(at_pixels(clean(results[lead, m].skill_two_alternative_forced_choice)), "i16", 10)])
write(f"skill-raw-pearson-{lead_key[lead]}", [(at_pixels(raw_pearson[lead]), "i16", 1000)])
days_of = dict(zip(targets.target, targets.days))
for t in test_T:
stamp = f"{pd.Timestamp(t):%Y-%m-%d}"
write(f"obs-{days_of[pd.Timestamp(t)]}-{stamp}", [(at_pixels(observed.sel(T=t)), "u16", 10), (at_pixels(clim_2023.sel(T=t).where(mask)), "u16", 10),
(at_pixels(category.sel(T=t)), "u8", 1)])
for (lead, m), f in forecast.items():
for t in test_T:
stamp = f"{pd.Timestamp(t):%Y-%m-%d}"
write(f"fc-{lead_key[lead]}-{m}-{stamp}", [(at_pixels(f["total"].sel(T=t)), "u16", 10), (at_pixels(f["most_likely"].sel(T=t)), "u8", 1),
(at_pixels(f["top"].sel(T=t)), "u8", 1)])
def records(frame):
return [{k: (None if isinstance(v, float) and not np.isfinite(v) else v) for k, v in row.items()}
for row in frame.to_dict(orient="records")]
top = float(np.ceil(np.nanpercentile(np.concatenate([observed.values.ravel()] + [forecast[k]["total"].values.ravel() for k in forecast]), 99) / 50) * 50)
manifest = {
"grid": {"west": round(float(mask.X.min()) - .025, 3), "north": round(float(mask.Y.max()) + .025, 3), "step": 0.05,
"nx": int(mask.sizes["X"]), "ny": int(mask.sizes["Y"]), "pixels": n},
"leads": leads, "leadKeys": [lead_key[lead] for lead in leads], "leadNames": [lead_names[lead] for lead in leads],
"methods": ["CCA", "PCR"], "rawName": "raw GEFS", "training": "2000-2018",
"sample": "the 171 training dekads of 2000-2018", "cellText": f"Indonesia's {n:,} pixels at 0.05°",
"layout": {"skill": [["value", "i16"]], "obs": [["total", "u16", 10], ["mean", "u16", 10], ["category", "u8", 1]],
"fc": [["total", "u16", 10], ["category", "u8", 1], ["probability", "u8", 1]]},
"scales": {"pearson": 1000, "twoafc": 10},
"rainMax": {"8": top, "9": top, "10": top, "11": top}, "rainLegend": "mm in the dekad", "axisDays": 35,
"timelineTitle": "the next three dekads",
"timelineHint": "Issued at 00 UTC on the last day of a dekad. Bars are the target dekads in days after the issue date; hover for dates.",
"mapsHint": "Dekad +1 at the top. Totals are the 2000-2018 mean for that calendar dekad plus the calibrated anomaly; categories follow CPT's terciles of the training anomalies.",
"seriesHint": "Bars: observed. Dashed: 2000-2018 mean. Lines: CCA forecast totals made one, two and three dekads ahead.",
"issues": [{"issued": f"{s:%Y-%m-%d}", "label": f"{s:%d %b %Y}",
"targets": [{"lead": lead_key[lead], "start": f"{t:%Y-%m-%d}", "days": int(days)} for lead, t, days in items]}
for s, items in issues.items()],
"skillTable": records(skill_by_lead.reset_index().round(4)),
"scores2023": records(forecast_2023.reset_index().round(3)),
"headline": {"issued": f"{example:%Y-%m-%d}", "rows": records(headline.reset_index().round(3))},
"series": pd.DataFrame(area_rows).round(2).to_dict(orient="list"),
"seriesLeads": [lead_key[lead] for lead in series_leads], "seriesUnit": "mm per dekad",
"seriesTitle": "October-December 2023 · area-weighted dekad rainfall over Indonesia",
}
(assets / "manifest.json").write_text(json.dumps(manifest, separators=(",", ":"), allow_nan=False) + "\n")
sizes = sorted(p.stat().st_size for p in field_dir.iterdir())
print(f"{n:,} pixels; {len(sizes)} field files, {sum(sizes) / 1e6:.1f} MB in total (largest {sizes[-1] / 1e3:.0f} kB); "
f"manifest {(assets / 'manifest.json').stat().st_size / 1e3:.0f} kB; colour scale up to {top:.0f} mm")
for f in sorted(data_dir.iterdir()) + sorted(p for p in assets.iterdir() if p.is_file()):
print(f"{f.relative_to(repo)} ({f.stat().st_size / 1000:.0f} kB)")