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.
Files changed (97) hide show
  1. simcon_toolkit/__init__.py +40 -0
  2. simcon_toolkit/__main__.py +7 -0
  3. simcon_toolkit/_kit/LICENSE +202 -0
  4. simcon_toolkit/_kit/NOTICE +37 -0
  5. simcon_toolkit/_kit/assets/parts/clip_frame.stl +0 -0
  6. simcon_toolkit/_kit/assets/parts/simple_plate.stl +0 -0
  7. simcon_toolkit/_kit/packages/.ruff.toml +10 -0
  8. simcon_toolkit/_kit/packages/cadmould_cloud/__init__.py +8 -0
  9. simcon_toolkit/_kit/packages/cadmould_cloud/auth.py +681 -0
  10. simcon_toolkit/_kit/packages/cadmould_cloud/client.py +235 -0
  11. simcon_toolkit/_kit/packages/cadmould_geometry/__init__.py +5 -0
  12. simcon_toolkit/_kit/packages/cadmould_geometry/mesh.py +210 -0
  13. simcon_toolkit/_kit/packages/cadmould_geometry/stl.py +168 -0
  14. simcon_toolkit/_kit/packages/cadmould_results/__init__.py +30 -0
  15. simcon_toolkit/_kit/packages/cadmould_results/loader.py +288 -0
  16. simcon_toolkit/_kit/packages/cadmould_scoring/__init__.py +7 -0
  17. simcon_toolkit/_kit/packages/cadmould_scoring/metrics.py +519 -0
  18. simcon_toolkit/_kit/pyproject.toml +232 -0
  19. simcon_toolkit/_kit/templates/_shared/AGENTS.base.md +101 -0
  20. simcon_toolkit/_kit/templates/gate-study/.gitignore +18 -0
  21. simcon_toolkit/_kit/templates/gate-study/AGENTS.md +46 -0
  22. simcon_toolkit/_kit/templates/gate-study/GATING_STUDY_PLAYBOOK.md +219 -0
  23. simcon_toolkit/_kit/templates/gate-study/INITIAL_PROMPT.md +26 -0
  24. simcon_toolkit/_kit/templates/gate-study/README.md +137 -0
  25. simcon_toolkit/_kit/templates/gate-study/main.py +344 -0
  26. simcon_toolkit/_kit/templates/gate-study/pipeline.py +281 -0
  27. simcon_toolkit/_kit/templates/process-window/.gitignore +20 -0
  28. simcon_toolkit/_kit/templates/process-window/AGENTS.md +49 -0
  29. simcon_toolkit/_kit/templates/process-window/METHOD.md +155 -0
  30. simcon_toolkit/_kit/templates/process-window/README.md +176 -0
  31. simcon_toolkit/_kit/templates/process-window/configs/simple-plate.yaml +116 -0
  32. simcon_toolkit/_kit/templates/process-window/doe_spec.schema.md +249 -0
  33. simcon_toolkit/_kit/templates/process-window/main.py +82 -0
  34. simcon_toolkit/_kit/templates/process-window/process_window/__init__.py +5 -0
  35. simcon_toolkit/_kit/templates/process-window/process_window/centre.py +298 -0
  36. simcon_toolkit/_kit/templates/process-window/process_window/design.py +144 -0
  37. simcon_toolkit/_kit/templates/process-window/process_window/economics.py +367 -0
  38. simcon_toolkit/_kit/templates/process-window/process_window/emit.py +591 -0
  39. simcon_toolkit/_kit/templates/process-window/process_window/guardrails.py +153 -0
  40. simcon_toolkit/_kit/templates/process-window/process_window/harness.py +360 -0
  41. simcon_toolkit/_kit/templates/process-window/process_window/identity.py +92 -0
  42. simcon_toolkit/_kit/templates/process-window/process_window/inspect_part.py +184 -0
  43. simcon_toolkit/_kit/templates/process-window/process_window/kpis.py +355 -0
  44. simcon_toolkit/_kit/templates/process-window/process_window/material_card.py +163 -0
  45. simcon_toolkit/_kit/templates/process-window/process_window/probe_proxy.py +169 -0
  46. simcon_toolkit/_kit/templates/process-window/process_window/run_confirm.py +403 -0
  47. simcon_toolkit/_kit/templates/process-window/process_window/run_epsilon_floor.py +198 -0
  48. simcon_toolkit/_kit/templates/process-window/process_window/run_feedback.py +322 -0
  49. simcon_toolkit/_kit/templates/process-window/process_window/run_refine.py +279 -0
  50. simcon_toolkit/_kit/templates/process-window/process_window/run_screening.py +370 -0
  51. simcon_toolkit/_kit/templates/process-window/process_window/run_sweep.py +166 -0
  52. simcon_toolkit/_kit/templates/process-window/process_window/setup_campaign.py +312 -0
  53. simcon_toolkit/_kit/templates/process-window/process_window/surrogate.py +201 -0
  54. simcon_toolkit/_kit/templates/process-window/process_window/test_centre.py +169 -0
  55. simcon_toolkit/_kit/templates/process-window/process_window/test_design.py +113 -0
  56. simcon_toolkit/_kit/templates/process-window/process_window/test_guardrails.py +157 -0
  57. simcon_toolkit/_kit/templates/process-window/process_window/test_surrogate.py +127 -0
  58. simcon_toolkit/_kit/templates/process-window/process_window/units.py +152 -0
  59. simcon_toolkit/_kit/templates/quoting/.gitignore +24 -0
  60. simcon_toolkit/_kit/templates/quoting/AGENTS.md +58 -0
  61. simcon_toolkit/_kit/templates/quoting/INTERVIEW.md +147 -0
  62. simcon_toolkit/_kit/templates/quoting/METHOD.md +256 -0
  63. simcon_toolkit/_kit/templates/quoting/PROMPT.md +46 -0
  64. simcon_toolkit/_kit/templates/quoting/QUOTING_PLAYBOOK.md +245 -0
  65. simcon_toolkit/_kit/templates/quoting/README.md +158 -0
  66. simcon_toolkit/_kit/templates/quoting/main.py +484 -0
  67. simcon_toolkit/_kit/templates/quoting/parts/.gitkeep +0 -0
  68. simcon_toolkit/_kit/templates/quoting/quoting/__init__.py +11 -0
  69. simcon_toolkit/_kit/templates/quoting/quoting/costing.py +725 -0
  70. simcon_toolkit/_kit/templates/quoting/quoting/geometry.py +398 -0
  71. simcon_toolkit/_kit/templates/quoting/quoting/shop.py +193 -0
  72. simcon_toolkit/_kit/templates/quoting/quoting/state.py +260 -0
  73. simcon_toolkit/_kit/templates/quoting/quoting/study.py +577 -0
  74. simcon_toolkit/_kit/templates/quoting/quoting/toolkit.py +50 -0
  75. simcon_toolkit/_kit/templates/quoting/shop/README.md +43 -0
  76. simcon_toolkit/_kit/templates/quoting/shop/commercial.md +86 -0
  77. simcon_toolkit/_kit/templates/quoting/shop/lessons.md +94 -0
  78. simcon_toolkit/_kit/templates/quoting/shop/machines.md +68 -0
  79. simcon_toolkit/_kit/templates/quoting/shop/materials.md +92 -0
  80. simcon_toolkit/_kit/templates/quoting/shop/shop-profile.md +87 -0
  81. simcon_toolkit/_kit/templates/quoting/shop/tooling.md +145 -0
  82. simcon_toolkit/_kit/templates/run-one-simulation/.gitignore +16 -0
  83. simcon_toolkit/_kit/templates/run-one-simulation/AGENTS.md +41 -0
  84. simcon_toolkit/_kit/templates/run-one-simulation/README.md +133 -0
  85. simcon_toolkit/_kit/templates/run-one-simulation/main.py +216 -0
  86. simcon_toolkit/_kit/templates.toml +83 -0
  87. simcon_toolkit/choices.py +11 -0
  88. simcon_toolkit/cli.py +381 -0
  89. simcon_toolkit/generate.py +590 -0
  90. simcon_toolkit/instructions.py +152 -0
  91. simcon_toolkit/manifest.py +86 -0
  92. simcon_toolkit/project.py +356 -0
  93. simcon_toolkit/wizard.py +160 -0
  94. simcon_toolkit-0.1.0.dist-info/METADATA +48 -0
  95. simcon_toolkit-0.1.0.dist-info/RECORD +97 -0
  96. simcon_toolkit-0.1.0.dist-info/WHEEL +4 -0
  97. 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())