File size: 13,290 Bytes
41dd7e0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | """Reproduce one entry of Table 2, or one depth point of Table 6 / Fig. 3: csFID-10k of a model on the ten test conditions.
python -m reproduce.table2 list
python -m reproduce.table2 check --system SYS --method M [--S S] [--checkpoint FILE] [--samples DIR] [--hash-large]
python -m reproduce.table2 run --system SYS --method M [--S S] --checkpoint FILE --out DIR [--device cuda]
python -m reproduce.table2 generate --system SYS --method M [--S S] --checkpoint FILE --out DIR [--gids g1,g2] [--n-samples N]
python -m reproduce.table2 score --system SYS --method M [--S S] --out DIR [--samples DIR]
python -m reproduce.table2 compare --system SYS --method M [--S S] --out DIR [--tolerance REL]
SYS is euler | sqg | waves | nlkg | passive_scalar; M is direct_si | edm | cascaded_si (without --S the selected depth S*,
with --S the Cascaded SI model of that depth). The package and the truth bank are taken from <SYS>_DATA and
<SYS>_TRUTH_BANK or given with --data and --truth-bank. Everything else comes from records/<SYS>/: the run configuration
of the model, its sampler resolution n*, the image adapter and the recorded values.
generate 10,000 samples per test condition (bifurcation.sample with the settings of configs/evaluation.yaml:
n* sampler steps, noise stream 777 * 100003 + gid, chunks of 2,000) -> OUT/samples/<variant>/group_<gid>.npz
score csFID per condition (bifurcation.fid.evaluate.evaluate_csfid_run) of OUT/samples, or of the archived samples
given with --samples -> OUT/csfid/{csfid_per_ic.csv, csfid_summary.json, MANIFEST_CSFID.json}
compare per-condition values, mean and SEM against the recorded values -> OUT/COMPARE.json
run generate + score + compare
check the package, truth bank, checkpoint and archived samples against the recorded sha256
"""
from __future__ import annotations
import argparse
import csv
import hashlib
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from bifurcation.config import RECORDS, REPO, SYSTEMS, data_paths, dataset_card, evaluation_settings, records, run_config, unit
def sha256(path: Path) -> str:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1 << 24), b""):
h.update(chunk)
return h.hexdigest()
def asset_hashes(system: str) -> dict:
"""{section: {relative path: sha256}} from records/<system>/assets.sha256."""
out, sec = {}, None
for line in (RECORDS / system / "assets.sha256").read_text().splitlines():
if line.startswith("# "):
sec = line[2:].split(":")[0].strip()
continue
h, q = line.split(" ", 1)
out.setdefault(sec, {})[q] = h
return out
def cmd_list(a):
print(f"{'system':15s} {'model':16s} {'paper':22s} {'n*':>4s} {'NFE':>6s} csFID-10k")
for s in SYSTEMS:
rec = records(s)
for u in rec["units"]:
names = ", ".join({"direct_si": "Direct SI", "edm": "EDM", "cascaded_si": f"Cascaded SI (S*={rec['s_star']})"}[n]
for n in u["paper_names"]) or f"depth S={u['S']}"
e = u["expected_csfid"]
print(f"{s:15s} {u['key']:16s} {names:22s} {u['sampler']['n_steps']:4d} {u['sampler']['nfe']:6d} {e['mean']:.2f} +- {e['sem']:.2f}")
def cmd_check(a) -> int:
rec, u = unit(a.system, a.method, a.S)
data, bank = data_paths(a.system, a.data, a.truth_bank)
card = dataset_card(a.system)
problems, checked = [], 0
big = 200 * 1024 * 1024
for rel, want in card["expected_sha256"].items():
p = data / rel
if not p.is_file():
problems.append(f"--data: missing {rel}"); continue
if p.stat().st_size > big and not a.hash_large:
continue
checked += 1
if sha256(p) != want:
problems.append(f"--data: {rel} sha256 differs from configs/{a.system}/dataset.yaml")
hashes = asset_hashes(a.system)
for gid in rec["test_gids"]:
rel = f"group_{gid:04d}.npz"
p = bank / rel
if not p.is_file():
problems.append(f"--truth-bank: missing {rel}"); continue
if a.hash_large:
checked += 1
if sha256(p) != hashes["truth_bank"][rel]:
problems.append(f"--truth-bank: {rel} sha256 differs")
if a.checkpoint:
got = sha256(Path(a.checkpoint)); checked += 1
if u["checkpoint"] is None:
problems.append(f"--checkpoint: no checkpoint of {u['key']} is distributed")
elif got != u["checkpoint"]["sha256"]:
problems.append(f"--checkpoint: sha256 {got[:16]} is not the recorded {u['checkpoint']['file']}")
if a.samples:
v = u["sampler"]["variant"]
sec = f"samples/{u['paper_names'][0]}" if u["paper_names"] else None # archived samples exist for the Table 2 models
for gid in rec["test_gids"]:
rel = f"{v}/group_{gid:04d}.npz"
p = Path(a.samples) / rel
if not p.is_file():
problems.append(f"--samples: missing {rel}"); continue
want = (hashes.get(sec) or {}).get(rel) if sec else None
if want and a.hash_large:
checked += 1
if sha256(p) != want:
problems.append(f"--samples: {rel} sha256 differs from the archived samples")
print(json.dumps({"system": a.system, "model": u["key"], "files_hashed": checked, "problems": problems}, indent=1))
return 2 if problems else 0
def _fresh(out: Path, sub: str) -> Path:
d = out / sub
if d.exists() and any(d.iterdir()):
raise SystemExit(f"refusing: {d} exists and is not empty (use a fresh --out)")
d.mkdir(parents=True, exist_ok=True)
return d
def cmd_generate(a):
from bifurcation import sample
import yaml
rec, u = unit(a.system, a.method, a.S)
data, bank = data_paths(a.system, a.data, a.truth_bank)
ev = evaluation_settings()
out = Path(a.out).resolve()
out.mkdir(parents=True, exist_ok=True)
cfg_path = out / "config.yaml"
cfg_path.write_text(yaml.safe_dump(run_config(a.system, u, data), sort_keys=False))
n_samples = int(a.n_samples) if getattr(a, "n_samples", None) else int(ev["fid"]["n_generated"])
argv = ["--config", str(cfg_path), "--data", str(data), "--checkpoint", str(Path(a.checkpoint).resolve()),
"--out", str(out / "samples"), "--split", "test", "--n-samples", str(n_samples),
"--n-steps", str(u["sampler"]["n_steps"]), "--seed-base", str(ev["sampling"]["crn_base_seed"]),
"--chunk", str(ev["sampling"]["sample_chunk"]), "--device", a.device]
if rec["x0_from_truth_bank"]:
argv += ["--truth-bank", str(bank)]
if a.gids:
argv += ["--gids", a.gids]
print("$ python -m bifurcation.sample " + " ".join(argv), flush=True)
sample.main(argv)
log = out / "GENERATED.json"
entries = json.loads(log.read_text()) if log.is_file() else []
entries.append({"system": a.system, "model": u["key"], "checkpoint": str(Path(a.checkpoint).resolve()), "gids": a.gids or "all",
"argv": argv, "at": datetime.now(timezone.utc).isoformat(timespec="seconds")})
log.write_text(json.dumps(entries, indent=1) + "\n")
def cmd_score(a):
from bifurcation.fid.adapters import FIDAdapter
from bifurcation.fid.evaluate import evaluate_csfid_run, generated_states
rec, u = unit(a.system, a.method, a.S)
data, bank = data_paths(a.system, a.data, a.truth_bank)
card, fid_cfg = dataset_card(a.system), evaluation_settings()["fid"]
out = Path(a.out).resolve()
samples = Path(a.samples).resolve() if a.samples else out / "samples"
variant = u["sampler"]["variant"]
missing = [g for g in rec["test_gids"] if not (samples / variant / f"group_{g:04d}.npz").is_file()]
if missing:
raise SystemExit(f"{samples / variant}: no samples for test conditions {missing}")
n_gen = int(fid_cfg["n_generated"])
gen_raw = generated_states(samples, variant, data, n_gen)
gen_raw = {g: x for g, x in gen_raw.items() if g in set(rec["test_gids"])}
for g, x in gen_raw.items():
if len(x) != n_gen:
raise SystemExit(f"condition {g}: {len(x)} generated states, the paper's read uses {n_gen}")
adapter = FIDAdapter.load(RECORDS / a.system / "fid_adapter.json", **(card["fid_adapter"].get("params") or {}))
csdir = _fresh(out, "csfid")
summary = evaluate_csfid_run(gen_raw, bank, int(rec["fine_horizon_idx"]), int(fid_cfg["n_truth"]), adapter, csdir,
out / "fid_cache", fid_cfg, a.device,
provenance={"system": a.system, "model": u["key"], "samples": str(samples), "source": "reproduce.table2 score"})
print(f"csFID-10k {a.system} {u['key']}: {summary['csFID']['mean']:.6f} +- {summary['csFID']['sem']:.6f} (n = {summary['csFID']['n']})")
def cmd_compare(a) -> int:
rec, u = unit(a.system, a.method, a.S)
out = Path(a.out).resolve()
got = {int(r["gid"]): float(r["sfid"]) for r in csv.DictReader(open(out / "csfid" / "csfid_per_ic.csv"))}
summ = json.loads((out / "csfid" / "csfid_summary.json").read_text())["csFID"]
exp = u["expected_csfid"]
rows, worst = [], 0.0
for g in rec["test_gids"]:
e, v = exp["per_condition"][str(g)], got.get(g, float("nan"))
rel = abs(v - e) / abs(e)
worst = max(worst, rel)
rows.append({"gid": g, "recorded": e, "this_run": v, "rel_diff": rel})
agg = {k: {"recorded": exp[k], "this_run": summ[k], "rel_diff": abs(summ[k] - exp[k]) / abs(exp[k])} for k in ("mean", "sem")}
for r in rows:
print(f" gid {r['gid']:4d} recorded {r['recorded']:.6f} this run {r['this_run']:.6f} rel diff {r['rel_diff']:.2e}")
print(f" mean {summ['mean']:.6f} (recorded {exp['mean']:.6f}, rel diff {agg['mean']['rel_diff']:.2e}); "
f"SEM {summ['sem']:.6f} (recorded {exp['sem']:.6f}, rel diff {agg['sem']['rel_diff']:.2e})")
verdict = {"system": a.system, "model": u["key"], "max_rel_diff_per_condition": worst, "aggregate": agg, "per_condition": rows,
"bitwise_equal": all(r["rel_diff"] == 0.0 for r in rows) and all(v["rel_diff"] == 0.0 for v in agg.values())}
if a.tolerance is not None:
verdict["tolerance"] = a.tolerance
verdict["within_tolerance"] = worst <= a.tolerance and all(v["rel_diff"] <= a.tolerance for v in agg.values())
(out / "COMPARE.json").write_text(json.dumps(verdict, indent=1) + "\n")
print(f" max relative difference per condition: {worst:.3e}" + (f"; within {a.tolerance:g}: {verdict['within_tolerance']}" if a.tolerance is not None else ""))
return 1 if (a.tolerance is not None and not verdict["within_tolerance"]) else 0
def cmd_run(a) -> int:
cmd_generate(a)
a.samples = None
cmd_score(a)
return cmd_compare(a)
def main(argv=None) -> int:
ap = argparse.ArgumentParser(prog="python -m reproduce.table2", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("list")
def unit_args(p, need_data=True):
p.add_argument("--system", required=True, choices=SYSTEMS)
p.add_argument("--method", required=True, choices=("direct_si", "edm", "cascaded_si"))
p.add_argument("--S", type=int, default=None, help="depth of a Cascaded SI model (Table 6); default: the selected S*")
if need_data:
p.add_argument("--data", default=None, help="dataset package directory (default: $<SYSTEM>_DATA)")
p.add_argument("--truth-bank", default=None, help="truth-bank directory (default: $<SYSTEM>_TRUTH_BANK)")
c = sub.add_parser("check"); unit_args(c)
c.add_argument("--checkpoint"); c.add_argument("--samples")
c.add_argument("--hash-large", action="store_true", help="also hash the large files (trajectories, truth banks, samples)")
for name in ("generate", "run"):
g = sub.add_parser(name); unit_args(g)
g.add_argument("--checkpoint", required=True); g.add_argument("--out", required=True)
g.add_argument("--gids", default=None, help="comma-separated subset of the test conditions (split generation across jobs)")
g.add_argument("--device", default="cuda")
if name == "generate":
g.add_argument("--n-samples", type=int, default=None, help="fewer samples per condition (a smoke test; `score` refuses fewer than 10,000)")
if name == "run":
g.add_argument("--tolerance", type=float, default=None)
s = sub.add_parser("score"); unit_args(s)
s.add_argument("--out", required=True); s.add_argument("--samples", default=None, help="archived samples (default OUT/samples)")
s.add_argument("--device", default="cuda")
m = sub.add_parser("compare"); unit_args(m, need_data=False)
m.add_argument("--out", required=True); m.add_argument("--tolerance", type=float, default=None)
a = ap.parse_args(argv)
fn = {"list": cmd_list, "check": cmd_check, "generate": cmd_generate, "score": cmd_score, "compare": cmd_compare, "run": cmd_run}[a.cmd]
return int(fn(a) or 0)
if __name__ == "__main__":
sys.exit(main())
|