simcon-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simcon_toolkit/__init__.py +40 -0
- simcon_toolkit/__main__.py +7 -0
- simcon_toolkit/_kit/LICENSE +202 -0
- simcon_toolkit/_kit/NOTICE +37 -0
- simcon_toolkit/_kit/assets/parts/clip_frame.stl +0 -0
- simcon_toolkit/_kit/assets/parts/simple_plate.stl +0 -0
- simcon_toolkit/_kit/packages/.ruff.toml +10 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/__init__.py +8 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/auth.py +681 -0
- simcon_toolkit/_kit/packages/cadmould_cloud/client.py +235 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/__init__.py +5 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/mesh.py +210 -0
- simcon_toolkit/_kit/packages/cadmould_geometry/stl.py +168 -0
- simcon_toolkit/_kit/packages/cadmould_results/__init__.py +30 -0
- simcon_toolkit/_kit/packages/cadmould_results/loader.py +288 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/__init__.py +7 -0
- simcon_toolkit/_kit/packages/cadmould_scoring/metrics.py +519 -0
- simcon_toolkit/_kit/pyproject.toml +232 -0
- simcon_toolkit/_kit/templates/_shared/AGENTS.base.md +101 -0
- simcon_toolkit/_kit/templates/gate-study/.gitignore +18 -0
- simcon_toolkit/_kit/templates/gate-study/AGENTS.md +46 -0
- simcon_toolkit/_kit/templates/gate-study/GATING_STUDY_PLAYBOOK.md +219 -0
- simcon_toolkit/_kit/templates/gate-study/INITIAL_PROMPT.md +26 -0
- simcon_toolkit/_kit/templates/gate-study/README.md +137 -0
- simcon_toolkit/_kit/templates/gate-study/main.py +344 -0
- simcon_toolkit/_kit/templates/gate-study/pipeline.py +281 -0
- simcon_toolkit/_kit/templates/process-window/.gitignore +20 -0
- simcon_toolkit/_kit/templates/process-window/AGENTS.md +49 -0
- simcon_toolkit/_kit/templates/process-window/METHOD.md +155 -0
- simcon_toolkit/_kit/templates/process-window/README.md +176 -0
- simcon_toolkit/_kit/templates/process-window/configs/simple-plate.yaml +116 -0
- simcon_toolkit/_kit/templates/process-window/doe_spec.schema.md +249 -0
- simcon_toolkit/_kit/templates/process-window/main.py +82 -0
- simcon_toolkit/_kit/templates/process-window/process_window/__init__.py +5 -0
- simcon_toolkit/_kit/templates/process-window/process_window/centre.py +298 -0
- simcon_toolkit/_kit/templates/process-window/process_window/design.py +144 -0
- simcon_toolkit/_kit/templates/process-window/process_window/economics.py +367 -0
- simcon_toolkit/_kit/templates/process-window/process_window/emit.py +591 -0
- simcon_toolkit/_kit/templates/process-window/process_window/guardrails.py +153 -0
- simcon_toolkit/_kit/templates/process-window/process_window/harness.py +360 -0
- simcon_toolkit/_kit/templates/process-window/process_window/identity.py +92 -0
- simcon_toolkit/_kit/templates/process-window/process_window/inspect_part.py +184 -0
- simcon_toolkit/_kit/templates/process-window/process_window/kpis.py +355 -0
- simcon_toolkit/_kit/templates/process-window/process_window/material_card.py +163 -0
- simcon_toolkit/_kit/templates/process-window/process_window/probe_proxy.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_confirm.py +403 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_epsilon_floor.py +198 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_feedback.py +322 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_refine.py +279 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_screening.py +370 -0
- simcon_toolkit/_kit/templates/process-window/process_window/run_sweep.py +166 -0
- simcon_toolkit/_kit/templates/process-window/process_window/setup_campaign.py +312 -0
- simcon_toolkit/_kit/templates/process-window/process_window/surrogate.py +201 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_centre.py +169 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_design.py +113 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_guardrails.py +157 -0
- simcon_toolkit/_kit/templates/process-window/process_window/test_surrogate.py +127 -0
- simcon_toolkit/_kit/templates/process-window/process_window/units.py +152 -0
- simcon_toolkit/_kit/templates/quoting/.gitignore +24 -0
- simcon_toolkit/_kit/templates/quoting/AGENTS.md +58 -0
- simcon_toolkit/_kit/templates/quoting/INTERVIEW.md +147 -0
- simcon_toolkit/_kit/templates/quoting/METHOD.md +256 -0
- simcon_toolkit/_kit/templates/quoting/PROMPT.md +46 -0
- simcon_toolkit/_kit/templates/quoting/QUOTING_PLAYBOOK.md +245 -0
- simcon_toolkit/_kit/templates/quoting/README.md +158 -0
- simcon_toolkit/_kit/templates/quoting/main.py +484 -0
- simcon_toolkit/_kit/templates/quoting/parts/.gitkeep +0 -0
- simcon_toolkit/_kit/templates/quoting/quoting/__init__.py +11 -0
- simcon_toolkit/_kit/templates/quoting/quoting/costing.py +725 -0
- simcon_toolkit/_kit/templates/quoting/quoting/geometry.py +398 -0
- simcon_toolkit/_kit/templates/quoting/quoting/shop.py +193 -0
- simcon_toolkit/_kit/templates/quoting/quoting/state.py +260 -0
- simcon_toolkit/_kit/templates/quoting/quoting/study.py +577 -0
- simcon_toolkit/_kit/templates/quoting/quoting/toolkit.py +50 -0
- simcon_toolkit/_kit/templates/quoting/shop/README.md +43 -0
- simcon_toolkit/_kit/templates/quoting/shop/commercial.md +86 -0
- simcon_toolkit/_kit/templates/quoting/shop/lessons.md +94 -0
- simcon_toolkit/_kit/templates/quoting/shop/machines.md +68 -0
- simcon_toolkit/_kit/templates/quoting/shop/materials.md +92 -0
- simcon_toolkit/_kit/templates/quoting/shop/shop-profile.md +87 -0
- simcon_toolkit/_kit/templates/quoting/shop/tooling.md +145 -0
- simcon_toolkit/_kit/templates/run-one-simulation/.gitignore +16 -0
- simcon_toolkit/_kit/templates/run-one-simulation/AGENTS.md +41 -0
- simcon_toolkit/_kit/templates/run-one-simulation/README.md +133 -0
- simcon_toolkit/_kit/templates/run-one-simulation/main.py +216 -0
- simcon_toolkit/_kit/templates.toml +83 -0
- simcon_toolkit/choices.py +11 -0
- simcon_toolkit/cli.py +381 -0
- simcon_toolkit/generate.py +590 -0
- simcon_toolkit/instructions.py +152 -0
- simcon_toolkit/manifest.py +86 -0
- simcon_toolkit/project.py +356 -0
- simcon_toolkit/wizard.py +160 -0
- simcon_toolkit-0.1.0.dist-info/METADATA +48 -0
- simcon_toolkit-0.1.0.dist-info/RECORD +97 -0
- simcon_toolkit-0.1.0.dist-info/WHEEL +4 -0
- simcon_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""G5: emit `doe_spec.yaml` and `dossier.md`.
|
|
2
|
+
|
|
3
|
+
Assembles every stage's output into the two deliverables, and **runs the guardrail lint before
|
|
4
|
+
writing anything** — if a criterion thresholds an absolute pressure, or the noise floor was
|
|
5
|
+
imported rather than derived, or an absolute-valued claim carries no caveats, this fails and emits
|
|
6
|
+
nothing. A spec that violates the guardrails is worse than no spec.
|
|
7
|
+
|
|
8
|
+
The dossier records every decision, threshold, run id, rejection and caveat with timestamps, and
|
|
9
|
+
attaches the stated limitations automatically to any claim that rests on absolute values.
|
|
10
|
+
|
|
11
|
+
Run: python main.py emit configs/simple-plate.yaml
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import subprocess
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import yaml
|
|
24
|
+
|
|
25
|
+
from .guardrails import GuardrailViolation, lint_spec
|
|
26
|
+
from .run_screening import GATE_KPI, VARS, load_records
|
|
27
|
+
from .setup_campaign import dossier_dir_for, load_config, resolve_config, runs_dir_for, stage_dir_for
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
30
|
+
|
|
31
|
+
CAVEATS = {
|
|
32
|
+
"cavities_1_of_32": (
|
|
33
|
+
"1 of 32 cavities modelled, fed by a single gate. Verified from the geometry: the part is one connected body of 0.4444 cm3 and one gate was submitted to every run, so every result is per-cavity."
|
|
34
|
+
),
|
|
35
|
+
"no_runner_system": (
|
|
36
|
+
"No runner system of any kind is in the AI model, so the real tool's hot "
|
|
37
|
+
"runner is absent rather than approximated. Cavity pressures therefore "
|
|
38
|
+
"exclude every pressure drop upstream of the gate, the hot runner "
|
|
39
|
+
"contributes no heat, and cavity-to-cavity balance is not represented at "
|
|
40
|
+
"all -- with one cavity modelled there is not even a notion of it."
|
|
41
|
+
),
|
|
42
|
+
"no_process_description": "Almost no description of the real process beyond the material.",
|
|
43
|
+
"material_placeholder": (
|
|
44
|
+
"Material is PP Generic as a PLACEHOLDER; the real grade was not "
|
|
45
|
+
"supplied. The wide box, the no-flow gate and the pack/cool nominals "
|
|
46
|
+
"all derive from the material card, so all three move with it."
|
|
47
|
+
),
|
|
48
|
+
"multi_gate_pressure_underprediction": (
|
|
49
|
+
"The AI solver was measured to under-predict absolute cavity pressure on MULTI-GATE parts "
|
|
50
|
+
"(~0.3x on an 8-gate part). This model is single-gate, so that measured factor does not "
|
|
51
|
+
"apply to it directly and no bias figure is established for the single-gate case. The real "
|
|
52
|
+
"tool, however, is a 32-cavity hot-runner-fed system -- squarely the regime where the bias "
|
|
53
|
+
"was found -- so absolute pressures must not be trusted on transfer and the direction of "
|
|
54
|
+
"the discrepancy is unknown. Hence every criterion is a rank, ratio or comparison."
|
|
55
|
+
),
|
|
56
|
+
"weld_temp_circularity": (
|
|
57
|
+
"Weld-front temperature was reported to order points well (Spearman ~0.95, roughly constant "
|
|
58
|
+
"offset) but was tested against a melt perturbation, while melt drives that KPI ~1:1 — "
|
|
59
|
+
"measured here at an effect share of 0.97, so the correlation is substantially "
|
|
60
|
+
"self-fulfilling."
|
|
61
|
+
),
|
|
62
|
+
"fill_race_not_transferable": (
|
|
63
|
+
"Fill-race metrics were measured not to transfer (Spearman 0.10-0.23). Computed here as a "
|
|
64
|
+
"labelled anomaly detector only, never as a screening criterion."
|
|
65
|
+
),
|
|
66
|
+
"ai_always_fills": (
|
|
67
|
+
"The AI model fills completely on every run, so 'complete fill / short shot' cannot be the "
|
|
68
|
+
"binary feasibility gate the brief specifies. Every feasibility statement here is an "
|
|
69
|
+
"inferred thermal proxy."
|
|
70
|
+
),
|
|
71
|
+
"fake_real_is_not_physics": (
|
|
72
|
+
"The self-diagnosis ran against fake-real data generated by hidden input deviations on the "
|
|
73
|
+
"AI solver, so it measures sensitivity to input misspecification, not the AI-to-physics "
|
|
74
|
+
"offset."
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def git_commit() -> str:
|
|
80
|
+
try:
|
|
81
|
+
return (
|
|
82
|
+
subprocess.run(
|
|
83
|
+
["git", "rev-parse", "--short", "HEAD"], cwd=ROOT, capture_output=True, text=True, timeout=10
|
|
84
|
+
).stdout.strip()
|
|
85
|
+
or "unknown"
|
|
86
|
+
)
|
|
87
|
+
except Exception:
|
|
88
|
+
return "unknown"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load(p: Path):
|
|
92
|
+
return json.loads(p.read_text()) if p.exists() else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_spec(cfg: dict, stage_dir: Path, runs_dir: Path) -> tuple[dict, dict]:
|
|
96
|
+
floor = load(stage_dir / "noise_floor.json")
|
|
97
|
+
scr = load(stage_dir / "g2_screening.json")
|
|
98
|
+
ref = load(stage_dir / "g3_refinement.json")
|
|
99
|
+
con = load(stage_dir / "g4_confirmation.json")
|
|
100
|
+
fb = load(stage_dir / "g6_feedback.json")
|
|
101
|
+
sweep = load(stage_dir / "g1_sweep.json")
|
|
102
|
+
if not (floor and scr and ref):
|
|
103
|
+
missing = [n for n, x in (("noise_floor", floor), ("g2_screening", scr), ("g3_refinement", ref)) if not x]
|
|
104
|
+
raise SystemExit(f" missing stage outputs: {', '.join(missing)}")
|
|
105
|
+
|
|
106
|
+
recs = load_records(runs_dir, "G1_wide_sweep")
|
|
107
|
+
ref_recs = load_records(runs_dir, "G3_refinement")
|
|
108
|
+
eps_recs = load_records(runs_dir, "G1a_epsilon_floor")
|
|
109
|
+
|
|
110
|
+
is_ph = bool(cfg["material"].get("is_placeholder"))
|
|
111
|
+
caveat_keys = [
|
|
112
|
+
"cavities_1_of_32",
|
|
113
|
+
"no_runner_system",
|
|
114
|
+
"no_process_description",
|
|
115
|
+
"multi_gate_pressure_underprediction",
|
|
116
|
+
"weld_temp_circularity",
|
|
117
|
+
"fill_race_not_transferable",
|
|
118
|
+
"ai_always_fills",
|
|
119
|
+
]
|
|
120
|
+
if is_ph:
|
|
121
|
+
caveat_keys.insert(0, "material_placeholder")
|
|
122
|
+
if fb:
|
|
123
|
+
caveat_keys.append("fake_real_is_not_physics")
|
|
124
|
+
|
|
125
|
+
centre = ref["centre"]
|
|
126
|
+
rbox = ref["recommended_box"]
|
|
127
|
+
margins = ref["margin_variable_space"]
|
|
128
|
+
per_kpi = scr["per_kpi"]
|
|
129
|
+
|
|
130
|
+
def rank_of(v):
|
|
131
|
+
spans = {k: per_kpi[k]["effect_span"][v] for k in per_kpi if "effect_span" in per_kpi[k]}
|
|
132
|
+
return spans
|
|
133
|
+
|
|
134
|
+
factors, fixed = [], []
|
|
135
|
+
for v in VARS:
|
|
136
|
+
inert_all = scr["inert_across_all_kpis"].get(v, False)
|
|
137
|
+
gate_span = per_kpi.get(GATE_KPI, {}).get("effect_span", {}).get(v)
|
|
138
|
+
entry = {
|
|
139
|
+
"name": v,
|
|
140
|
+
"unit_engineer": "degC" if v.endswith("_C") else "cm3_s",
|
|
141
|
+
"centre": round(float(centre[v]), 4),
|
|
142
|
+
"low": round(float(rbox[v][0]), 4),
|
|
143
|
+
"high": round(float(rbox[v][1]), 4),
|
|
144
|
+
"bounds_basis": "derived",
|
|
145
|
+
"margin_applied": round(float(margins[v]), 4),
|
|
146
|
+
"effect_share_per_kpi": {k: per_kpi[k]["share"][v] for k in per_kpi if "share" in per_kpi[k]},
|
|
147
|
+
"effect_span_per_kpi": {k: per_kpi[k]["effect_span"][v] for k in per_kpi if "effect_span" in per_kpi[k]},
|
|
148
|
+
"gate_effect_span": gate_span,
|
|
149
|
+
"above_noise_floor_for_gate": (
|
|
150
|
+
None if gate_span is None else bool(gate_span >= floor["per_kpi"][GATE_KPI]["floor"])
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
if v.endswith("_C"):
|
|
154
|
+
entry["centre_K"] = round(float(centre[v]) + 273.15, 4)
|
|
155
|
+
entry["low_K"] = round(float(rbox[v][0]) + 273.15, 4)
|
|
156
|
+
entry["high_K"] = round(float(rbox[v][1]) + 273.15, 4)
|
|
157
|
+
if inert_all or (rbox[v][1] - rbox[v][0]) <= 0:
|
|
158
|
+
entry["role"] = "inert"
|
|
159
|
+
fixed.append(
|
|
160
|
+
{
|
|
161
|
+
"name": v,
|
|
162
|
+
"value": entry["centre"],
|
|
163
|
+
"reason": "inert" if inert_all else "no usable window",
|
|
164
|
+
"detail": ("effect below the derived floor for every KPI — fixed, not ranked low (guardrail 2)")
|
|
165
|
+
if inert_all
|
|
166
|
+
else "the safety margin consumed the available room",
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
else:
|
|
170
|
+
entry["role"] = "control"
|
|
171
|
+
factors.append(entry)
|
|
172
|
+
|
|
173
|
+
factors.sort(key=lambda e: -(e.get("gate_effect_span") or 0.0))
|
|
174
|
+
for i, e in enumerate(factors, 1):
|
|
175
|
+
e["rank"] = i
|
|
176
|
+
|
|
177
|
+
spec = {
|
|
178
|
+
"schema": "simcon.doe_spec/1",
|
|
179
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
180
|
+
"generated_by": f"frontloaded-doe POC, commit {git_commit()}",
|
|
181
|
+
"status": "draft",
|
|
182
|
+
"part": cfg["part"]["name"],
|
|
183
|
+
"centre_point": {
|
|
184
|
+
"definition": "chebyshev_centre_shrunk",
|
|
185
|
+
"note": (
|
|
186
|
+
"Deepest interior point of the green region. NOT the survivor mean, which on a "
|
|
187
|
+
"non-convex or disconnected region can fall outside the region entirely."
|
|
188
|
+
),
|
|
189
|
+
**{
|
|
190
|
+
v: {
|
|
191
|
+
"value": round(float(centre[v]), 4),
|
|
192
|
+
**({"value_K": round(float(centre[v]) + 273.15, 4)} if v.endswith("_C") else {}),
|
|
193
|
+
"basis": "derived",
|
|
194
|
+
}
|
|
195
|
+
for v in VARS
|
|
196
|
+
},
|
|
197
|
+
"inradius_normalised": ref["inradius_unit"],
|
|
198
|
+
"safety_margin": {v: round(float(margins[v]), 4) for v in VARS},
|
|
199
|
+
"margin_basis": ref.get("band_basis"),
|
|
200
|
+
"binding_constraint": ref.get("binding_constraint"),
|
|
201
|
+
"slack_at_centre_bands": ref.get("slack_at_centre_bands"),
|
|
202
|
+
},
|
|
203
|
+
"factors": factors,
|
|
204
|
+
"fixed_factors": fixed
|
|
205
|
+
+ [
|
|
206
|
+
{
|
|
207
|
+
"name": "htc_W_m2K",
|
|
208
|
+
"value": cfg["solver"]["htc_W_m2K"],
|
|
209
|
+
"reason": "out_of_scope",
|
|
210
|
+
"detail": "melt-to-mould contact property, not a machine setting; user directive",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"name": "gate_positions_mm",
|
|
214
|
+
"value": [list(map(float, g)) for g in load(runs_dir / "campaign_state.json")["gates_mm"]],
|
|
215
|
+
"reason": "tool_property",
|
|
216
|
+
"detail": "gate layout is tool design, not a machine parameter; fixed across the DOE",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
"readouts": [
|
|
220
|
+
{
|
|
221
|
+
"name": "switchover_point",
|
|
222
|
+
"unit": "phi",
|
|
223
|
+
"values": cfg["variables"]["switchover_readout_phi"],
|
|
224
|
+
"method": (
|
|
225
|
+
"field state read at fill fraction phi from runs already paid for; this is "
|
|
226
|
+
"the state AT switchover, not the effect OF switching there"
|
|
227
|
+
),
|
|
228
|
+
"caveats": [
|
|
229
|
+
"incompressible melt: the phi axis equates injected with filled volume, "
|
|
230
|
+
"sub-percent during fill but worst as phi -> 1, which is where switchover "
|
|
231
|
+
"lives"
|
|
232
|
+
],
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
"acceptance_criteria": scr["acceptance_criteria"],
|
|
236
|
+
"noise_floor": {
|
|
237
|
+
"method": floor["method"],
|
|
238
|
+
"mechanism": floor["mechanism"],
|
|
239
|
+
"perturbations": floor["perturbations"],
|
|
240
|
+
"justification": floor["justification"],
|
|
241
|
+
"per_kpi": floor["per_kpi"],
|
|
242
|
+
"applied_relatively": (
|
|
243
|
+
"the quantisation is floating-point, so the absolute floor "
|
|
244
|
+
"scales with the KPI value; the inert test uses the relative floor"
|
|
245
|
+
),
|
|
246
|
+
},
|
|
247
|
+
"feasibility": scr["feasibility"],
|
|
248
|
+
"feasibility_gate": {
|
|
249
|
+
"primary": GATE_KPI,
|
|
250
|
+
"why": (
|
|
251
|
+
"A short shot is what happens when the flow channel closes. The at-arrival "
|
|
252
|
+
"reading of the same KPI was measured NOT to discriminate on this part "
|
|
253
|
+
"(32.9-108.8 K against a 15 K gate) because fountain flow keeps the front hot."
|
|
254
|
+
),
|
|
255
|
+
"fill_completeness": scr.get("fill_completeness_diagnostic"),
|
|
256
|
+
},
|
|
257
|
+
"numerical_confirmation": (
|
|
258
|
+
{
|
|
259
|
+
"solver": "cadmould_numerical",
|
|
260
|
+
"mesh": "same_as_ai_runs",
|
|
261
|
+
"n_points": con["validation_summary"]["decks"],
|
|
262
|
+
"validated_by_round_trip": con["validation_summary"]["round_trip_ok"],
|
|
263
|
+
"with_packing": con["validation_summary"]["with_packing"],
|
|
264
|
+
"pack_nominal": con["pack_nominal"],
|
|
265
|
+
"cooling_nominal_s": con["cooling_nominal_s"],
|
|
266
|
+
"switchover_variants": con["switchover_variants"],
|
|
267
|
+
"rule": con["inward_only_rule"],
|
|
268
|
+
"status": "prepared, not submitted (scope: decks are the deliverable)",
|
|
269
|
+
"points": [
|
|
270
|
+
{
|
|
271
|
+
"id": p["id"],
|
|
272
|
+
"role": p["role"],
|
|
273
|
+
"melt_C": p["melt_C"],
|
|
274
|
+
"wall_C": p["wall_C"],
|
|
275
|
+
"flow_cm3_s": p["flow_cm3_s"],
|
|
276
|
+
"phi_s": p["phi_s"],
|
|
277
|
+
"hold_bar": p["hold_bar"],
|
|
278
|
+
"hold_s": p["hold_s"],
|
|
279
|
+
"cool_s": p["cool_s"],
|
|
280
|
+
"rm1": p["rm1"],
|
|
281
|
+
"validated": p["validation"].get("all_ok"),
|
|
282
|
+
"note": p["note"],
|
|
283
|
+
}
|
|
284
|
+
for p in con["points"]
|
|
285
|
+
],
|
|
286
|
+
}
|
|
287
|
+
if con
|
|
288
|
+
else {"status": "not built"}
|
|
289
|
+
),
|
|
290
|
+
"provenance": {
|
|
291
|
+
"part_geometry": {
|
|
292
|
+
"stl": cfg["part"]["stl"],
|
|
293
|
+
"cfex": (load(runs_dir / "campaign_state.json") or {}).get("cfex_name"),
|
|
294
|
+
"volume_cm3": (load(runs_dir / "campaign_state.json") or {}).get("volume_cm3"),
|
|
295
|
+
"geometry_id": (load(runs_dir / "campaign_state.json") or {}).get("geometry_id"),
|
|
296
|
+
},
|
|
297
|
+
"material": {
|
|
298
|
+
"plb": cfg["material"]["plb"],
|
|
299
|
+
"catalogue_name": cfg["material"]["catalogue_name"],
|
|
300
|
+
"is_placeholder": is_ph,
|
|
301
|
+
},
|
|
302
|
+
"machine_limits": {
|
|
303
|
+
**cfg["machine_limits"],
|
|
304
|
+
"absolute": True,
|
|
305
|
+
"caveats": ["cavities_1_of_32", "no_runner_system", "multi_gate_pressure_underprediction"],
|
|
306
|
+
},
|
|
307
|
+
"wide_box": {
|
|
308
|
+
v: {
|
|
309
|
+
"low": round(float(np.array(sweep["box"][v], float)[0]), 4),
|
|
310
|
+
"high": round(float(np.array(sweep["box"][v], float)[1]), 4),
|
|
311
|
+
"basis": sweep["box_basis"][v],
|
|
312
|
+
}
|
|
313
|
+
for v in VARS
|
|
314
|
+
}
|
|
315
|
+
if sweep
|
|
316
|
+
else None,
|
|
317
|
+
"ai_runs": {
|
|
318
|
+
# The provenance that matters is which endpoint answered, not a name for it.
|
|
319
|
+
"base_url": cfg["cloud"]["rest_base_url"],
|
|
320
|
+
"model_version": cfg["cloud"]["model_version"],
|
|
321
|
+
"num_timesteps": cfg["solver"]["num_timesteps"],
|
|
322
|
+
"stages": {
|
|
323
|
+
"G1a_epsilon_floor": len(eps_recs),
|
|
324
|
+
"G1_wide_sweep": len(recs),
|
|
325
|
+
"G3_refinement": len(ref_recs),
|
|
326
|
+
},
|
|
327
|
+
"total_scored": len(eps_recs) + len(recs) + len(ref_recs),
|
|
328
|
+
"determinism_note": (
|
|
329
|
+
"identical request returns an identical result; no replicates "
|
|
330
|
+
"were run because they carry zero information"
|
|
331
|
+
),
|
|
332
|
+
"storage_note": (
|
|
333
|
+
"raw results scored then discarded (~29 MB each); the solver is "
|
|
334
|
+
"deterministic so targeted re-runs are free"
|
|
335
|
+
),
|
|
336
|
+
},
|
|
337
|
+
"surrogate": ref.get("surrogates"),
|
|
338
|
+
},
|
|
339
|
+
"self_diagnosis": (
|
|
340
|
+
fb.get("self_diagnosis") | {"limitation": fb["limitation"], "source": fb["fake_real_source"]}
|
|
341
|
+
if fb
|
|
342
|
+
else {"status": "not run"}
|
|
343
|
+
),
|
|
344
|
+
"guardrail_report": {},
|
|
345
|
+
"caveats": {k: CAVEATS[k] for k in caveat_keys},
|
|
346
|
+
}
|
|
347
|
+
return spec, {
|
|
348
|
+
"scr": scr,
|
|
349
|
+
"ref": ref,
|
|
350
|
+
"con": con,
|
|
351
|
+
"fb": fb,
|
|
352
|
+
"floor": floor,
|
|
353
|
+
"sweep": sweep,
|
|
354
|
+
"n_sweep": len(recs),
|
|
355
|
+
"n_ref": len(ref_recs),
|
|
356
|
+
"n_eps": len(eps_recs),
|
|
357
|
+
"recs": recs,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def write_dossier(spec: dict, ctx: dict, out: Path, cfg: dict) -> None:
|
|
362
|
+
scr, ref, con, fb = ctx["scr"], ctx["ref"], ctx["con"], ctx["fb"]
|
|
363
|
+
L: list[str] = []
|
|
364
|
+
A = L.append
|
|
365
|
+
A(f"# DOE dossier — {spec['part']}")
|
|
366
|
+
A("")
|
|
367
|
+
A(f"Generated {spec['generated_at']} by {spec['generated_by']}.")
|
|
368
|
+
A(f"Status: **{spec['status']}**. Model `{cfg['cloud']['model_version']}` at `{cfg['cloud']['rest_base_url']}`.")
|
|
369
|
+
A("")
|
|
370
|
+
A(
|
|
371
|
+
"Beside this file: `doe_spec.yaml`, the machine-readable answer. The stage records it "
|
|
372
|
+
"was built from are under `cache/`, which is safe to delete."
|
|
373
|
+
)
|
|
374
|
+
A("")
|
|
375
|
+
|
|
376
|
+
A("## The answer")
|
|
377
|
+
A("")
|
|
378
|
+
A("| variable | low | centre | high | margin applied | rank |")
|
|
379
|
+
A("|---|---|---|---|---|---|")
|
|
380
|
+
for f in spec["factors"]:
|
|
381
|
+
A(f"| {f['name']} | {f['low']} | {f['centre']} | {f['high']} | {f['margin_applied']} | {f['rank']} |")
|
|
382
|
+
for f in spec["fixed_factors"]:
|
|
383
|
+
A(f"| {f['name']} *(fixed: {f['reason']})* | — | {f.get('value')} | — | — | — |")
|
|
384
|
+
A("")
|
|
385
|
+
cp = spec["centre_point"]
|
|
386
|
+
A(f"Centre point is the **{cp['definition']}** — {cp['note']}")
|
|
387
|
+
A("")
|
|
388
|
+
A(
|
|
389
|
+
f"Binding constraint at the centre: **{cp['binding_constraint']}**. "
|
|
390
|
+
f"Slack in units of each KPI's own resolution band: `{cp['slack_at_centre_bands']}`."
|
|
391
|
+
)
|
|
392
|
+
A("")
|
|
393
|
+
for n in ref.get("notes", []):
|
|
394
|
+
A(f"- {n}")
|
|
395
|
+
A("")
|
|
396
|
+
|
|
397
|
+
A("## What was excluded, and why")
|
|
398
|
+
A("")
|
|
399
|
+
fe = scr["feasibility"]
|
|
400
|
+
A(f"- **RED — freeze-off** (`{GATE_KPI}` < {fe['cuts']['no_flow_end_min_K']} K): {fe['red_freeze_off']} points")
|
|
401
|
+
A(f"- **AMBER — hotspot** worst decile (> {fe['cuts']['hotspot_cut']}): {fe['amber_hotspot']}")
|
|
402
|
+
A(
|
|
403
|
+
f"- **AMBER — pressure rank** worst decile (> {fe['cuts']['p80_cut_bar']} bar, "
|
|
404
|
+
f"ordering only): {fe['amber_pressure_rank']}"
|
|
405
|
+
)
|
|
406
|
+
A(f"- **GREEN**: {fe['green']} points ({100 * fe['green_fraction']:.1f}%)")
|
|
407
|
+
A("")
|
|
408
|
+
A(
|
|
409
|
+
f"Gate KPI observed over the sweep: {fe.get('gate_observed_K')} K. "
|
|
410
|
+
f"At-arrival secondary reading: {fe.get('at_arrival_observed_K')} K."
|
|
411
|
+
)
|
|
412
|
+
A("")
|
|
413
|
+
|
|
414
|
+
A("## Variable ranking and inert variables")
|
|
415
|
+
A("")
|
|
416
|
+
A("| KPI | LOO nrmse | floor applied | " + " | ".join(VARS) + " |")
|
|
417
|
+
A("|---|---|---|" + "---|" * len(VARS))
|
|
418
|
+
for k, d in scr["per_kpi"].items():
|
|
419
|
+
spans = " | ".join(f"{d['effect_span'][v]:.4g}{' **INERT**' if d['inert'][v] else ''}" for v in VARS)
|
|
420
|
+
A(f"| {k} | {100 * d['loo']['nrmse_range']:.2f}% | {d['floor_applied_here']:.4g} | {spans} |")
|
|
421
|
+
A("")
|
|
422
|
+
A(
|
|
423
|
+
"Effect spans are in each KPI's own units. A variable is declared **inert** — and fixed, not "
|
|
424
|
+
"ranked low — when its whole-box span falls below that KPI's derived resolution floor."
|
|
425
|
+
)
|
|
426
|
+
A("")
|
|
427
|
+
|
|
428
|
+
A("## Noise floor (derived for this part, never imported)")
|
|
429
|
+
A("")
|
|
430
|
+
nf = spec["noise_floor"]
|
|
431
|
+
A(f"{nf['mechanism']}")
|
|
432
|
+
A("")
|
|
433
|
+
A(f"Perturbations: `{nf['perturbations']}`. {nf['justification']}")
|
|
434
|
+
A("")
|
|
435
|
+
A("| KPI | floor | unit | relative | n |")
|
|
436
|
+
A("|---|---|---|---|---|")
|
|
437
|
+
for k, d in nf["per_kpi"].items():
|
|
438
|
+
if d.get("floor") is None:
|
|
439
|
+
continue
|
|
440
|
+
A(
|
|
441
|
+
f"| {k} | {d['floor']:.6g} | {d['unit']} | "
|
|
442
|
+
f"{d['rel_floor_pct'] if d['rel_floor_pct'] is not None else '—'} | {d['n_runs']} |"
|
|
443
|
+
)
|
|
444
|
+
A("")
|
|
445
|
+
A(f"**{nf['applied_relatively']}**")
|
|
446
|
+
A("")
|
|
447
|
+
|
|
448
|
+
A("## Acceptance criteria")
|
|
449
|
+
A("")
|
|
450
|
+
for c in spec["acceptance_criteria"]:
|
|
451
|
+
A(f"### `{c['name']}` — {c.get('role', '')}")
|
|
452
|
+
A("")
|
|
453
|
+
A(f"- expression: `{c['expression']}` (kind: {c['kind']}, absolute: {c['absolute']})")
|
|
454
|
+
A(f"- reasoning: {c['reasoning']}")
|
|
455
|
+
for extra in (
|
|
456
|
+
"why_kelvin_is_allowed",
|
|
457
|
+
"why_not_the_ratio",
|
|
458
|
+
"why_the_5th_percentile_not_the_minimum",
|
|
459
|
+
"why_this_replaced_the_at_arrival_reading",
|
|
460
|
+
):
|
|
461
|
+
if c.get(extra):
|
|
462
|
+
A(f"- {extra.replace('_', ' ')}: {c[extra]}")
|
|
463
|
+
if c.get("judgement"):
|
|
464
|
+
A(f"- **this is a judgement call.** What would change my mind: {c.get('would_change_my_mind')}")
|
|
465
|
+
A("")
|
|
466
|
+
|
|
467
|
+
A("## Numerical confirmation set")
|
|
468
|
+
A("")
|
|
469
|
+
if con:
|
|
470
|
+
nc = spec["numerical_confirmation"]
|
|
471
|
+
A(
|
|
472
|
+
f"{nc['n_points']} decks, {nc['validated_by_round_trip']} validated by "
|
|
473
|
+
f"`Rm1Writer` → `Rm1Reader` round trip, {nc['with_packing']} carrying the packing "
|
|
474
|
+
f"profile. Built on the **same mesh** as the AI runs. Status: {nc['status']}."
|
|
475
|
+
)
|
|
476
|
+
A("")
|
|
477
|
+
A(
|
|
478
|
+
f"Packing nominal from `tools.suggest_pack_profile` (peak "
|
|
479
|
+
f"{nc['pack_nominal']['peak_bar']:.0f} bar, {nc['pack_nominal']['total_s']:.2f} s); "
|
|
480
|
+
f"cooling {nc['cooling_nominal_s']['value']:.2f} s from `tools.suggest_cooling_time`. "
|
|
481
|
+
f"Switchover swept at {nc['switchover_variants']}."
|
|
482
|
+
)
|
|
483
|
+
A("")
|
|
484
|
+
A(f"**{nc['rule']}**")
|
|
485
|
+
A("")
|
|
486
|
+
A("| id | role | melt °C | wall °C | Q cm³/s | φ_s | validated | note |")
|
|
487
|
+
A("|---|---|---|---|---|---|---|---|")
|
|
488
|
+
for p in nc["points"]:
|
|
489
|
+
A(
|
|
490
|
+
f"| {p['id']} | {p['role']} | {p['melt_C']} | {p['wall_C']} | {p['flow_cm3_s']} | "
|
|
491
|
+
f"{p['phi_s']} | {'yes' if p['validated'] else 'NO'} | {p['note']} |"
|
|
492
|
+
)
|
|
493
|
+
else:
|
|
494
|
+
A("Not built.")
|
|
495
|
+
A("")
|
|
496
|
+
|
|
497
|
+
A("## Self-diagnosis")
|
|
498
|
+
A("")
|
|
499
|
+
if fb:
|
|
500
|
+
sd = spec["self_diagnosis"]
|
|
501
|
+
A(f"**Verdict: {sd['verdict'].upper()}** ({sd['basis']}).")
|
|
502
|
+
A("")
|
|
503
|
+
A(f"Thresholds: {sd['thresholds']}. Basis: {sd['threshold_basis']}.")
|
|
504
|
+
A("")
|
|
505
|
+
A(f"**Limitation:** {sd['limitation']}")
|
|
506
|
+
A("")
|
|
507
|
+
A("| deviation | KPI | mean offset | spread | in floors | verdict | ordering |")
|
|
508
|
+
A("|---|---|---|---|---|---|---|")
|
|
509
|
+
for dev, blk in fb["per_deviation"].items():
|
|
510
|
+
for k, d in blk["per_kpi"].items():
|
|
511
|
+
if "error" in d:
|
|
512
|
+
continue
|
|
513
|
+
A(
|
|
514
|
+
f"| {dev} | {k} | {d['mean_offset']:+.3f} | {d['spread']:.3f} | "
|
|
515
|
+
f"{d['spread_in_floors']:.2f} | {d['verdict']} | "
|
|
516
|
+
f"{d['ordering']['agreement']} |"
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
A("Not run.")
|
|
520
|
+
A("")
|
|
521
|
+
|
|
522
|
+
A("## Provenance")
|
|
523
|
+
A("")
|
|
524
|
+
pr = spec["provenance"]
|
|
525
|
+
ar = pr["ai_runs"]
|
|
526
|
+
A(f"- AI runs scored: **{ar['total_scored']}** — {ar['stages']}")
|
|
527
|
+
A(f"- {ar['determinism_note']}")
|
|
528
|
+
A(f"- {ar['storage_note']}")
|
|
529
|
+
A(
|
|
530
|
+
f"- material: `{pr['material']['catalogue_name']}`"
|
|
531
|
+
+ (" — **PLACEHOLDER**" if pr["material"]["is_placeholder"] else "")
|
|
532
|
+
)
|
|
533
|
+
A(f"- machine limits: {cfg['machine_limits']} — applied as annotation only, never as a screening criterion")
|
|
534
|
+
A("")
|
|
535
|
+
|
|
536
|
+
A("## Guardrail report")
|
|
537
|
+
A("")
|
|
538
|
+
for c in spec["guardrail_report"].get("checks_passed", []):
|
|
539
|
+
A(f"- PASS — {c}")
|
|
540
|
+
A("")
|
|
541
|
+
|
|
542
|
+
A("## Stated limitations (attached automatically to absolute-valued claims)")
|
|
543
|
+
A("")
|
|
544
|
+
for k, v in spec["caveats"].items():
|
|
545
|
+
A(f"- **{k}** — {v}")
|
|
546
|
+
A("")
|
|
547
|
+
|
|
548
|
+
out.write_text("\n".join(L), encoding="utf-8")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def main() -> int:
|
|
552
|
+
ap = argparse.ArgumentParser(description="G5 emit doe_spec.yaml + dossier.md")
|
|
553
|
+
ap.add_argument(
|
|
554
|
+
"config",
|
|
555
|
+
nargs="?",
|
|
556
|
+
help="campaign config; defaults to configs/simple-plate.yaml while that is unambiguous",
|
|
557
|
+
)
|
|
558
|
+
args = ap.parse_args()
|
|
559
|
+
|
|
560
|
+
cfg = load_config(resolve_config(args.config))
|
|
561
|
+
d = dossier_dir_for(cfg)
|
|
562
|
+
runs_dir = runs_dir_for(cfg)
|
|
563
|
+
spec, ctx = build_spec(cfg, stage_dir_for(cfg), runs_dir)
|
|
564
|
+
|
|
565
|
+
# Guardrails BEFORE writing. A spec that violates them must not exist.
|
|
566
|
+
try:
|
|
567
|
+
passed = lint_spec(spec)
|
|
568
|
+
except GuardrailViolation as exc:
|
|
569
|
+
print(f" GUARDRAIL VIOLATION — nothing emitted:\n {exc}")
|
|
570
|
+
return 1
|
|
571
|
+
spec["guardrail_report"] = {"checks_passed": passed}
|
|
572
|
+
print(f" guardrail lint passed ({len(passed)} checks):")
|
|
573
|
+
for p in passed:
|
|
574
|
+
print(f" - {p}")
|
|
575
|
+
|
|
576
|
+
yml = d / "doe_spec.yaml"
|
|
577
|
+
yml.write_text(yaml.safe_dump(spec, sort_keys=False, allow_unicode=True, width=100), encoding="utf-8")
|
|
578
|
+
print(f"\n wrote {yml.relative_to(ROOT)}")
|
|
579
|
+
|
|
580
|
+
md = d / "dossier.md"
|
|
581
|
+
write_dossier(spec, ctx, md, cfg)
|
|
582
|
+
print(f" wrote {md.relative_to(ROOT)}")
|
|
583
|
+
|
|
584
|
+
print("\n centre: " + ", ".join(f"{v}={spec['centre_point'][v]['value']}" for v in VARS))
|
|
585
|
+
print(" box: " + ", ".join(f"{f['name']} {f['low']}..{f['high']}" for f in spec["factors"]))
|
|
586
|
+
print(f" AI runs scored: {spec['provenance']['ai_runs']['total_scored']}")
|
|
587
|
+
return 0
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
if __name__ == "__main__":
|
|
591
|
+
raise SystemExit(main())
|