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,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.
|