246 """A RadClusterSimulation configured from one manifest entry."""
247 from radcluster_code.py_utils.simulation
import RadClusterSimulation
248 sim = RadClusterSimulation(
249 I=e[
"I"], V=e[
"V"], solver_mode=e[
"solver_mode"],
250 equations=e[
"equations"], cascade=e[
"cascade"],
251 he_kinetics=e[
"he_kinetics"],
252 i_mobile=e[
"i_mobile"], v_mobile=e[
"v_mobile"])
258 import run_ensemble
as _re
259 _re.apply_bin_config(sim, {
260 "equations": e[
"equations"],
261 "i_discrete": e.get(
"i_discrete", 0),
"v_discrete": e.get(
"v_discrete", 0),
262 "I_bin": e.get(
"I_bin", 0),
"V_bin": e.get(
"V_bin", 0),
263 "shape_function": e[
"shape_function"]})
264 sim.input_data.reactions[
"LOOP_NETWORK_LOSS"] = int(e.get(
"lnl", 1))
267 sim.input_data.reactions[
"LOOP_COAL"] = int(e.get(
"loop_coal", 1))
268 sim.input_data._calculate_derived()
319def execute(e: dict, log: ProgressLog, save_plots=
True) -> dict:
320 """Run one entry to completion. Returns the record to publish."""
321 import run_ensemble
as re_mod
323 rec = {
"run_id": e[
"run_id"],
"table": e[
"table"],
"label": e[
"label"],
324 "machine": log.machine}
326 if e.get(
"omp_threads"):
327 os.environ[
"OMP_NUM_THREADS"] = str(e[
"omp_threads"])
330 sim.run_tag = e[
"run_id"]
331 G = float(sim.input_data.reactions[
"G"])
335 cfg = {
"I": e[
"I"],
"V": e[
"V"],
"dose": float(e[
"dose"]),
336 "C_floor": float(sim.input_data.reactions.get(
"C_floor", 1e-15)),
337 "equations": e[
"equations"],
338 "i_discrete": e.get(
"i_discrete", 0),
"I_bin": e.get(
"I_bin", 0),
339 "v_discrete": e.get(
"v_discrete", 0),
"V_bin": e.get(
"V_bin", 0),
340 "shape_function": e[
"shape_function"]}
346 if e[
"equations"] ==
"bin_moment":
347 rec.update(re_mod.bin_layout(sim, cfg))
354 _re_obj = getattr(sim,
"rate_equations",
None)
355 rec[
"N_eq"] = int(getattr(_re_obj,
"N_eq", -1))
if _re_obj
else -1
356 rec[
"bin_shape"] =
"discrete"
358 log.render(state=
"starting")
359 ckpt = MOD /
"output" / f
"CKPT_{e['run_id']}"
360 log.start_heartbeat(sim=sim, ckpt_dir=ckpt)
361 log.event(
"start", **{k: e.get(k)
for k
in
362 (
"table",
"label",
"equations",
"I",
"V",
"dose",
363 "i_discrete",
"I_bin",
"v_discrete",
"V_bin",
364 "shape_function",
"rtol",
"preconditioner",
365 "he_kinetics",
"cascade",
"omp_threads")})
370 saved = sys.stdout, sys.stderr
372 sys.stdout = sys.stderr = buf
373 if int(e.get(
"lnl", 1)):
379 res = sim.run_adaptive(solver_config=scfg, save_output=save_plots,
380 progress_callback=log.tick,
381 timeout_s=scfg[
"timeout_s"], max_doublings=0)
383 res = sim.run(solver_config=scfg, save_output=save_plots,
384 progress_callback=log.tick)
386 sys.stdout, sys.stderr = saved
390 rec.update({
"status":
"failed",
"error":
"solver returned None"})
391 rec[
"wall_s"] = round(time.time() - t0, 1)
396 rec.update(re_mod.observe(res, sim, cfg, 1.0))
397 rec[
"loop_coal"] = int(e.get(
"loop_coal", 1))
398 rec[
"status"] =
"done"
399 rec[
"wall_s"] = round(time.time() - t0, 1)
400 rec[
"dose_target"] = float(e[
"dose"])
405 reads = e[
"dose_read"]
406 reads = reads
if isinstance(reads, (list, tuple))
else [reads]
407 lad = rec.get(
"at_dose")
or {}
410 key = f
"{float(target):g}"
412 rec[
"scored"][key] = got
if got
else {
"missing":
True,
413 "dose_reached": rec.get(
"dose_reached")}
420 rec[
"reached_all_scoring_doses"] = all(
421 not v.get(
"missing")
and not v.get(
"off_grid")
422 for v
in rec[
"scored"].values())
424 out_dir = getattr(sim,
"_last_output_dir",
None)
426 rec[
"output_dir"] = str(out_dir)
450 ap = argparse.ArgumentParser(description=__doc__,
451 formatter_class=argparse.RawDescriptionHelpFormatter)
452 ap.add_argument(
"--run", help=
"run this run_id and stop")
453 ap.add_argument(
"--loop", action=
"store_true",
454 help=
"keep claiming runs until the board is empty")
455 ap.add_argument(
"--status", action=
"store_true", help=
"print the board")
456 ap.add_argument(
"--list", action=
"store_true", help=
"print the manifest")
457 ap.add_argument(
"--release", help=
"drop a claim so another machine can take it")
458 ap.add_argument(
"--branch", default=
"campaign-verification")
459 ap.add_argument(
"--only", default=
None,
460 help=
"restrict to one or more tables, comma separated, "
461 "e.g. --only T4 or --only T4,T1,T2,T3")
462 ap.add_argument(
"--offline", action=
"store_true",
463 help=
"no fetch/push; claims stay local (smoke tests)")
464 ap.add_argument(
"--no-plots", action=
"store_true",
465 help=
"skip the output directory entirely (diagnostics only)")
466 ap.add_argument(
"--omp-threads", type=int, default=0,
467 help=
"override the manifest omp_threads to match this "
468 "machine (0 = use the manifest value). Never applied "
469 "to T5, where thread count is the measured variable.")
470 ap.add_argument(
"--max-runs", type=int, default=0,
471 help=
"stop after N runs (0 = no limit)")
472 ap.add_argument(
"--smoke", action=
"store_true",
473 help=
"tiny end-to-end run (seconds) exercising the real "
474 "code path: output directory, live log, claim, sync")
475 a = ap.parse_args(argv)
477 entries = {e[
"run_id"]: e
for e
in manifest_mod.manifest()}
478 _tables = {t.strip()
for t
in (a.only
or "").split(
",")
if t.strip()}
479 order = [e[
"run_id"]
for e
in manifest_mod.runnable(tables=_tables)]
484 smoke = dict(manifest_mod.BASE)
486 "run_id":
"SMOKE",
"table":
"--",
"label":
"pipeline smoke test",
487 "I": 200,
"V": 100,
"i_discrete": 10,
"I_bin": 6,
488 "v_discrete": 5,
"V_bin": 6,
493 "dose": 1e-2,
"n_points": 10,
"dose_read": 0.005,
494 "timeout_s": 600,
"omp_threads": 2,
496 entries[
"SMOKE"] = smoke
500 want = {t.strip()
for t
in a.only.split(
",")
if t.strip()}
501 unknown = want - {e[
"table"]
for e
in entries.values()}
503 ap.error(f
"--only: no such table(s): {','.join(sorted(unknown))}")
504 order = [r
for r
in order
if entries[r][
"table"]
in want]
511 for e
in entries.values():
512 if e[
"table"] !=
"T5":
513 e[
"omp_threads"] = a.omp_threads
516 manifest_mod.__dict__[
"__name__"] =
"__main__"
517 os.system(f
"{sys.executable} {HERE/'runs.py'}")
520 sync =
CampaignSync(REPO, branch=a.branch, offline=a.offline)
524 ok = sync.release(a.release)
525 print(f
"release {a.release}: {'ok' if ok else 'nothing to release'}")
532 machine = machine_id()
535 rid = a.run
if a.run
else sync.next_unclaimed(order)
537 print(
"board empty — every run is claimed or finished.")
539 if rid
not in entries:
540 print(f
"unknown run_id {rid!r}", file=sys.stderr)
547 print(f
"\n=== SMOKE (pipeline test, not published) on {machine} ===",
550 rec =
execute(e, log, save_plots=
not a.no_plots)
551 log.render(state=rec.get(
"status",
"?").upper(), extra=
summary_line(rec))
552 print(f
" {summary_line(rec)}", flush=
True)
553 print(
" board not touched (smoke)", flush=
True)
555 if not sync.claim(rid, meta={
"table": e[
"table"],
"label": e[
"label"]}):
556 print(f
" {rid}: claimed by another machine, skipping")
561 print(f
"\n=== {rid} ({e['table']} / {e['label']}) on {machine} ===",
565 rec =
execute(e, log, save_plots=
not a.no_plots)
566 except BaseException
as exc:
567 rec = {
"run_id": rid,
"table": e[
"table"],
"label": e[
"label"],
568 "status":
"failed",
"machine": machine,
569 "error": f
"{type(exc).__name__}: {exc}"[:300],
570 "traceback_tail": traceback.format_exc()[-800:]}
571 log.event(
"error", error=rec[
"error"])
572 log.render(state=
"FAILED", extra=rec[
"error"])
573 if isinstance(exc, KeyboardInterrupt):
577 log.event(
"end", status=rec.get(
"status"),
578 wall_s=rec.get(
"wall_s"),
579 dose_reached=rec.get(
"dose_reached"))
580 log.render(state=rec.get(
"status",
"?").upper(), extra=
summary_line(rec))
581 print(f
" {summary_line(rec)}", flush=
True)
585 print(f
" synced to {a.branch}: {'yes' if pushed else 'NO (will retry next run)'}",
589 if a.run
or (a.max_runs
and done_count >= a.max_runs)
or not a.loop: