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,281 @@
1
+ """Cloud study harness for the gating study.
2
+
3
+ Wraps the live Cloud Solver Service (via ``cadmould_cloud.client``) and
4
+ the local ``cadmould`` SDK into the few operations a gating study repeats:
5
+
6
+ * **one project, many groups** - the cloud project is created once; each study
7
+ phase is its own group so the run list stays legible.
8
+ * **upload geometry once** - reused by every simulation.
9
+ * **run a batch** - submit a list of gate/process configs concurrently
10
+ (the AI filling path is synchronous), download each result, and score it
11
+ with :mod:`cadmould_scoring.metrics`.
12
+
13
+ Meshing and gate placement are not here: they are shared code, in
14
+ ``cadmould_geometry.mesh``.
15
+
16
+ Units mirror the API: temperatures Kelvin, spatial mm, flow cm^3/s.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import concurrent.futures as cf
22
+ import json
23
+ import os
24
+ import re
25
+ import time
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+
29
+ import httpx
30
+ import numpy as np
31
+
32
+ from cadmould_cloud import auth as cloud_auth
33
+ from cadmould_cloud.client import PlatformAPI
34
+ from cadmould_geometry.mesh import Geometry
35
+ from cadmould_scoring import metrics as M
36
+
37
+ # --- platform config (override via env: CLOUD_BASE_URL) ----------------------
38
+ BASE_URL = os.environ.get("CLOUD_BASE_URL", "https://api.simcon.ai/api/v1")
39
+ MODEL_VERSION = "v0.1.0"
40
+ MATERIAL_NAME = "PP Generic"
41
+ NUM_TIMESTEPS = 50
42
+ HTC_W_M2K = 1000.0
43
+
44
+ GATELOC_DT = np.dtype([("x", "<f8"), ("y", "<f8"), ("z", "<f8"), ("kno", "<i4")])
45
+
46
+
47
+ # ==========================================================================
48
+ # simulation config + batch runner
49
+ # ==========================================================================
50
+ @dataclass
51
+ class GateConfig:
52
+ name: str
53
+ gates_mm: list[list[float]]
54
+ flow_rate_cm3_s: float
55
+ melt_K: float
56
+ wall_K: float
57
+ fill_time_s: float
58
+ notes: str = ""
59
+ num_timesteps: int = NUM_TIMESTEPS
60
+ solver_type: str = "ai"
61
+
62
+
63
+ @dataclass
64
+ class RunResult:
65
+ config: GateConfig
66
+ simulation_id: str
67
+ h5_path: Path
68
+ analysis: M.Analysis
69
+ error: str = ""
70
+
71
+ def row(self) -> str:
72
+ a = self.analysis
73
+ ng = len(self.config.gates_mm)
74
+ return f" {self.config.name:22s} {ng}g " + (a.summary() if a else f"FAILED: {self.error}")
75
+
76
+
77
+ def _augment_for_viewer(path: Path, cfg: GateConfig, geometry_name: str) -> None:
78
+ """Inject the /input group + root attrs the result loader expects."""
79
+ import h5py
80
+
81
+ with h5py.File(path, "a") as f:
82
+ f.attrs.setdefault("geometry", geometry_name)
83
+ f.attrs.setdefault("material", MATERIAL_NAME)
84
+ if "input" in f:
85
+ return
86
+ inp = f.create_group("input")
87
+ inp.attrs["filling_time"] = float(cfg.fill_time_s)
88
+ inp.attrs["flowrate"] = float(cfg.flow_rate_cm3_s)
89
+ inp.attrs["melt_temperature"] = float(cfg.melt_K - 273.15)
90
+ inp.attrs["wall_temperature"] = float(cfg.wall_K - 273.15)
91
+ gl = np.zeros(len(cfg.gates_mm), dtype=GATELOC_DT)
92
+ for i, (x, y, z) in enumerate(cfg.gates_mm):
93
+ gl["x"][i], gl["y"][i], gl["z"][i], gl["kno"][i] = x, y, z, -1
94
+ inp.create_dataset("gate_locations", data=gl)
95
+
96
+
97
+ class Study:
98
+ """One cloud project; meshes once, uploads once, runs many grouped batches."""
99
+
100
+ def __init__(
101
+ self,
102
+ project_name: str,
103
+ geo: Geometry,
104
+ stl_name: str,
105
+ results_root: Path,
106
+ *,
107
+ token: str | None = None,
108
+ geometry_id: str | None = None,
109
+ logger=print,
110
+ ):
111
+ self.geo = geo
112
+ self.stl_name = stl_name
113
+ self.results_root = Path(results_root)
114
+ self.logger = logger
115
+ self.long_axis = geo.long_axis
116
+ token = token or cloud_auth.get_access_token()
117
+ self.api = PlatformAPI(BASE_URL, token, logger=logger)
118
+ self.project = self.api.get_or_create_project(project_name, notes=f"Gating study for {self.stl_name}")
119
+ self.project_id = self.project["id"]
120
+ # material
121
+ mat = self.api.find_material(MATERIAL_NAME)
122
+ self.material_id = mat["material_id"]
123
+ ver = self.api.get_material_version(mat["latest_version"]["version_id"])
124
+ self.melt_K = float(ver["suggested_mass_temp_K"])
125
+ self.wall_K = float(ver["suggested_wall_temp_K"])
126
+ self.geometry_id: str | None = geometry_id # reuse a prior upload if given
127
+ self.logger(f" project '{project_name}' id={self.project_id}")
128
+ self.logger(
129
+ f" material {MATERIAL_NAME} id={self.material_id} "
130
+ f"melt={self.melt_K - 273.15:.0f}C wall={self.wall_K - 273.15:.0f}C"
131
+ )
132
+
133
+ def close(self):
134
+ self.api.close()
135
+
136
+ def upload_geometry(self):
137
+ if self.geometry_id:
138
+ return self.geometry_id
139
+ up = self.api.request_geometry_upload(self.geo.cfex_path.name, description=f"gating study: {self.stl_name}")
140
+ self.geometry_id = up["geometry_id"]
141
+ self.api.put_cfex(up["presigned_put_url"], self.geo.cfex_path)
142
+ self.logger(f" geometry_id={self.geometry_id}")
143
+ return self.geometry_id
144
+
145
+ def _filling_with_retry(
146
+ self, cfg: GateConfig, group_name: str, group_id: str, *, retries: int = 4, backoff: float = 2.0
147
+ ) -> dict:
148
+ """run_filling with backoff on transient 503/timeout (the endpoint cold-starts
149
+ and sheds load under concurrency)."""
150
+ last = None
151
+ for attempt in range(retries + 1):
152
+ try:
153
+ return self.api.run_filling(
154
+ geometry_id=self.geometry_id,
155
+ material_id=self.material_id,
156
+ gate_positions_mm=cfg.gates_mm,
157
+ melt_temperature_K=cfg.melt_K,
158
+ wall_temperature_K=cfg.wall_K,
159
+ flow_rate_cm3_s=cfg.flow_rate_cm3_s,
160
+ estimated_filling_time_s=cfg.fill_time_s,
161
+ model_version=MODEL_VERSION,
162
+ num_timesteps=cfg.num_timesteps,
163
+ htc_W_m2K=HTC_W_M2K,
164
+ label=f"{group_name} / {cfg.name}",
165
+ project_id=self.project_id,
166
+ group_id=group_id,
167
+ solver_type=cfg.solver_type,
168
+ )
169
+ except httpx.HTTPStatusError as exc:
170
+ last = exc
171
+ if exc.response.status_code not in (429, 500, 502, 503, 504):
172
+ raise
173
+ except (httpx.TimeoutException, httpx.TransportError) as exc:
174
+ last = exc
175
+ if attempt < retries:
176
+ time.sleep(backoff * (attempt + 1)) # 2,4,6,8 s
177
+ raise last
178
+
179
+ def make_config(
180
+ self,
181
+ name: str,
182
+ gates_mm,
183
+ *,
184
+ flow_rate_cm3_s: float,
185
+ melt_K: float | None = None,
186
+ wall_K: float | None = None,
187
+ notes: str = "",
188
+ num_timesteps: int = NUM_TIMESTEPS,
189
+ solver_type: str = "ai",
190
+ ) -> GateConfig:
191
+ melt_K = self.melt_K if melt_K is None else melt_K
192
+ wall_K = self.wall_K if wall_K is None else wall_K
193
+ fill_time = self.geo.volume_cm3 / flow_rate_cm3_s if flow_rate_cm3_s > 0 else 1.0
194
+ return GateConfig(
195
+ name,
196
+ [list(map(float, g)) for g in gates_mm],
197
+ float(flow_rate_cm3_s),
198
+ float(melt_K),
199
+ float(wall_K),
200
+ float(round(fill_time, 3)),
201
+ notes,
202
+ int(num_timesteps),
203
+ str(solver_type),
204
+ )
205
+
206
+ def run_group(
207
+ self,
208
+ group_name: str,
209
+ configs: list[GateConfig],
210
+ *,
211
+ description: str = "",
212
+ max_workers: int = 8,
213
+ group_id: str | None = None,
214
+ ) -> list[RunResult]:
215
+ """Create a group (or reuse ``group_id``) and run its configs concurrently."""
216
+ self.upload_geometry()
217
+ if group_id is None:
218
+ grp = self.api.create_group(
219
+ self.project_id, group_name, description=description or f"{len(configs)} configs"
220
+ )
221
+ group_id = grp["id"]
222
+ else:
223
+ self.logger(f" reusing existing group id={group_id}")
224
+ out_dir = self.results_root / re.sub(r"[^A-Za-z0-9_-]+", "_", group_name)
225
+ out_dir.mkdir(parents=True, exist_ok=True)
226
+ self.logger(f"\n group '{group_name}' id={group_id} -> {len(configs)} sims")
227
+
228
+ quiet = self.api.logger
229
+ self.api.logger = lambda *_: None
230
+
231
+ def run_one(cfg: GateConfig) -> RunResult:
232
+ try:
233
+ res = self._filling_with_retry(cfg, group_name, group_id)
234
+ sim_id = res["simulation_id"]
235
+ h5 = out_dir / f"{cfg.name}__{sim_id[:8]}.h5"
236
+ self.api.download_to(res["result"]["download"]["url"], h5)
237
+ _augment_for_viewer(h5, cfg, self.stl_name)
238
+ analysis = M.analyze(h5, cfg.gates_mm, long_axis=self.long_axis)
239
+ return RunResult(cfg, sim_id, h5, analysis)
240
+ except Exception as exc:
241
+ return RunResult(cfg, "", out_dir / f"{cfg.name}.FAILED", None, error=f"{type(exc).__name__}: {exc}")
242
+
243
+ results: list[RunResult] = []
244
+ with cf.ThreadPoolExecutor(max_workers=min(max_workers, len(configs))) as pool:
245
+ futs = {pool.submit(run_one, c): c for c in configs}
246
+ for fut in cf.as_completed(futs):
247
+ r = fut.result()
248
+ results.append(r)
249
+ self.logger(r.row())
250
+ self.api.logger = quiet
251
+
252
+ results.sort(key=lambda r: configs.index(r.config))
253
+ manifest = out_dir / "manifest.json"
254
+ manifest.write_text(
255
+ json.dumps(
256
+ {
257
+ "group": group_name,
258
+ "group_id": group_id,
259
+ "project_id": self.project_id,
260
+ "results": [
261
+ {
262
+ "name": r.config.name,
263
+ "gates_mm": r.config.gates_mm,
264
+ "flow_rate_cm3_s": r.config.flow_rate_cm3_s,
265
+ "melt_K": r.config.melt_K,
266
+ "wall_K": r.config.wall_K,
267
+ "fill_time_s": r.config.fill_time_s,
268
+ "notes": r.config.notes,
269
+ "simulation_id": r.simulation_id,
270
+ "h5": str(r.h5_path),
271
+ "metrics": r.analysis.as_dict() if r.analysis else None,
272
+ "error": r.error,
273
+ }
274
+ for r in results
275
+ ],
276
+ },
277
+ indent=2,
278
+ )
279
+ )
280
+ self.logger(f" wrote {manifest}")
281
+ return results
@@ -0,0 +1,20 @@
1
+ # The campaign writes nothing that is committed. cache/ is ours (mesh, campaign state,
2
+ # and the records each stage leaves for the next) and output/ is yours (the spec and the
3
+ # dossier). Both are listed here rather than left to the kit's root ignore file, because
4
+ # a generated project receives this file and not that one. The output schema is tracked,
5
+ # in doe_spec.schema.md.
6
+ cache/
7
+ output/
8
+ *.h5
9
+ *.car
10
+ *.rm1
11
+ *.cfex
12
+
13
+ .venv/
14
+ __pycache__/
15
+ *.pyc
16
+ .pytest_cache/
17
+
18
+ # never commit tokens
19
+ *token*.json
20
+ .env
@@ -0,0 +1,49 @@
1
+ # Process window — how to drive this template
2
+
3
+ A design-of-experiments campaign that works out which machine parameters to vary, where to
4
+ centre them and how wide to set the bounds, before anyone books machine time. It sweeps the
5
+ cheap AI solver, screens the result for feasibility, refines the boundary, and writes a DOE
6
+ specification plus a dossier.
7
+
8
+ The method is in [`METHOD.md`](METHOD.md) — the guardrails, what may be swept, which KPI is
9
+ allowed to decide what, and two traps that silently corrupt a screening. **Read it before
10
+ running a sweep or changing what the campaign measures.**
11
+
12
+ ## What to run
13
+
14
+ ```
15
+ python main.py --list # every step, in order
16
+ python main.py inspect --part path/to/part.stl # is this part worth a campaign at all?
17
+ python main.py sweep configs/simple-plate.yaml
18
+ ```
19
+
20
+ Each step is resumable and writes what the next one reads, so they run in the order `--list`
21
+ prints. There is deliberately no command that runs the whole campaign: judgement belongs
22
+ between the steps.
23
+
24
+ Everything after the step name belongs to that step — `python main.py refine --help` prints
25
+ that step's own options. The steps do not share a flag set, because the same flag name means
26
+ different things to different steps and folding them together would silently change what a
27
+ campaign ran.
28
+
29
+ ## What is in this folder
30
+
31
+ - `main.py` is the only thing you run; `process_window/` is the pipeline.
32
+ - `configs/` holds one file per part; `doe_spec.schema.md` is the output schema.
33
+ - `cadmould_cloud/`, `cadmould_geometry/` and `cadmould_scoring/` arrive as packages.
34
+ - `sample/` holds the default geometry, and it is ours.
35
+ - `output/` is yours — the spec and the dossier. `cache/` is ours — the mesh and campaign
36
+ state. Both are safe to delete; only the cache costs time.
37
+
38
+ ## Things to get right here
39
+
40
+ **Run `inspect` first.** It is free, and it tells you whether the part is thermally marginal
41
+ enough for a campaign to find anything.
42
+
43
+ **Every criterion must be relative.** The code fails the run if an absolute pressure threshold
44
+ appears in one, and that guard is there because the model under-reads absolute pressure.
45
+
46
+ **A campaign is keyed on the part's contents, not its filename.** Re-export the part and it is
47
+ a different campaign, deliberately.
48
+
49
+ **Do not run anything else while a sweep is running.** It already holds the concurrency budget.
@@ -0,0 +1,155 @@
1
+ ---
2
+ # Front matter so the generator can emit this file unchanged as a Claude skill; it is
3
+ # inert everywhere else, and the method below is the same either way.
4
+ name: process-window
5
+ description: >-
6
+ Find the process window for an injection-moulded part before machine trials: which machine
7
+ parameters to vary, what centre point to start from, and how wide the bounds should be.
8
+ Runs a cheap AI sweep over melt temperature, mould temperature and injection rate, screens
9
+ it for feasibility, refines the boundary, and emits a DOE specification plus a dossier. Use
10
+ whenever someone asks which settings to trial, how wide to set the bounds, or what a safe
11
+ starting point is for a new mould.
12
+ ---
13
+
14
+ # Finding a process window
15
+
16
+ The campaign replaces "standard settings ±10 % and a guess" with a large number of near-free
17
+ AI-solver runs plus a handful of numerical confirmations.
18
+
19
+ ```
20
+ config -> AI sweep -> screening -> boundary refinement
21
+ -> numerical confirmation set -> doe_spec.yaml + dossier.md
22
+ ```
23
+
24
+ Judgement belongs between the stages, which is why there is no command that runs the whole
25
+ campaign. A sweep costs thousands of solver runs, and whether the result is worth refining is
26
+ a decision, not a step.
27
+
28
+ ## Three guardrails, enforced in code
29
+
30
+ **1. Relative statements only.** The AI solver under-predicts absolute pressure substantially
31
+ on multi-gate parts, and a campaign usually models fewer cavities than the real tool. Every
32
+ criterion must therefore be a rank, a ratio or a comparison. `process_window/guardrails.py`
33
+ fails the run if an absolute pressure threshold appears in any criterion.
34
+
35
+ **2. Derive the noise floor for this part, never import one.** Run the epsilon-input micro
36
+ experiment and let it tell you the floor. Any variable whose entire effect sits below it is
37
+ declared inert and fixed, rather than ranked low. Figures measured on another part are
38
+ part-specific and do not transfer.
39
+
40
+ What the floor is matters: the solver is deterministic, so this measures storage quantisation
41
+ and model conditioning, not run-to-run scatter. Name that mechanism in the dossier rather
42
+ than calling it noise.
43
+
44
+ **3. Limitations travel with the claim.** How many cavities were modelled, whether a runner
45
+ system was present at all, and how much of the real process is described beyond the material.
46
+ Every dossier claim resting on absolute values carries the caveat automatically.
47
+
48
+ ## What can actually be swept
49
+
50
+ The AI filling endpoint takes one constant flow rate. There is no switchover input and no
51
+ velocity profile, so exactly three variables are AI-visible:
52
+
53
+ - melt temperature
54
+ - mould wall temperature
55
+ - injection rate
56
+
57
+ Anything derived from those is derived, never swept alongside them — sweeping a fill time
58
+ independently of the flow that produces it submits self-inconsistent inputs.
59
+
60
+ **Switchover is a read-out axis, not a swept input.** Every run holds the full field state at
61
+ every fill fraction, so the pressure, weld-front temperature and unfilled fraction at a
62
+ candidate switchover point come free from runs already paid for. That is the state *at*
63
+ switchover, not the effect *of* switching there — the effect lives in packing, so switchover
64
+ becomes a genuinely swept variable only in the numerical decks.
65
+
66
+ **Numerical-only variables**, entering at the confirmation stage: switchover point, holding
67
+ pressure level, holding pressure time, cooling time.
68
+
69
+ ## The box
70
+
71
+ Temperature is an interval scale, so "±50 %" of it depends on the unit chosen and is not a
72
+ well-defined instruction. The campaign therefore takes:
73
+
74
+ - **melt and mould temperature** — the material card's full processing window, read at
75
+ runtime, so the box is automatically grade-specific and every corner is physically runnable.
76
+ - **injection rate** — ±50 % around nominal, which is a ratio scale where that is meaningful.
77
+
78
+ Expect most of the box to be excluded. Knowing *which* part is the whole exercise.
79
+
80
+ ## The KPIs, and what each is allowed to decide
81
+
82
+ | KPI | Role |
83
+ |---|---|
84
+ | No-flow margin at end of fill | **The hard feasibility gate.** Whether the flow channel stayed open |
85
+ | No-flow margin at arrival | Reported, never a gate — fountain flow keeps the front near melt temperature, so it does not discriminate |
86
+ | Complete fill | Diagnostic only, reported not gated |
87
+ | Max cavity pressure at 80 % fill | Primary continuous KPI. **Ordering only, never an absolute threshold** |
88
+ | Weld-front temperature | Fill-phase quality proxy. Orders points well; carries a circularity caveat when melt temperature drives it |
89
+ | Thermal heterogeneity | Warpage **risk flag** only |
90
+
91
+ **Never screen on a fill-race metric** — a fill-time difference between two points was
92
+ measured not to transfer. It may be computed as an anomaly detector, clearly labelled.
93
+
94
+ **Machine limits are a constraint, not a criterion.** They annotate the recommended box; they
95
+ never include or exclude a sweep point. Comparing a pressure the model under-reads against a
96
+ correct machine maximum would pass every point and tell you nothing. Any clamp-force statement
97
+ rests on the projected area actually modelled, so state the extrapolation or omit the claim.
98
+
99
+ ## Two traps that silently corrupt a screening
100
+
101
+ **Do not gate on fill completeness.** A residual unfilled fraction is a model artefact, and it
102
+ has grown with flow rate — so a "fill percentage must exceed 99" gate rejects every run and
103
+ produces "high injection rate causes short shots", a conclusion that sounds like textbook
104
+ plastics and points the same way a real effect would, while being pure artefact.
105
+
106
+ Use the no-flow margin instead. It is better engineering regardless: a short shot is a
107
+ *consequence* of the melt freezing before the cavity fills, so the margin is the causal
108
+ quantity, and being continuous it gives a distance to infeasibility, which is what boundary
109
+ refinement needs. A binary gate carries almost no gradient. Measure the residual anyway and
110
+ report the regime.
111
+
112
+ **Do not average replicates.** The solver is deterministic, so repeats carry no information
113
+ and there are no error bars to be had from them.
114
+
115
+ ## Two practical rules
116
+
117
+ **Predict whether a sweep will discriminate from fill time against freeze time, not from
118
+ length-to-thickness ratio.** A part can be trivially fillable by that ratio and still be
119
+ thermally marginal, because a thin wall freezes faster than it fills. The inspect step reports
120
+ the wall thickness both times are computed from, and it is free — run it before spending
121
+ anything.
122
+
123
+ **Never derive the nominal flow from a guessed fill time.** Use the SDK's own suggestion
124
+ tools. A hand-picked fill time was measured 3.5× too slow on a real part, which would have put
125
+ the whole box into freeze-off.
126
+
127
+ **Do not run anything concurrently with a sweep.** The sweep already holds the concurrency
128
+ budget; adding more makes the endpoint shed load.
129
+
130
+ ## Storage
131
+
132
+ Results are tens to hundreds of megabytes each and scale with nodes × timesteps, so a
133
+ thousand-run campaign is measured in tens of gigabytes. Score each result, write a small
134
+ record, then delete the result file. Keep the full file only for boundary runs, flagged
135
+ anomalies and confirmation anchors, and never let raw results accumulate in a synced folder.
136
+ Re-scoring a discarded run means re-running it, which is cheap because the solver is
137
+ deterministic.
138
+
139
+ The timestep count is a knob on both file size and metric fidelity: fewer steps shrink files
140
+ but coarsen the fill-fraction grid the pressure and switchover read-outs depend on.
141
+
142
+ ## Resuming
143
+
144
+ Assume the session dies mid-sweep. Every run's inputs and outputs go to disk immediately and
145
+ re-running the command resumes rather than restarts. The design sequence is incremental, so a
146
+ sweep can stop early or continue without recommitting.
147
+
148
+ The KPI definition is part of every run id, so redefining a KPI invalidates records rather
149
+ than letting a resume report them done while permanently missing the new field. Superseded
150
+ records stay on disk as history, so readers must filter.
151
+
152
+ ## Before scaling up
153
+
154
+ Authenticate, submit one simulation, read the result, print the KPIs. Prove that loop before
155
+ any sweep runs. That is where data-format surprises surface.
@@ -0,0 +1,176 @@
1
+ # Frontloaded DOE — find the process window before the machine trial
2
+
3
+ A process engineer gets a new mould and has machine trials tomorrow. He needs three things: **which
4
+ parameters to vary, where to start, and how wide the bounds should be.** The usual answer is
5
+ standard settings ±10 % and a guess.
6
+
7
+ This template replaces the guess with a few thousand near-free AI-solver simulations and a validated
8
+ set of numerical confirmation jobs — from one config file and one command per stage.
9
+
10
+ ```
11
+ config.yaml -> [wide sweep] -> [screening] -> [boundary refinement]
12
+ -> [numerical confirmation decks] -> doe_spec.yaml + dossier.md
13
+ ```
14
+
15
+ The pipeline writes two things: `doe_spec.yaml`, the machine-readable answer, and `dossier.md`,
16
+ the written one. Both land in `output/<part-key>/` as it runs.
17
+
18
+ ---
19
+
20
+ ## What is and isn't in this directory
21
+
22
+ | | |
23
+ |---|---|
24
+ | ✅ the full pipeline | `process_window/`, ten stages plus four test suites, numpy only |
25
+ | ✅ the output schema | [`doe_spec.schema.md`](doe_spec.schema.md) — what `doe_spec.yaml` contains |
26
+ | ✅ a working config | [`configs/simple-plate.yaml`](configs/simple-plate.yaml), wired to the sample part |
27
+ | ❌ any campaign output | **not included** — the pipeline writes its own as it runs |
28
+
29
+ [`configs/simple-plate.yaml`](configs/simple-plate.yaml) points at the sample geometry in
30
+ `sample/`, so you can execute the pipeline end to end immediately. It is also the default every
31
+ stage falls back to when you pass no config, while that is unambiguous. To run the method on
32
+ your own part, copy it and
33
+ change the `part:` block — the part is configured, not passed on the command line, because it
34
+ travels with the material, the bounds and the budget that make a campaign mean something.
35
+
36
+ Two folders appear as it runs. `cache/` is ours — the mesh and the campaign state — and is safe
37
+ to delete at the cost of recomputing it. `output/` is yours: the spec and the dossier.
38
+
39
+ ## Running it
40
+
41
+ One command per stage, in this order. `python main.py --list` prints the same list.
42
+
43
+ ```bash
44
+ python main.py inspect # free: will a sweep even discriminate?
45
+ python main.py epsilon-floor # derive THIS part's resolution floor
46
+ python main.py probe # does the feasibility gate discriminate?
47
+ python main.py sweep --n 256 # space-filling sweep, resumable
48
+ python main.py screen # rank variables, map what is excluded
49
+ python main.py refine # adaptive boundary refinement + centre
50
+ python main.py confirm # numerical .rm1 decks
51
+ python main.py feedback # hidden-deviation self-diagnosis
52
+ python main.py emit # doe_spec.yaml + dossier.md
53
+ python main.py economics # cycle time vs robustness
54
+ ```
55
+
56
+ Every stage takes the config as an optional argument and falls back to
57
+ `configs/simple-plate.yaml`. Each keeps its own options — `python main.py refine --help`.
58
+
59
+ ⚠️ **Once you have run a campaign on your own part, pass the config every time.** The
60
+ fallback is withdrawn as soon as a second campaign exists, because from then on a bare
61
+ stage could mean either — and picking wrong would read and write the sample's campaign
62
+ while looking like it continued yours. A stage that cannot tell says so and lists what it
63
+ found, rather than guessing.
64
+
65
+ There is deliberately no command that runs the whole campaign: a sweep costs thousands of
66
+ solver runs, and the point is that you look at each stage before paying for the next.
67
+
68
+ To inspect several candidate parts side by side:
69
+
70
+ ```bash
71
+ python main.py inspect --part a.stl b.stl
72
+ ```
73
+
74
+ Every stage is **resumable**: a run's identity is a hash of its inputs — including the geometry, the
75
+ model version and the KPI definition — so re-running continues rather than restarts, and redefining
76
+ a KPI invalidates stale records instead of silently reusing them.
77
+
78
+ Tests need no cloud access and run in seconds:
79
+
80
+ ```bash
81
+ python -m process_window.test_design
82
+ python -m process_window.test_guardrails
83
+ python -m process_window.test_surrogate
84
+ python -m process_window.test_centre
85
+ ```
86
+
87
+ Three modules are runnable the same way, as diagnostics rather than stages — score one
88
+ result file, read a material card, or print the campaign's derived bounds:
89
+
90
+ ```bash
91
+ python -m process_window.kpis <result.h5> [melt_K] [wall_K] [no_flow_K]
92
+ python -m process_window.material_card <your.plb>
93
+ python -m process_window.setup_campaign [configs/simple-plate.yaml]
94
+ ```
95
+
96
+ ## What makes this more than a parameter sweep
97
+
98
+ **Three guardrails are enforced in code, not in comments.**
99
+
100
+ 1. **Relative statements only.** The AI solver under-predicts absolute cavity pressure on multi-gate
101
+ parts, so every criterion must be a rank, ratio or comparison. `process_window/guardrails.py` *fails the
102
+ run* if an absolute pressure threshold appears in any criterion.
103
+ 2. **The resolution floor is derived per part, never imported.** Nudge the inputs by amounts no
104
+ machine can hold, measure how far the outputs move anyway, and treat everything below that as the
105
+ solver's own arithmetic. The floor is a property of the part, not of the
106
+ method, so importing one part's figure into another wrecks every downstream verdict.
107
+ 3. **Stated limitations attach themselves** to any claim that rests on an absolute value.
108
+
109
+ **The centre point is the Chebyshev centre** — the deepest interior point of the feasible region,
110
+ not the mean of the surviving runs. On an awkwardly shaped region that mean can fall *outside* the
111
+ region entirely, which `process_window/test_centre.py` demonstrates.
112
+
113
+ ## Layout
114
+
115
+ ```
116
+ main.py the only file you run configs/ one yaml per part
117
+ process_window/ the pipeline + tests sample/ the bundled sample part
118
+ doe_spec.schema.md the output schema
119
+ cache/ mesh + campaign state, ours, safe to delete
120
+ output/ the spec and the dossier, yours
121
+ METHOD.md the method: guardrails, KPIs, and the traps worth reading first
122
+ ```
123
+
124
+ `METHOD.md` is the highest-value file for anyone extending this: it records the three
125
+ guardrails, which KPI is allowed to decide what, and the two traps that silently corrupt a
126
+ screening. The unit boundaries and the result-file semantics moved to `AGENTS.md`, which
127
+ carries them for every workflow rather than this one.
128
+
129
+ ## Requirements
130
+
131
+ <!-- simcon-toolkit:setup -->
132
+ **Python 3.12 or newer**, and a virtual environment created *with that interpreter* — a venv
133
+ isolates packages, not the interpreter.
134
+
135
+ **Three sign-ins, each in your browser, and each does a different job.**
136
+
137
+ 1. **The package index**, once, in the steps below: `cadmould-sdk-auth` lets pip install the
138
+ licensed `cadmould` wheel from SIMCON's private index. Your company's email domain has to be
139
+ enabled for it: if you can sign in but the browser tab then says `Access denied: your email
140
+ domain is not authorized for this API`, ask SIMCON support.
141
+ 2. **Your licence**, the first time the code opens a licence session. It first checks the
142
+ Thales Sentinel run-time on your machine, and prints `License init failed: HASP driver
143
+ version too old` or `... runtime version too old` when that needs installing or updating.
144
+ 3. **The cloud**, the first time the code calls it.
145
+
146
+ ⚠️ A failed licence sign-in does not stop the script. It prints `License init failed: ...` or
147
+ `License acquisition failed.` to the terminal and carries on, so the error you meet later is `Cadmould API licence (LICENCE_API)
148
+ required`. When you see that one, scroll up to the first.
149
+
150
+ **macOS / Linux (bash):**
151
+
152
+ ```bash
153
+ python3.12 -m venv .venv
154
+ source .venv/bin/activate
155
+ pip install --upgrade pip cadmould-sdk-auth
156
+ cadmould-sdk-auth # opens a browser; sign in with your Simcon account
157
+ pip install . # installs cadmould and everything this campaign needs
158
+ ```
159
+
160
+ **Windows (PowerShell):**
161
+
162
+ ```powershell
163
+ py -3.12 -m venv .venv
164
+ .\.venv\Scripts\python.exe -m pip install --upgrade pip cadmould-sdk-auth
165
+ $env:VIRTUAL_ENV = "$PWD\.venv" # tell the login CLI which venv to configure
166
+ .\.venv\Scripts\cadmould-sdk-auth.exe # opens a browser; sign in with your Simcon account
167
+ .\.venv\Scripts\python.exe -m pip install .
168
+ ```
169
+
170
+ The Windows route never activates the environment. `Activate.ps1` is blocked by the default
171
+ execution policy on many machines; naming the interpreter outright avoids it.
172
+ <!-- /simcon-toolkit:setup -->
173
+
174
+ No sklearn: the Sobol sequence,
175
+ the Gaussian-process surrogate, the Sobol sensitivity indices and the distance transform are all
176
+ implemented on numpy and validated against closed-form benchmarks in the test suites.