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,137 @@
|
|
|
1
|
+
# Template · Gate study
|
|
2
|
+
|
|
3
|
+
A **ready-to-run** early-stage gating study: drive the Simcon cloud AI solver through the
|
|
4
|
+
typical decisions for an injection-moulded part — **how many gates, where, and what process** —
|
|
5
|
+
scoring each run for filling pressure, fill evenness and **weld lines**, organised into tidy
|
|
6
|
+
cloud groups. It ships with the original prompt, the playbook, and all the code, pointed at a
|
|
7
|
+
neutral sample part (`simple_plate.stl`), so you start from something that already runs instead
|
|
8
|
+
of a blank page.
|
|
9
|
+
|
|
10
|
+
## Why a custom REST client (not `cadmould.cloud`)?
|
|
11
|
+
|
|
12
|
+
The `cadmould.cloud` SDK wraps the **per-simulation** surface — upload geometry, look up materials,
|
|
13
|
+
run a filling simulation, download the result. It does **not** wrap the **management** surface an
|
|
14
|
+
organised study needs: **projects, groups, and the decision log** (project notes/events). This
|
|
15
|
+
study groups runs by phase (gate count → placement → process → validation) and records *why* each
|
|
16
|
+
choice was made, so it drives the cloud over the **REST API** directly via the `cadmould_cloud`
|
|
17
|
+
package that ships beside this file (`client.py::PlatformAPI` + Auth0 PKCE login). For a
|
|
18
|
+
simple one-off run you don't need any of this — the **run-one-simulation** template shows the SDK
|
|
19
|
+
path; reach for REST only when you need the management/decision-log surface (or just prefer your
|
|
20
|
+
own client). The API itself is documented at <https://api.simcon.ai/scalar>.
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
<!-- simcon-toolkit:setup -->
|
|
25
|
+
**Python 3.12 or newer**, and a virtual environment created *with that interpreter* — a venv
|
|
26
|
+
isolates packages, not the interpreter, so naming it is what makes the version real.
|
|
27
|
+
|
|
28
|
+
**Three sign-ins, each in your browser, and each does a different job.**
|
|
29
|
+
|
|
30
|
+
1. **The package index**, once, in the steps below: `cadmould-sdk-auth` lets pip install the
|
|
31
|
+
licensed `cadmould` wheel from SIMCON's private index. Your company's email domain has to be
|
|
32
|
+
enabled for it: if you can sign in but the browser tab then says `Access denied: your email
|
|
33
|
+
domain is not authorized for this API`, ask SIMCON support.
|
|
34
|
+
2. **Your licence**, the first time the code opens a licence session. It first checks the
|
|
35
|
+
Thales Sentinel run-time on your machine, and prints `License init failed: HASP driver
|
|
36
|
+
version too old` or `... runtime version too old` when that needs installing or updating.
|
|
37
|
+
3. **The cloud**, the first time the code calls it.
|
|
38
|
+
|
|
39
|
+
⚠️ A failed licence sign-in does not stop the script. It prints `License init failed: ...` or
|
|
40
|
+
`License acquisition failed.` to the terminal and carries on, so the error you meet later is `Cadmould API licence (LICENCE_API)
|
|
41
|
+
required`. When you see that one, scroll up to the first.
|
|
42
|
+
|
|
43
|
+
**macOS / Linux (bash):**
|
|
44
|
+
```bash
|
|
45
|
+
python3.12 -m venv .venv
|
|
46
|
+
source .venv/bin/activate
|
|
47
|
+
pip install --upgrade pip cadmould-sdk-auth
|
|
48
|
+
cadmould-sdk-auth # opens a browser; sign in with your Simcon account
|
|
49
|
+
pip install . # installs cadmould and everything this study needs
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Windows (PowerShell):**
|
|
53
|
+
```powershell
|
|
54
|
+
py -3.12 -m venv .venv
|
|
55
|
+
.\.venv\Scripts\python.exe -m pip install --upgrade pip cadmould-sdk-auth
|
|
56
|
+
$env:VIRTUAL_ENV = "$PWD\.venv" # tell the login CLI which venv to configure
|
|
57
|
+
.\.venv\Scripts\cadmould-sdk-auth.exe # opens a browser; sign in with your Simcon account
|
|
58
|
+
.\.venv\Scripts\python.exe -m pip install .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The Windows route never activates the environment. `Activate.ps1` is blocked by the default
|
|
62
|
+
execution policy on many machines; naming the interpreter outright avoids it.
|
|
63
|
+
<!-- /simcon-toolkit:setup -->
|
|
64
|
+
|
|
65
|
+
The first cloud run opens the third sign-in, for the cloud — no manual token needed.
|
|
66
|
+
|
|
67
|
+
## Run it
|
|
68
|
+
|
|
69
|
+
All commands assume you are **in this folder**.
|
|
70
|
+
|
|
71
|
+
**macOS / Linux (bash):**
|
|
72
|
+
```bash
|
|
73
|
+
# each phase is its own cloud group; the FIRST run opens the cloud login
|
|
74
|
+
python main.py group1 # gate count: 1 vs 2 vs 3 vs 4
|
|
75
|
+
python main.py group2 # single-gate placement sweep
|
|
76
|
+
python main.py group3 # process / flow-rate sweep
|
|
77
|
+
python main.py group4 # validation
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Windows (PowerShell):**
|
|
81
|
+
```powershell
|
|
82
|
+
python main.py group1
|
|
83
|
+
python main.py group2
|
|
84
|
+
python main.py group3
|
|
85
|
+
python main.py group4
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Study your own part instead of the sample one — no code edit needed:
|
|
89
|
+
```bash
|
|
90
|
+
python main.py group1 --part path/to/your-part.stl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The part is meshed and uploaded **once**; `cache/<part>.study_state.json` holds the
|
|
94
|
+
geometry id and the decisions so later phases reuse them — one file per part, so studying a
|
|
95
|
+
second part never inherits the first one's geometry. Each group prints a scored table and
|
|
96
|
+
writes results + a `manifest.json` under `output/<group>/`.
|
|
97
|
+
|
|
98
|
+
The kit stops at the scored numbers. Turning them into a deck or a write-up is your assistant's
|
|
99
|
+
job, working from the `manifest.json` and the result files — that way the document is in your
|
|
100
|
+
format rather than ours.
|
|
101
|
+
|
|
102
|
+
## Layout
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
gate-study/
|
|
106
|
+
├── main.py # the driver: group1..4 (run this)
|
|
107
|
+
├── pipeline.py # cloud project/group/upload/batch-run + scoring
|
|
108
|
+
├── INITIAL_PROMPT.md # the prompt that kicks off a study
|
|
109
|
+
├── GATING_STUDY_PLAYBOOK.md # HOW TO run a gate study + every gotcha (read this)
|
|
110
|
+
├── sample/ # your copy of the sample part (written when the project
|
|
111
|
+
│ # is generated; not present in this repository)
|
|
112
|
+
├── output/ # yours: scored results, one folder per group
|
|
113
|
+
└── cache/ # ours: the meshed .cfex and study_state.json
|
|
114
|
+
```
|
|
115
|
+
`output/` and `cache/` are both safe to delete — clearing the cache costs a re-mesh and a
|
|
116
|
+
re-upload, never a different answer. The cloud project is found again by its name.
|
|
117
|
+
|
|
118
|
+
Running it **inside this repository** needs the shared packages on the path, because the
|
|
119
|
+
generator has not copied them in yet:
|
|
120
|
+
```bash
|
|
121
|
+
PYTHONPATH=packages python templates/gate-study/main.py group1 --part assets/parts/simple_plate.stl
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## What each script does
|
|
125
|
+
|
|
126
|
+
- **`main.py`** — `group1`…`group4`; builds gate configs, submits them concurrently (low
|
|
127
|
+
concurrency + retry, because the endpoint sheds load), downloads + scores each.
|
|
128
|
+
- **`cadmould_scoring.metrics`** (in `packages/`) — peak pressure (bar), fill evenness (`tail`), and weld lines via
|
|
129
|
+
convergence of the flow-direction field. Pure numpy, h5py and scipy: reading and scoring a
|
|
130
|
+
result needs no graphics stack.
|
|
131
|
+
|
|
132
|
+
## Read this before adapting to a new part
|
|
133
|
+
|
|
134
|
+
[`GATING_STUDY_PLAYBOOK.md`](GATING_STUDY_PLAYBOOK.md) — the method, the
|
|
135
|
+
metrics, **§7 how to repoint at a new part**, and §5 the non-obvious cloud/SDK/data details
|
|
136
|
+
(weld-detector calibration, the fill-front format, re-centred geometry, endpoint concurrency
|
|
137
|
+
limits, the numerical-solver path).
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Driver for the gating study (live Simcon cloud AI solver).
|
|
2
|
+
|
|
3
|
+
Each phase is a subcommand and a logical cloud *group* under one *project*:
|
|
4
|
+
|
|
5
|
+
python main.py group1 # gate-count: 1 vs 2 vs 3 vs 4 gates
|
|
6
|
+
python main.py group2 # gate placement for the chosen count
|
|
7
|
+
python main.py group3 # process / pressure tuning
|
|
8
|
+
python main.py group4 # validation of the final design
|
|
9
|
+
|
|
10
|
+
The part is meshed and the geometry uploaded **once**; the cached state under
|
|
11
|
+
``cache/`` remembers the project + geometry ids (and decisions) so later phases
|
|
12
|
+
reuse them. Scored results land under ``output/``.
|
|
13
|
+
|
|
14
|
+
The sample part is a default, not a fixture: pass ``--part path/to/your.stl``
|
|
15
|
+
to study your own geometry.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import pipeline
|
|
27
|
+
|
|
28
|
+
from cadmould_geometry.mesh import Geometry, gate_line, parse_cfex_nodes, prepare_geometry
|
|
29
|
+
|
|
30
|
+
HERE = Path(__file__).resolve().parent
|
|
31
|
+
# Written by the generator, so this path resolves in your project and not in ours.
|
|
32
|
+
DEFAULT_PART = HERE / "sample" / "simple_plate.stl"
|
|
33
|
+
# Yours: scored results, one folder per group. Safe to delete.
|
|
34
|
+
OUTPUT = HERE / "output"
|
|
35
|
+
# Ours: the meshed geometry and the study's own bookkeeping, both per part. Deleting
|
|
36
|
+
# it costs a re-mesh and a re-upload, never a wrong answer — the cloud project is
|
|
37
|
+
# found again by name.
|
|
38
|
+
CACHE = HERE / "cache"
|
|
39
|
+
|
|
40
|
+
NOMINAL_FLOW = 45.0 # cm^3/s -> ~2.3 s fill for the 103.6 cm^3 sample part
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------
|
|
44
|
+
def part_key(part: Path) -> str:
|
|
45
|
+
"""Cache identity for a part: its name, plus enough of its content to be honest.
|
|
46
|
+
|
|
47
|
+
The name alone is not an identity. Two different parts can both be bracket.stl,
|
|
48
|
+
and re-exporting the same part changes its bytes but not its path — either way a
|
|
49
|
+
name-keyed cache hands the study a mesh and an uploaded geometry from the wrong
|
|
50
|
+
version of the part, with nothing to show that it happened.
|
|
51
|
+
"""
|
|
52
|
+
digest = hashlib.sha256(part.read_bytes()).hexdigest()[:12]
|
|
53
|
+
return f"{part.stem}-{digest}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def state_path(part: Path) -> Path:
|
|
57
|
+
"""One state file per part: ids and decisions describe a geometry, not a folder."""
|
|
58
|
+
return CACHE / f"{part_key(part)}.study_state.json"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def studies_on_file() -> list[str]:
|
|
62
|
+
"""Every part a study has actually been started for."""
|
|
63
|
+
if not CACHE.is_dir():
|
|
64
|
+
return []
|
|
65
|
+
return sorted(p.name.removesuffix(".study_state.json") for p in CACHE.glob("*.study_state.json"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_part(explicit: Path | None) -> Path:
|
|
69
|
+
"""The geometry this phase studies, refusing to guess when the flag was left off.
|
|
70
|
+
|
|
71
|
+
A later phase reads state a previous one wrote, so falling back to the bundled sample
|
|
72
|
+
is not a harmless default: run ``group1 --part mine.stl`` and then a bare ``group2``,
|
|
73
|
+
and the second phase studies the SAMPLE - scoring it, writing its results, and saying
|
|
74
|
+
nothing. The default therefore stands only while it is unambiguous, which is the demo
|
|
75
|
+
case of the sample being the only study on file.
|
|
76
|
+
"""
|
|
77
|
+
if explicit is not None:
|
|
78
|
+
return explicit
|
|
79
|
+
started = studies_on_file()
|
|
80
|
+
mine = part_key(DEFAULT_PART) if DEFAULT_PART.is_file() else None
|
|
81
|
+
if [k for k in started if k != mine]:
|
|
82
|
+
listed = "\n ".join(started)
|
|
83
|
+
raise SystemExit(
|
|
84
|
+
"this phase continues a study, so it needs to know which one.\n"
|
|
85
|
+
"Pass --part pointing at the geometry you ran group1 on.\n"
|
|
86
|
+
f"Studies on file:\n {listed}"
|
|
87
|
+
)
|
|
88
|
+
return DEFAULT_PART
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_state(state: Path) -> dict:
|
|
92
|
+
return json.loads(state.read_text()) if state.exists() else {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def save_state(state: Path, **kw) -> None:
|
|
96
|
+
st = load_state(state)
|
|
97
|
+
st.update(kw)
|
|
98
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
state.write_text(json.dumps(st, indent=2))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_geometry(part: Path, state: Path) -> Geometry:
|
|
103
|
+
"""Reuse the meshed .cfex if we already have it, else mesh now."""
|
|
104
|
+
cfex = CACHE / f"{part_key(part)}.cfex"
|
|
105
|
+
st = load_state(state)
|
|
106
|
+
if cfex.exists() and st.get("suggested_gate"):
|
|
107
|
+
nodes = parse_cfex_nodes(cfex)
|
|
108
|
+
print(f" reusing meshed {cfex.name} ({len(nodes)} nodes)")
|
|
109
|
+
return Geometry(cfex, nodes, st["suggested_gate"], st["volume_cm3"], st["long_axis"])
|
|
110
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
geo = prepare_geometry(part, cfex)
|
|
112
|
+
# A fresh mesh invalidates the upload that went with the old one: keeping the id would
|
|
113
|
+
# skip the upload and run the new gates against the geometry already in the cloud.
|
|
114
|
+
save_state(
|
|
115
|
+
state,
|
|
116
|
+
geometry_id=None,
|
|
117
|
+
suggested_gate=geo.suggested_gate,
|
|
118
|
+
volume_cm3=geo.volume_cm3,
|
|
119
|
+
long_axis=geo.long_axis,
|
|
120
|
+
)
|
|
121
|
+
return geo
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def make_study(geo: Geometry, part: Path, state: Path) -> pipeline.Study:
|
|
125
|
+
"""One cloud project per part, named after it, reused across the four phases."""
|
|
126
|
+
st = load_state(state)
|
|
127
|
+
study = pipeline.Study(part.stem, geo, part.name, OUTPUT, geometry_id=st.get("geometry_id"))
|
|
128
|
+
study.upload_geometry()
|
|
129
|
+
save_state(state, geometry_id=study.geometry_id)
|
|
130
|
+
return study
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def table(results: list[pipeline.RunResult]) -> None:
|
|
134
|
+
print(
|
|
135
|
+
f"\n {'config':20s} {'g':>2} {'flow':>5} {'maxP(bar)':>9} {'shear(1/s)':>10} "
|
|
136
|
+
f"{'fill%':>6} {'t_fill':>7} {'tail':>5} {'welds':>6} weld axis-pos/angle"
|
|
137
|
+
)
|
|
138
|
+
print(" " + "-" * 116)
|
|
139
|
+
for r in results:
|
|
140
|
+
a = r.analysis
|
|
141
|
+
if a is None:
|
|
142
|
+
print(f" {r.config.name:20s} FAILED: {r.error}")
|
|
143
|
+
continue
|
|
144
|
+
wl = ", ".join(f"{c.axis_pos:.2f}/{c.mean_angle_deg:.0f}" for c in a.weld_clusters[:5]) or "-"
|
|
145
|
+
print(
|
|
146
|
+
f" {r.config.name:20s} {len(r.config.gates_mm):>2} {r.config.flow_rate_cm3_s:>5.0f} "
|
|
147
|
+
f"{a.max_pressure_bar:>9.1f} {a.max_shear_rate_1s:>10.0f} "
|
|
148
|
+
f"{a.fill_pct:>6.1f} {a.total_fill_time_s:>7.2f} {a.tail_fraction:>5.2f} "
|
|
149
|
+
f"{a.n_welds:>6} {wl}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# --------------------------------------------------------------------------
|
|
154
|
+
def group1(study: pipeline.Study, geo: Geometry, state: Path) -> list[pipeline.RunResult]:
|
|
155
|
+
"""Gate-count study at a fixed nominal process (1 vs 2 vs 3 vs 4 gates)."""
|
|
156
|
+
cfgs = [
|
|
157
|
+
study.make_config(
|
|
158
|
+
"1gate_center",
|
|
159
|
+
gate_line(geo, [0.5]),
|
|
160
|
+
flow_rate_cm3_s=NOMINAL_FLOW,
|
|
161
|
+
notes="single central gate (= cadmould suggestion)",
|
|
162
|
+
),
|
|
163
|
+
study.make_config(
|
|
164
|
+
"2gate", gate_line(geo, [0.25, 0.75]), flow_rate_cm3_s=NOMINAL_FLOW, notes="2 gates at 1/4, 3/4 length"
|
|
165
|
+
),
|
|
166
|
+
study.make_config(
|
|
167
|
+
"3gate",
|
|
168
|
+
gate_line(geo, [1 / 6, 0.5, 5 / 6]),
|
|
169
|
+
flow_rate_cm3_s=NOMINAL_FLOW,
|
|
170
|
+
notes="3 gates at 1/6, 1/2, 5/6",
|
|
171
|
+
),
|
|
172
|
+
study.make_config(
|
|
173
|
+
"4gate",
|
|
174
|
+
gate_line(geo, [0.125, 0.375, 0.625, 0.875]),
|
|
175
|
+
flow_rate_cm3_s=NOMINAL_FLOW,
|
|
176
|
+
notes="4 gates evenly",
|
|
177
|
+
),
|
|
178
|
+
]
|
|
179
|
+
res = study.run_group(
|
|
180
|
+
"G1 - gate count (nominal process)",
|
|
181
|
+
cfgs,
|
|
182
|
+
description=(
|
|
183
|
+
f"Gate-count study on {study.stl_name} at fixed {NOMINAL_FLOW:.0f} cm3/s, "
|
|
184
|
+
f"melt {study.melt_K - 273.15:.0f}C, wall {study.wall_K - 273.15:.0f}C. "
|
|
185
|
+
"Trade flow length / pressure against number of weld lines."
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
return res
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def group2(study: pipeline.Study, geo: Geometry, state: Path) -> list[pipeline.RunResult]:
|
|
192
|
+
"""Single-gate placement: sweep position along the length for balanced fill."""
|
|
193
|
+
fracs = [0.40, 0.45, 0.50, 0.55, 0.60]
|
|
194
|
+
cfgs = [
|
|
195
|
+
study.make_config(
|
|
196
|
+
f"pos_{int(f * 100):02d}",
|
|
197
|
+
gate_line(geo, [f]),
|
|
198
|
+
flow_rate_cm3_s=NOMINAL_FLOW,
|
|
199
|
+
notes=f"single gate at {f:.2f} of length",
|
|
200
|
+
)
|
|
201
|
+
for f in fracs
|
|
202
|
+
]
|
|
203
|
+
res = study.run_group(
|
|
204
|
+
"G2 - single-gate placement",
|
|
205
|
+
cfgs,
|
|
206
|
+
description=(
|
|
207
|
+
"Single central gate swept along the part length at fixed "
|
|
208
|
+
f"{NOMINAL_FLOW:.0f} cm3/s. Find the flow-balanced position: "
|
|
209
|
+
"most even fill (lowest tail), lowest peak pressure, welds at the "
|
|
210
|
+
"hidden ends. The part tapers, so the optimum may be off geometric centre."
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
# pick: most even fill (low tail) then lowest pressure, among full fills
|
|
214
|
+
ok = [r for r in res if r.analysis and not r.analysis.short_shot]
|
|
215
|
+
if ok:
|
|
216
|
+
best = min(ok, key=lambda r: (round(r.analysis.tail_fraction, 2), r.analysis.max_pressure_bar))
|
|
217
|
+
save_state(
|
|
218
|
+
state,
|
|
219
|
+
best_gate_frac=float(best.config.name.split("_")[1]) / 100.0,
|
|
220
|
+
best_gate_mm=best.config.gates_mm,
|
|
221
|
+
best_gate_flow=best.config.flow_rate_cm3_s,
|
|
222
|
+
)
|
|
223
|
+
print(
|
|
224
|
+
f"\n >> best single-gate placement: {best.config.name} "
|
|
225
|
+
f"(P={best.analysis.max_pressure_bar:.0f}bar, tail={best.analysis.tail_fraction:.2f}) "
|
|
226
|
+
f"-> saved to state"
|
|
227
|
+
)
|
|
228
|
+
return res
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def chosen_gate(geo: Geometry, state: Path) -> tuple[list[list[float]], str]:
|
|
232
|
+
"""The gate later phases run at, and an honest name for it.
|
|
233
|
+
|
|
234
|
+
Each phase can be run on its own, so group2 may not have happened. Falling back to a
|
|
235
|
+
central gate is the right behaviour — it is a legitimate baseline — but the description
|
|
236
|
+
travels to the cloud as this study's record, so it has to say which of the two it got.
|
|
237
|
+
"""
|
|
238
|
+
gate = load_state(state).get("best_gate_mm")
|
|
239
|
+
if gate:
|
|
240
|
+
return gate, "the gate placement group2 selected"
|
|
241
|
+
return gate_line(geo, [0.5]), "a central gate (group2 has not run, so nothing was selected)"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def group3(study: pipeline.Study, geo: Geometry, state: Path) -> list[pipeline.RunResult]:
|
|
245
|
+
"""Process tuning: flow-rate sweep at the gate group2 chose, or at a central baseline."""
|
|
246
|
+
gate, placement = chosen_gate(geo, state)
|
|
247
|
+
print(f" process sweep at {placement}")
|
|
248
|
+
flows = [25.0, 35.0, 45.0, 60.0, 80.0] # -> ~4.1, 3.0, 2.3, 1.7, 1.3 s fill
|
|
249
|
+
cfgs = [
|
|
250
|
+
study.make_config(
|
|
251
|
+
f"flow_{int(fl):02d}",
|
|
252
|
+
gate,
|
|
253
|
+
flow_rate_cm3_s=fl,
|
|
254
|
+
notes=f"{fl:.0f} cm3/s -> ~{geo.volume_cm3 / fl:.1f} s fill",
|
|
255
|
+
)
|
|
256
|
+
for fl in flows
|
|
257
|
+
]
|
|
258
|
+
res = study.run_group(
|
|
259
|
+
"G3 - process tuning (flow sweep)",
|
|
260
|
+
cfgs,
|
|
261
|
+
description=(
|
|
262
|
+
f"Flow-rate sweep at {placement} to map the process window: "
|
|
263
|
+
"peak pressure and shear vs fill time. Pick the fastest fill that "
|
|
264
|
+
"keeps pressure and shear comfortable."
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
return res
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def group4(study: pipeline.Study, geo: Geometry, state: Path) -> list[pipeline.RunResult]:
|
|
271
|
+
"""Validate the final design: timestep-resolution stability + numerical cross-check."""
|
|
272
|
+
gate, placement = chosen_gate(geo, state)
|
|
273
|
+
print(f" validating at {placement}")
|
|
274
|
+
flow = 45.0
|
|
275
|
+
cfgs = [
|
|
276
|
+
study.make_config(
|
|
277
|
+
"final_ai_50steps",
|
|
278
|
+
gate,
|
|
279
|
+
flow_rate_cm3_s=flow,
|
|
280
|
+
notes=f"final design: {placement}, {flow:.0f} cm3/s, 50 steps",
|
|
281
|
+
),
|
|
282
|
+
study.make_config(
|
|
283
|
+
"final_ai_100steps",
|
|
284
|
+
gate,
|
|
285
|
+
flow_rate_cm3_s=flow,
|
|
286
|
+
num_timesteps=100,
|
|
287
|
+
notes="resolution check: same design at 100 timesteps",
|
|
288
|
+
),
|
|
289
|
+
study.make_config(
|
|
290
|
+
"final_numerical",
|
|
291
|
+
gate,
|
|
292
|
+
flow_rate_cm3_s=flow,
|
|
293
|
+
solver_type="numerical",
|
|
294
|
+
notes="numerical-solver cross-check (best-effort; may be async/unavailable)",
|
|
295
|
+
),
|
|
296
|
+
]
|
|
297
|
+
return study.run_group(
|
|
298
|
+
"G4 - validation (final design)",
|
|
299
|
+
cfgs,
|
|
300
|
+
max_workers=1,
|
|
301
|
+
description=(
|
|
302
|
+
f"Validation of the recommended design ({placement}, at "
|
|
303
|
+
f"{flow:.0f} cm3/s). Confirms metrics are stable under finer time resolution "
|
|
304
|
+
"and cross-checks the AI surrogate against the numerical solver."
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
GROUPS = {"group1": group1, "group2": group2, "group3": group3, "group4": group4}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main() -> int:
|
|
313
|
+
ap = argparse.ArgumentParser(description="gating study driver")
|
|
314
|
+
ap.add_argument("group", choices=sorted(GROUPS), help="which study phase to run")
|
|
315
|
+
# Defaults to None rather than to the sample, so a phase can tell "the user said
|
|
316
|
+
# nothing" from "the user asked for the sample" and refuse to guess between studies.
|
|
317
|
+
ap.add_argument(
|
|
318
|
+
"--part",
|
|
319
|
+
type=Path,
|
|
320
|
+
default=None,
|
|
321
|
+
help=f"STL to study (default: {DEFAULT_PART.name} under sample/)",
|
|
322
|
+
)
|
|
323
|
+
args = ap.parse_args()
|
|
324
|
+
args.part = resolve_part(args.part)
|
|
325
|
+
|
|
326
|
+
if not args.part.is_file():
|
|
327
|
+
print(f"geometry not found: {args.part}", file=sys.stderr)
|
|
328
|
+
print("Pass your own STL with --part, or put one back at sample/simple_plate.stl.", file=sys.stderr)
|
|
329
|
+
return 2
|
|
330
|
+
|
|
331
|
+
state = state_path(args.part)
|
|
332
|
+
geo = get_geometry(args.part, state)
|
|
333
|
+
study = make_study(geo, args.part, state)
|
|
334
|
+
try:
|
|
335
|
+
results = GROUPS[args.group](study, geo, state)
|
|
336
|
+
finally:
|
|
337
|
+
study.close()
|
|
338
|
+
|
|
339
|
+
table(results)
|
|
340
|
+
return 0
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
raise SystemExit(main())
|