STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
layer1_sweep.py
Go to the documentation of this file.
1"""Run Layer 1 on many cases at once and collect the results.
2
3 python3 scripts/layer1_sweep.py cases.json --out runs/ --parallel 3 --workers 3
4
5``cases.json`` is a list of cases::
6
7 {"name": "6500N_seed1", "config": "configs/ethalox_6500N.yaml",
8 "set": {"design_requirements.layer1_random_seed": 1, "design_requirements.target_thrust": 6500}}
9
10``config`` may also be ``"blank:impinging:ethalox"``: the canonical impinging config with the
11ethalox preset switched in, exactly what a user gets from a new design in the UI.
12
13Each case runs ``scripts/layer1_run.py`` in its own process (``ED_L1_WORKERS`` = --workers), so
14a crash or hang in one case cannot take the sweep down. Writes, per case, ``<name>.json`` (summary),
15``<name>_config.yaml`` (the optimised design), ``<name>.log``, and one ``summary.json`` for all.
16"""
17from __future__ import annotations
18
19import argparse
20import copy
21import json
22import os
23import subprocess
24import sys
25import time
26from concurrent.futures import ThreadPoolExecutor, as_completed
27from pathlib import Path
28
29import yaml
30
31ROOT = Path(__file__).resolve().parents[1]
32sys.path.insert(0, str(ROOT))
33
34
35def _set(d: dict, dotted: str, value) -> None:
36 keys = dotted.split(".")
37 for k in keys[:-1]:
38 if d.get(k) is None:
39 d[k] = {}
40 d = d[k]
41 d[keys[-1]] = value
42
43
44def blank_config(injector: str, propellant: str) -> dict:
45 """What the UI hands a user who starts a new design: the canonical config for the injector
46 type, with the propellant preset switched in."""
47 from engine.pipeline.config_switch import switch_config
48 base = yaml.safe_load((ROOT / "configs" / "canonical" / f"{injector}.yaml").read_text())
49 return switch_config(base, injector_type=None, propellant_preset=propellant)
50
51
52def build_case_config(case: dict, out_dir: Path) -> Path:
53 src = case["config"]
54 if src.startswith("blank:"):
55 _, inj, prop = src.split(":")
56 cfg = blank_config(inj, prop)
57 else:
58 cfg = yaml.safe_load((ROOT / src).read_text() if not Path(src).is_absolute() else Path(src).read_text())
59 cfg = copy.deepcopy(cfg)
60 for k, v in (case.get("set") or {}).items():
61 _set(cfg, k, v)
62 p = out_dir / f"{case['name']}_input.yaml"
63 p.write_text(yaml.safe_dump(cfg, sort_keys=False))
64 return p
65
66
67def run_case(case: dict, out_dir: Path, workers: int, timeout: int) -> dict:
68 name = case["name"]
69 t0 = time.time()
70 try:
71 cfg_path = build_case_config(case, out_dir)
72 except Exception as e: # a case that cannot even be built is a result, not a crash
73 return {"name": name, "status": "build_failed", "error": repr(e)}
74 env = dict(os.environ, ED_L1_WORKERS=str(workers),
75 PYTHONPATH=str(ROOT.parent / "lib" / "stardesign") + os.pathsep + os.environ.get("PYTHONPATH", ""),
76 PYTHONWARNINGS="ignore")
77 out_json = out_dir / f"{name}.json"
78 log = out_dir / f"{name}.log"
79 try:
80 with open(log, "w") as fh:
81 proc = subprocess.run(
82 [sys.executable, "-W", "ignore", str(ROOT / "scripts" / "layer1_run.py"),
83 "--config", str(cfg_path), "--out", str(out_json), "--label", name],
84 cwd=str(ROOT), env=env, stdout=fh, stderr=subprocess.STDOUT, timeout=timeout)
85 status = "ok" if proc.returncode == 0 else f"exit_{proc.returncode}"
86 except subprocess.TimeoutExpired:
87 status = "timeout"
88 res = {"name": name, "status": status, "seconds": round(time.time() - t0, 1), "case": case}
89 if out_json.exists():
90 try:
91 res["result"] = json.loads(out_json.read_text())
92 except Exception as e:
93 res["result_error"] = repr(e)
94 if status != "ok":
95 res["log_tail"] = log.read_text()[-3000:] if log.exists() else ""
96 return res
97
98
99def main() -> None:
100 ap = argparse.ArgumentParser()
101 ap.add_argument("cases")
102 ap.add_argument("--out", required=True)
103 ap.add_argument("--parallel", type=int, default=3)
104 ap.add_argument("--workers", type=int, default=3)
105 ap.add_argument("--timeout", type=int, default=1800)
106 a = ap.parse_args()
107 cases = json.loads(Path(a.cases).read_text())
108 out = Path(a.out)
109 out.mkdir(parents=True, exist_ok=True)
110 results = []
111 summary_path = out / "summary.json"
112 with ThreadPoolExecutor(max_workers=a.parallel) as ex:
113 futs = {ex.submit(run_case, c, out, a.workers, a.timeout): c for c in cases}
114 for f in as_completed(futs):
115 r = f.result()
116 results.append(r)
117 rr = r.get("result") or {}
118 print(f"[{len(results)}/{len(cases)}] {r['name']}: {r['status']} {r.get('seconds')}s "
119 f"F={rr.get('F')} O/F={rr.get('MR')} Pc={rr.get('Pc_psi')} Isp={rr.get('Isp')} "
120 f"fail={rr.get('failure_reasons')}", flush=True)
121 summary_path.write_text(json.dumps(results, indent=1, default=str))
122 print(f"wrote {summary_path}")
123
124
125if __name__ == "__main__":
126 main()
dict blank_config(str injector, str propellant)
Path build_case_config(dict case, Path out_dir)
None _set(dict d, str dotted, value)
dict run_case(dict case, Path out_dir, int workers, int timeout)
TOML_NODISCARD_CTOR time() noexcept=default