import json
import math
from r5_common import EXP, FIRES, HOLDOUT, VAL
METRICS = ["p0_mean", "dur_mean", "wind_scale_mean", "contained_fraction", "area_ratio_mean"]
SEEDS = range(5)
GROWTH_FLOOR = 1e-4
def sigmoid(x):
return 1.0 / (1.0 + math.exp(-x))
def mean_sd_median(xs):
n = len(xs)
m = sum(xs) / n
var = sum((x - m) ** 2 for x in xs) / n if n > 1 else 0.0
sd = math.sqrt(var)
s = sorted(xs)
med = s[n // 2] if n % 2 else (s[n // 2 - 1] + s[n // 2]) / 2.0
return m, sd, med
def load_reports(fire):
return [json.loads((EXP / "exp33_noise" / f"{fire}_base_seed{s}.json").read_text()) for s in SEEDS]
def posterior_trajectory(fire, reports):
scores0 = reports[0]["scores"]
n = len(scores0)
hours0 = [s["hours"] for s in scores0]
for r in reports[1:]:
hrs = [s["hours"] for s in r["scores"]]
obsb = [s["obs_burned"] for s in r["scores"]]
assert hrs == hours0, f"{fire}: seed hours mismatch"
assert obsb == [s["obs_burned"] for s in scores0], f"{fire}: seed obs_burned mismatch"
day0 = hours0[0]
obs = []
for i in range(n):
row = {
"day": (hours0[i] - day0) / 24.0,
"hours": hours0[i],
"obs_burned": scores0[i]["obs_burned"],
}
for metric in METRICS:
vals = [r["scores"][i][metric] for r in reports]
m, sd, med = mean_sd_median(vals)
row[metric + "_mean"] = m
row[metric + "_sd"] = sd
row[metric + "_median"] = med
obs.append(row)
return obs
def wide_gaps(obs):
out = []
for i in range(1, len(obs)):
gap_hours = obs[i]["hours"] - obs[i - 1]["hours"]
if gap_hours > 24.0 + 1e-6:
out.append({"day": obs[i]["day"], "gap_hours": gap_hours})
return out
def containment_curve(reports, obs):
members = []
for r in reports:
for g in r["final_genomes"]:
members.append((g["contain_a"]["Float"], g["contain_b"]["Float"]))
n = len(obs)
floor_hits = 0
growths = []
for d in range(1, n):
before = obs[d - 1]["obs_burned"]
burned = obs[d]["obs_burned"]
raw = (burned - before) / before
g = max(raw, GROWTH_FLOOR)
if g == GROWTH_FLOOR and raw < GROWTH_FLOOR:
floor_hits += 1
growths.append(g)
survival = [1.0] * len(members)
curve = [0.0]
for gd in growths:
lg = math.log(gd)
for i, (a, b) in enumerate(members):
p = sigmoid(a + b * lg)
survival[i] *= (1.0 - p)
curve.append(1.0 - sum(survival) / len(survival))
return curve, len(members), floor_hits
def crossing_day(days, values, threshold=0.5):
if not values:
return None
if values[0] >= threshold:
return days[0]
for i in range(1, len(values)):
if values[i] >= threshold:
d0, d1, v0, v1 = days[i - 1], days[i], values[i - 1], values[i]
frac = (threshold - v0) / (v1 - v0) if v1 != v0 else 0.0
return d0 + frac * (d1 - d0)
return None
def ics209_curve(fire, hours_of_first_mask):
path = VAL / "data" / "scenarios" / fire / "containment.json"
data = json.loads(path.read_text())
rows = sorted(data["rows"], key=lambda r: r["hours"])
days = [(r["hours"] - hours_of_first_mask) / 24.0 for r in rows]
pct = [r["pct_contained"] for r in rows]
return {
"incident_name": data.get("incident_name"),
"incident_id": data.get("incident_id"),
"provenance_note": data.get("provenance", {}).get("note"),
"days": days,
"pct_contained": pct,
}
def main():
out = {}
for fire in FIRES:
reports = load_reports(fire)
obs = posterior_trajectory(fire, reports)
curve, n_members, floor_hits = containment_curve(reports, obs)
model_days = [row["day"] for row in obs]
ics = ics209_curve(fire, obs[0]["hours"])
model_day50 = crossing_day(model_days, curve)
ics_day50 = crossing_day(ics["days"], [p / 100.0 for p in ics["pct_contained"]])
out[fire] = {
"holdout": fire in HOLDOUT,
"n_members": n_members,
"growth_floor_hits": floor_hits,
"wide_gaps": wide_gaps(obs),
"obs": obs,
"model_containment_curve": [{"day": d, "contained_fraction_model": c}
for d, c in zip(model_days, curve)],
"model_day50": model_day50,
"ics209": ics,
"ics_day50": ics_day50,
"day50_lead_days": (None if model_day50 is None or ics_day50 is None
else ics_day50 - model_day50),
}
(EXP / "exp42_posterior.json").write_text(json.dumps(out, indent=1))
print(f"wrote exp42_posterior.json, {len(out)} fires")
for fire, r in out.items():
tag = "*" if r["holdout"] else " "
m50 = f"{r['model_day50']:.1f}" if r["model_day50"] is not None else "never"
i50 = f"{r['ics_day50']:.1f}" if r["ics_day50"] is not None else "never"
lead = f"{r['day50_lead_days']:+.1f}" if r["day50_lead_days"] is not None else "n/a"
print(f" {fire:14s}{tag} model day50 {m50:>6s} ics day50 {i50:>6s} lead {lead:>6s}")
if __name__ == "__main__":
main()