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,163 @@
|
|
|
1
|
+
"""The material card: processing window, no-flow temperature, and the wide box.
|
|
2
|
+
|
|
3
|
+
`ProcessTemperatures` on a `.plb` reads in **Kelvin** while the SDK's rm1 builder takes
|
|
4
|
+
**Celsius**. Both live inside the same SDK, so a value read from the card and handed to
|
|
5
|
+
`with_material(melt_temp=...)` is wrong by 273.15 K and still produces a valid-looking deck.
|
|
6
|
+
This module is the only place the card is read, it converts once, and it exposes both units
|
|
7
|
+
so nothing downstream has to know.
|
|
8
|
+
|
|
9
|
+
It also builds the wide box, because the box *is* a material property here: temperature is an
|
|
10
|
+
interval scale, so "±50 %" of it is not a well-defined instruction, and the card's own processing
|
|
11
|
+
window is the honest wide box.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import cadmould
|
|
20
|
+
from cadmould import material as _cm_material
|
|
21
|
+
|
|
22
|
+
from .units import K_to_degC, UnitError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Window:
|
|
27
|
+
"""One variable's bounds, in both units, with the nominal centre."""
|
|
28
|
+
|
|
29
|
+
lo_C: float
|
|
30
|
+
rec_C: float
|
|
31
|
+
hi_C: float
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def lo_K(self) -> float:
|
|
35
|
+
return self.lo_C + 273.15
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def rec_K(self) -> float:
|
|
39
|
+
return self.rec_C + 273.15
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def hi_K(self) -> float:
|
|
43
|
+
return self.hi_C + 273.15
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def span_K(self) -> float:
|
|
47
|
+
return self.hi_C - self.lo_C
|
|
48
|
+
|
|
49
|
+
def rel_span_of_rec(self, unit: str) -> float:
|
|
50
|
+
"""Half-width as a fraction of the nominal, in the given unit.
|
|
51
|
+
|
|
52
|
+
Reported in both units precisely because the answer differs — which is the whole
|
|
53
|
+
argument for not expressing a temperature box as a percentage.
|
|
54
|
+
"""
|
|
55
|
+
half = 0.5 * (self.hi_C - self.lo_C)
|
|
56
|
+
if unit == "degC":
|
|
57
|
+
return half / self.rec_C
|
|
58
|
+
if unit == "K":
|
|
59
|
+
return half / self.rec_K
|
|
60
|
+
raise UnitError(f"unit must be 'degC' or 'K', got {unit!r}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class MaterialCard:
|
|
65
|
+
name: str
|
|
66
|
+
group: int
|
|
67
|
+
manufacturer: str
|
|
68
|
+
melt: Window
|
|
69
|
+
mould: Window
|
|
70
|
+
no_flow_C: float
|
|
71
|
+
source: Path
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def no_flow_K(self) -> float:
|
|
75
|
+
return self.no_flow_C + 273.15
|
|
76
|
+
|
|
77
|
+
def describe(self) -> str:
|
|
78
|
+
return (
|
|
79
|
+
f"{self.name} ({self.manufacturer})\n"
|
|
80
|
+
f" melt {self.melt.lo_C:6.1f} .. {self.melt.rec_C:6.1f} .. {self.melt.hi_C:6.1f} degC"
|
|
81
|
+
f" (+/-{self.melt.rel_span_of_rec('degC') * 100:.1f}% in degC,"
|
|
82
|
+
f" +/-{self.melt.rel_span_of_rec('K') * 100:.1f}% in K)\n"
|
|
83
|
+
f" mould {self.mould.lo_C:6.1f} .. {self.mould.rec_C:6.1f} .. {self.mould.hi_C:6.1f} degC"
|
|
84
|
+
f" (+/-{self.mould.rel_span_of_rec('degC') * 100:.1f}% in degC,"
|
|
85
|
+
f" +/-{self.mould.rel_span_of_rec('K') * 100:.1f}% in K)\n"
|
|
86
|
+
f" no-flow {self.no_flow_C:.1f} degC ({self.no_flow_K:.2f} K)\n"
|
|
87
|
+
f" usable melt superheat: {self.melt.rec_C - self.no_flow_C:.1f} K above no-flow"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load(plb_path: str | Path, *, session_open: bool = False) -> MaterialCard:
|
|
92
|
+
"""Read a `.plb` material card. Opens a licence session unless one is already held."""
|
|
93
|
+
plb_path = Path(plb_path)
|
|
94
|
+
if not plb_path.exists():
|
|
95
|
+
# A .plb is a licensed Cadmould material file, so none can ship with this template.
|
|
96
|
+
# Say what to supply and where, rather than raising a bare path nobody can act on.
|
|
97
|
+
raise FileNotFoundError(
|
|
98
|
+
f"Material card not found: {plb_path}\n"
|
|
99
|
+
"A .plb is a licensed Cadmould material file and cannot be distributed with this\n"
|
|
100
|
+
"template, so you have to supply your own. Export one from Cadmould, then either\n"
|
|
101
|
+
f" - save it as {plb_path}, or\n"
|
|
102
|
+
" - point `material.plb` in your config at wherever you keep it.\n"
|
|
103
|
+
"The card sets the melt and wall windows the whole campaign is designed inside,\n"
|
|
104
|
+
"so setup reads it before any stage runs; there is no way to skip it."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _read():
|
|
108
|
+
m = _cm_material.io.PlbReader().load(str(plb_path))
|
|
109
|
+
t = m.process_temperatures
|
|
110
|
+
# The card is in Kelvin. K_to_degC range-checks the result, so a card that were
|
|
111
|
+
# actually in Celsius would fail here loudly instead of being silently accepted.
|
|
112
|
+
return MaterialCard(
|
|
113
|
+
name=str(m.name),
|
|
114
|
+
group=int(m.group),
|
|
115
|
+
manufacturer=str(m.manufacturer),
|
|
116
|
+
melt=Window(
|
|
117
|
+
K_to_degC(t.melt_min, what="melt min"),
|
|
118
|
+
K_to_degC(t.melt_rec, what="melt rec"),
|
|
119
|
+
K_to_degC(t.melt_max, what="melt max"),
|
|
120
|
+
),
|
|
121
|
+
mould=Window(
|
|
122
|
+
K_to_degC(t.mold_min, what="wall min"),
|
|
123
|
+
K_to_degC(t.mold_rec, what="wall rec"),
|
|
124
|
+
K_to_degC(t.mold_max, what="wall max"),
|
|
125
|
+
),
|
|
126
|
+
no_flow_C=K_to_degC(t.no_flow_temp, what="wall no-flow"),
|
|
127
|
+
source=plb_path,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if session_open:
|
|
131
|
+
return _read()
|
|
132
|
+
with cadmould.Session.user_based():
|
|
133
|
+
return _read()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def wide_box(card: MaterialCard, *, nominal_flow_cm3_s: float, flow_rel_span: float = 0.50) -> dict:
|
|
137
|
+
"""The G1 wide box: card window for temperatures, nominal ±rel_span for flow.
|
|
138
|
+
|
|
139
|
+
Temperatures get the card's full processing window rather than a percentage, because a
|
|
140
|
+
percentage of a temperature depends on whether it is written in K or °C (D3). Flow rate
|
|
141
|
+
is a ratio scale with a true zero, so ±50 % on it is unambiguous.
|
|
142
|
+
"""
|
|
143
|
+
return {
|
|
144
|
+
"melt_C": (card.melt.lo_C, card.melt.hi_C),
|
|
145
|
+
"wall_C": (card.mould.lo_C, card.mould.hi_C),
|
|
146
|
+
"flow_cm3_s": (nominal_flow_cm3_s * (1.0 - flow_rel_span), nominal_flow_cm3_s * (1.0 + flow_rel_span)),
|
|
147
|
+
"basis": {
|
|
148
|
+
"melt_C": "material_card",
|
|
149
|
+
"wall_C": "material_card",
|
|
150
|
+
"flow_cm3_s": f"nominal_ratio(+/-{flow_rel_span:.0%})",
|
|
151
|
+
},
|
|
152
|
+
"centre_C": {"melt_C": card.melt.rec_C, "wall_C": card.mould.rec_C, "flow_cm3_s": nominal_flow_cm3_s},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
import sys
|
|
158
|
+
|
|
159
|
+
# No default path: no .plb ships, so guessing one only produces a confusing miss.
|
|
160
|
+
if len(sys.argv) < 2:
|
|
161
|
+
raise SystemExit("usage: material_card.py <path-to-your.plb>")
|
|
162
|
+
card = load(sys.argv[1])
|
|
163
|
+
print(card.describe())
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Which no-flow proxy actually discriminates? Test on box corners before re-running 1 024 runs.
|
|
2
|
+
|
|
3
|
+
F15 defined a trigger: if the at-arrival no-flow margin never approaches the gate, the proxy is
|
|
4
|
+
too weak and a channel-closure reading should replace it. That trigger has fired (minimum margin
|
|
5
|
+
32.9 K against a 15 K gate over 250 runs). Adding a KPI means re-scoring, and raw results are
|
|
6
|
+
discarded by policy, so it costs a full re-run of the sweep.
|
|
7
|
+
|
|
8
|
+
So test it first, on the handful of points where the proxies must differ most: the extreme corners
|
|
9
|
+
of the box. Six runs, raw results kept, all candidate readings computed side by side. Cheap, and it
|
|
10
|
+
answers the question instead of assuming it.
|
|
11
|
+
|
|
12
|
+
Candidate readings, all forms of "distance to the material's no-flow temperature":
|
|
13
|
+
A core T at ARRIVAL, min over nodes - the current KPI
|
|
14
|
+
B core T at END OF FILL, min over nodes - channel closure, but extremum-fragile
|
|
15
|
+
C core T at END OF FILL, 5th percentile - channel closure, robust
|
|
16
|
+
D core T at END OF FILL, 1st percentile - between B and C
|
|
17
|
+
E fraction of filled nodes below no-flow - a count, not a distance (scope-flagged)
|
|
18
|
+
|
|
19
|
+
Run: python main.py probe configs/simple-plate.yaml
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import h5py
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
from . import kpis as K
|
|
32
|
+
from .harness import RunSpec
|
|
33
|
+
from .setup_campaign import build, resolve_config
|
|
34
|
+
|
|
35
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
36
|
+
|
|
37
|
+
STAGE = "G1b_proxy_probe"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def readings(h5: Path, no_flow_K: float) -> dict:
|
|
41
|
+
with h5py.File(h5, "r") as h:
|
|
42
|
+
steps = K._steps(h)
|
|
43
|
+
_t, arr, reached, _f = K._fill_state(h)
|
|
44
|
+
core = K._core_temp_series(h, steps)
|
|
45
|
+
filled = arr >= 0
|
|
46
|
+
idx = np.where(filled)[0]
|
|
47
|
+
at_arr = core[arr[filled], idx]
|
|
48
|
+
at_arr = at_arr[np.isfinite(at_arr)]
|
|
49
|
+
end = core[-1][filled]
|
|
50
|
+
end = end[np.isfinite(end)]
|
|
51
|
+
|
|
52
|
+
worst = np.full(reached.size, np.inf)
|
|
53
|
+
for i in range(len(steps)):
|
|
54
|
+
m = (arr >= 0) & (arr <= i)
|
|
55
|
+
worst[m] = np.minimum(worst[m], core[i][m])
|
|
56
|
+
w = worst[np.isfinite(worst)]
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"reached_pct": round(100.0 * float(reached.mean()), 3),
|
|
60
|
+
"A_arrival_min_K": round(float(at_arr.min()) - no_flow_K, 2),
|
|
61
|
+
"B_end_min_K": round(float(end.min()) - no_flow_K, 2),
|
|
62
|
+
"C_end_p5_K": round(float(np.percentile(end, 5)) - no_flow_K, 2),
|
|
63
|
+
"D_end_p1_K": round(float(np.percentile(end, 1)) - no_flow_K, 2),
|
|
64
|
+
"E_frac_below_nf_pct": round(100.0 * float((end < no_flow_K).mean()), 4),
|
|
65
|
+
"F_worst_min_K": round(float(w.min()) - no_flow_K, 2),
|
|
66
|
+
"G_worst_p1_K": round(float(np.percentile(w, 1)) - no_flow_K, 2),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def main() -> int:
|
|
71
|
+
ap = argparse.ArgumentParser(description="G1b feasibility-proxy probe")
|
|
72
|
+
ap.add_argument(
|
|
73
|
+
"config",
|
|
74
|
+
nargs="?",
|
|
75
|
+
help="campaign config; defaults to configs/simple-plate.yaml while that is unambiguous",
|
|
76
|
+
)
|
|
77
|
+
args = ap.parse_args()
|
|
78
|
+
cfg_path = resolve_config(args.config)
|
|
79
|
+
campaign, info = build(cfg_path)
|
|
80
|
+
try:
|
|
81
|
+
box = info["box"]
|
|
82
|
+
m_lo, m_hi = box["melt_C"]
|
|
83
|
+
w_lo, w_hi = box["wall_C"]
|
|
84
|
+
q_lo, q_hi = box["flow_cm3_s"]
|
|
85
|
+
nf = info["card"].no_flow_K
|
|
86
|
+
|
|
87
|
+
# Corners chosen so the proxies must separate if they separate at all: the coldest and
|
|
88
|
+
# slowest combination is the freeze-off worst case, the hottest and fastest the best.
|
|
89
|
+
pts = [
|
|
90
|
+
("worst cold+cold+slow", m_lo, w_lo, q_lo),
|
|
91
|
+
("cold melt, slow ", m_lo, w_hi, q_lo),
|
|
92
|
+
("cold mould, slow ", m_hi, w_lo, q_lo),
|
|
93
|
+
("nominal-ish ", (m_lo + m_hi) / 2, (w_lo + w_hi) / 2, (q_lo + q_hi) / 2),
|
|
94
|
+
("hot, fast ", m_hi, w_hi, q_hi),
|
|
95
|
+
("best hot+hot+fast ", m_hi, w_hi, q_hi * 1.0),
|
|
96
|
+
]
|
|
97
|
+
seen, specs = set(), []
|
|
98
|
+
for label, m, w, q in pts:
|
|
99
|
+
key = (round(m, 4), round(w, 4), round(q, 4))
|
|
100
|
+
if key in seen:
|
|
101
|
+
continue
|
|
102
|
+
seen.add(key)
|
|
103
|
+
specs.append(
|
|
104
|
+
RunSpec(
|
|
105
|
+
melt_C=float(m),
|
|
106
|
+
wall_C=float(w),
|
|
107
|
+
flow_cm3_s=round(float(q), 4),
|
|
108
|
+
stage=STAGE,
|
|
109
|
+
label=label.strip(),
|
|
110
|
+
keep_h5=True,
|
|
111
|
+
meta={"probe": label.strip()},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
print(f"\n {len(specs)} corner probes, raw results KEPT so all readings can be compared")
|
|
116
|
+
recs = campaign.run_batch(
|
|
117
|
+
specs,
|
|
118
|
+
stage=STAGE,
|
|
119
|
+
max_workers=3,
|
|
120
|
+
description="Proxy probe: which no-flow reading discriminates at the box corners (F15 trigger).",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
rows = []
|
|
124
|
+
for r in recs:
|
|
125
|
+
if not r.ok or not r.h5_kept:
|
|
126
|
+
print(f" {r.spec.get('label')}: no kept h5 ({r.error or 'n/a'})")
|
|
127
|
+
continue
|
|
128
|
+
d = readings(info["runs_dir"] / r.h5_kept, nf)
|
|
129
|
+
d["label"] = r.spec["label"]
|
|
130
|
+
d["melt_C"] = r.spec["melt_C"]
|
|
131
|
+
d["wall_C"] = r.spec["wall_C"]
|
|
132
|
+
d["flow_cm3_s"] = r.spec["flow_cm3_s"]
|
|
133
|
+
d["fill_time_s"] = round(info["volume_cm3"] / r.spec["flow_cm3_s"], 4)
|
|
134
|
+
rows.append(d)
|
|
135
|
+
|
|
136
|
+
keys = [
|
|
137
|
+
"A_arrival_min_K",
|
|
138
|
+
"B_end_min_K",
|
|
139
|
+
"C_end_p5_K",
|
|
140
|
+
"D_end_p1_K",
|
|
141
|
+
"F_worst_min_K",
|
|
142
|
+
"G_worst_p1_K",
|
|
143
|
+
"E_frac_below_nf_pct",
|
|
144
|
+
]
|
|
145
|
+
print(f"\n {'point':22s} {'t_fill':>7s} " + " ".join(f"{k.split('_')[0]:>7s}" for k in keys))
|
|
146
|
+
for d in sorted(rows, key=lambda x: x["flow_cm3_s"]):
|
|
147
|
+
print(f" {d['label'][:22]:22s} {d['fill_time_s']:7.3f} " + " ".join(f"{d[k]:7.1f}" for k in keys))
|
|
148
|
+
|
|
149
|
+
print("\n discrimination (max - min across probes; bigger = more useful):")
|
|
150
|
+
floor = json.loads((info["stage_dir"] / "noise_floor.json").read_text())["per_kpi"]
|
|
151
|
+
f_gate = floor["no_flow_margin_K"]["floor"]
|
|
152
|
+
for k in keys:
|
|
153
|
+
v = np.array([d[k] for d in rows], float)
|
|
154
|
+
rng = float(v.max() - v.min())
|
|
155
|
+
unit = "%" if k.startswith("E") else "K"
|
|
156
|
+
extra = "" if k.startswith("E") else f" = {rng / f_gate:5.1f} x floor"
|
|
157
|
+
print(f" {k:22s} {v.min():8.2f} .. {v.max():8.2f} {unit} range {rng:7.2f}{extra}")
|
|
158
|
+
|
|
159
|
+
(info["stage_dir"] / "proxy_probe.json").write_text(
|
|
160
|
+
json.dumps({"no_flow_K": nf, "floor_no_flow_margin_K": f_gate, "probes": rows}, indent=2)
|
|
161
|
+
)
|
|
162
|
+
print(f"\n wrote {(info['stage_dir'] / 'proxy_probe.json').relative_to(ROOT)}")
|
|
163
|
+
finally:
|
|
164
|
+
info["api"].close()
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
raise SystemExit(main())
|