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,133 @@
|
|
|
1
|
+
# Run one simulation
|
|
2
|
+
|
|
3
|
+
The shortest path from a part file to a cloud result, using the `cadmould.cloud` SDK.
|
|
4
|
+
One script runs the whole loop:
|
|
5
|
+
|
|
6
|
+
**mesh locally → upload geometry → pick material → run AI filling → read peak pressure.**
|
|
7
|
+
|
|
8
|
+
The SDK handles the cloud login for you — there is no token to manage. It just has to run
|
|
9
|
+
inside an active licence session, and `main.py` wraps the whole script in one.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
<!-- simcon-toolkit:setup -->
|
|
14
|
+
**Python 3.12 or newer**, and a virtual environment created *with that interpreter* — a venv
|
|
15
|
+
isolates packages, not the interpreter.
|
|
16
|
+
|
|
17
|
+
**Three sign-ins, each in your browser, and each does a different job.**
|
|
18
|
+
|
|
19
|
+
1. **The package index**, once, in the steps below: `cadmould-sdk-auth` lets pip install the
|
|
20
|
+
licensed `cadmould` wheel from SIMCON's private index. Your company's email domain has to be
|
|
21
|
+
enabled for it: if you can sign in but the browser tab then says `Access denied: your email
|
|
22
|
+
domain is not authorized for this API`, ask SIMCON support.
|
|
23
|
+
2. **Your licence**, the first time the code opens a licence session. It first checks the
|
|
24
|
+
Thales Sentinel run-time on your machine, and prints `License init failed: HASP driver
|
|
25
|
+
version too old` or `... runtime version too old` when that needs installing or updating.
|
|
26
|
+
3. **The cloud**, the first time the code calls it.
|
|
27
|
+
|
|
28
|
+
⚠️ A failed licence sign-in does not stop the script. It prints `License init failed: ...` or
|
|
29
|
+
`License acquisition failed.` to the terminal and carries on, so the error you meet later is `Cadmould API licence (LICENCE_API)
|
|
30
|
+
required`. When you see that one, scroll up to the first.
|
|
31
|
+
|
|
32
|
+
**macOS / Linux (bash):**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python3.12 -m venv .venv
|
|
36
|
+
source .venv/bin/activate
|
|
37
|
+
pip install --upgrade pip cadmould-sdk-auth
|
|
38
|
+
cadmould-sdk-auth # opens a browser; sign in with your Simcon account
|
|
39
|
+
pip install . # installs cadmould and everything this script needs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Windows (PowerShell):**
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
py -3.12 -m venv .venv
|
|
46
|
+
.\.venv\Scripts\python.exe -m pip install --upgrade pip cadmould-sdk-auth
|
|
47
|
+
$env:VIRTUAL_ENV = "$PWD\.venv" # tell the login CLI which venv to configure
|
|
48
|
+
.\.venv\Scripts\cadmould-sdk-auth.exe # opens a browser; sign in with your Simcon account
|
|
49
|
+
.\.venv\Scripts\python.exe -m pip install .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The Windows route never activates the environment. `Activate.ps1` is blocked by the default
|
|
53
|
+
execution policy on many machines; naming the interpreter outright avoids it.
|
|
54
|
+
<!-- /simcon-toolkit:setup -->
|
|
55
|
+
|
|
56
|
+
## Run it
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python main.py # uses the bundled sample part
|
|
60
|
+
python main.py --part path/to/part.stl # your own geometry
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The first cloud call opens a browser once so you can sign in. Output lands in `build/`:
|
|
64
|
+
the meshed `.cfex` and the result `.h5`. The script prints the peak cavity pressure and the
|
|
65
|
+
id of the simulation it ran.
|
|
66
|
+
|
|
67
|
+
## View the result
|
|
68
|
+
|
|
69
|
+
Open <https://cadmould.simcon.ai> and find the simulation by the id the run printed. There
|
|
70
|
+
is nothing to install — the viewer is part of the platform.
|
|
71
|
+
|
|
72
|
+
*(The script cannot give you a direct link: the platform returns one, but the SDK does not
|
|
73
|
+
yet expose it. That is a known gap on our side, not something you have configured wrongly.)*
|
|
74
|
+
|
|
75
|
+
## Work with the numbers
|
|
76
|
+
|
|
77
|
+
The downloaded `.h5` is written **complete**. Cloud results carry the field data only, so
|
|
78
|
+
`main.py` adds back an `/input` group holding the gate position and the process settings the
|
|
79
|
+
run was given — meaning the file still tells you what it was asked for, months later.
|
|
80
|
+
|
|
81
|
+
Read it with the `cadmould_results` package that came with this project:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from cadmould_results import PRESSURE, PredictionResult
|
|
85
|
+
|
|
86
|
+
with PredictionResult("build/<the file>.h5") as res:
|
|
87
|
+
snap = res.snapshot(step=res.n_timesteps - 1) # every field at the last step
|
|
88
|
+
at_gate = res.node_history(PRESSURE, node=res.gate_node) # pressure at the gate over time
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## What it shows
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import cadmould
|
|
95
|
+
from cadmould import cloud
|
|
96
|
+
|
|
97
|
+
with cadmould.Session.user_based(): # ONE licence session…
|
|
98
|
+
cloud.set_base_url("https://api.simcon.ai", cloud.OidcEnvironment.Production) # …gated, so inside it
|
|
99
|
+
geometry_id = cloud.geometry.upload_geometry("part.cfex") # upload once
|
|
100
|
+
mat = cloud.material.get_materials("PP Generic")[0] # find a material
|
|
101
|
+
sim = cloud.simulation.create_filling_simulation( # run (AI = synchronous)
|
|
102
|
+
geometry_id=geometry_id, material_id=mat.material_id, ...)
|
|
103
|
+
sim.download("build/") # fetch the result .h5
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**The one rule worth remembering:** `cadmould.cloud` needs an active licence session carrying
|
|
107
|
+
the API entitlement, and `set_base_url` is gated too. Use a *single* `Session.user_based()`
|
|
108
|
+
for both the meshing and the cloud calls. Do not nest sessions — an inner one releases the
|
|
109
|
+
licence when it exits, and every later cloud call then fails.
|
|
110
|
+
|
|
111
|
+
## The sample part
|
|
112
|
+
|
|
113
|
+
`sample/` holds one geometry so the script runs the moment you generate the project. It is
|
|
114
|
+
ours, not yours. Replace the file with your own and `python main.py` keeps working; delete
|
|
115
|
+
the folder and you must pass a path every time, which is a perfectly good way to work.
|
|
116
|
+
|
|
117
|
+
## Changing the cloud address
|
|
118
|
+
|
|
119
|
+
The script targets production, and that is the cloud you have. SIMCON engineers can point it
|
|
120
|
+
elsewhere without editing the source:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
CADMOULD_API_BASE_URL=https://api.dev.simcon.ai python main.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The sign-in configuration and the web app address both travel with it, so a run and the page
|
|
127
|
+
you look at it on can never disagree. An address the script does not recognise is refused,
|
|
128
|
+
with the known ones named.
|
|
129
|
+
|
|
130
|
+
## Prefer your own client?
|
|
131
|
+
|
|
132
|
+
`cadmould.cloud` is the convenient default, but the platform is a plain REST API and you are
|
|
133
|
+
free to build your own client in any language — see <https://api.simcon.ai/scalar>.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Run one AI filling simulation on the cloud — via the cadmould.cloud SDK.
|
|
2
|
+
|
|
3
|
+
The minimal end-to-end on the **recommended default** cloud path: mesh a part locally,
|
|
4
|
+
upload it, pick a material, run an AI filling simulation, and read the peak pressure from
|
|
5
|
+
the result. Everything runs inside one `cadmould.Session.user_based()`: `cadmould.cloud`
|
|
6
|
+
requires an active licence session (with the API entitlement). `set_base_url` selects the
|
|
7
|
+
OIDC config and the SDK opens a browser once to sign in.
|
|
8
|
+
|
|
9
|
+
Run it from this folder:
|
|
10
|
+
|
|
11
|
+
python main.py # uses sample/simple_plate.stl
|
|
12
|
+
python main.py --part path/to/part.stl # your own geometry
|
|
13
|
+
|
|
14
|
+
The downloaded result is written viewer-ready (an `/input` group is added back), so the
|
|
15
|
+
arrays read back together with the process settings that produced them — the layout
|
|
16
|
+
`cadmould_results.ProcessInputs` reads.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import contextlib
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
import h5py
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
import cadmould
|
|
31
|
+
from cadmould import cloud
|
|
32
|
+
from cadmould_geometry import stl
|
|
33
|
+
|
|
34
|
+
HERE = Path(__file__).resolve().parent
|
|
35
|
+
DEFAULT_PART = HERE / "sample" / "simple_plate.stl"
|
|
36
|
+
BUILD = HERE / "build"
|
|
37
|
+
|
|
38
|
+
# The clouds that exist. Production is the only one a customer has; the override is for
|
|
39
|
+
# SIMCON engineers. An address carries its own sign-in configuration AND its own web app,
|
|
40
|
+
# so they are kept in one row: naming an address can never pair a run with the wrong
|
|
41
|
+
# tenant or send you to a viewer that does not hold the result. An unrecognised address
|
|
42
|
+
# is refused rather than guessed at.
|
|
43
|
+
CLOUDS = {
|
|
44
|
+
"https://api.simcon.ai": (cloud.OidcEnvironment.Production, "https://cadmould.simcon.ai"),
|
|
45
|
+
"https://api.dev.simcon.ai": (cloud.OidcEnvironment.Development, "https://cadmould.dev.simcon.ai"),
|
|
46
|
+
}
|
|
47
|
+
DEFAULT_API = "https://api.simcon.ai"
|
|
48
|
+
|
|
49
|
+
MATERIAL = "PP Generic"
|
|
50
|
+
FLOW_CM3_S = 45.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cloud_target() -> tuple[str, cloud.OidcEnvironment, str]:
|
|
54
|
+
"""The API address, its sign-in configuration and its web app — always from one row."""
|
|
55
|
+
raw = os.environ.get("CADMOULD_API_BASE_URL", DEFAULT_API).strip().rstrip("/")
|
|
56
|
+
# The SDK wants the API root. The REST templates use CLOUD_BASE_URL, which includes the
|
|
57
|
+
# /api/v1 prefix, so accept that spelling too rather than reading it as a fourth cloud.
|
|
58
|
+
base = raw.removesuffix("/api/v1")
|
|
59
|
+
if base not in CLOUDS:
|
|
60
|
+
raise ValueError(f"CADMOULD_API_BASE_URL={raw!r} is not a Cadmould cloud. Known: {', '.join(sorted(CLOUDS))}")
|
|
61
|
+
oidc, web_app = CLOUDS[base]
|
|
62
|
+
return base, oidc, web_app
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _mesh_classes():
|
|
66
|
+
"""Resolve the meshing classes from whichever namespace the wheel exposes."""
|
|
67
|
+
mesh_mod = getattr(cadmould, "mesh", None)
|
|
68
|
+
io_mod = getattr(mesh_mod, "io", None) if mesh_mod else None
|
|
69
|
+
return (
|
|
70
|
+
getattr(mesh_mod, "Mesh", None) or cadmould.domain.Mesh,
|
|
71
|
+
getattr(mesh_mod, "Mesher", None) or cadmould.tools.Mesher,
|
|
72
|
+
getattr(io_mod, "CfexWriter", None) or cadmould.io.CfexWriter,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def write_input_group(h5_path, gate_mm, flow_cm3_s, melt_c, wall_c, fill_time_s) -> None:
|
|
77
|
+
"""Make a cloud result self-contained by writing back the submitted process settings.
|
|
78
|
+
|
|
79
|
+
Cloud results carry the primary fields only — no `/input` group — so the gate positions and
|
|
80
|
+
process settings that produced them are lost the moment the file is downloaded. Write them
|
|
81
|
+
back in the layout `cadmould_results.ProcessInputs` expects (gate `/input/gate_locations` +
|
|
82
|
+
process attrs), so anything reading the result later can see what was asked for.
|
|
83
|
+
"""
|
|
84
|
+
gl_dt = np.dtype([("x", "<f8"), ("y", "<f8"), ("z", "<f8"), ("kno", "<i4")])
|
|
85
|
+
with h5py.File(h5_path, "a") as f:
|
|
86
|
+
if "input" in f:
|
|
87
|
+
return
|
|
88
|
+
g = f.create_group("input")
|
|
89
|
+
g.attrs["filling_time"] = float(fill_time_s)
|
|
90
|
+
g.attrs["flowrate"] = float(flow_cm3_s)
|
|
91
|
+
g.attrs["melt_temperature"] = float(melt_c) # °C (ProcessInputs reads °C)
|
|
92
|
+
g.attrs["wall_temperature"] = float(wall_c) # °C
|
|
93
|
+
gl = np.zeros(len(gate_mm), dtype=gl_dt)
|
|
94
|
+
for i, (x, y, z) in enumerate(gate_mm):
|
|
95
|
+
gl["x"][i], gl["y"][i], gl["z"][i], gl["kno"][i] = x, y, z, -1
|
|
96
|
+
g.create_dataset("gate_locations", data=gl)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def peak_pressure_bar(h5_path: str) -> float:
|
|
100
|
+
"""Peak per-node Pressure over all steps, in bar (honours the stored `unit`)."""
|
|
101
|
+
pmax, unit = 0.0, ""
|
|
102
|
+
with h5py.File(h5_path, "r") as f:
|
|
103
|
+
for k in f:
|
|
104
|
+
if k.startswith("TimeStep_") and "Pressure" in f[k]:
|
|
105
|
+
ds = f[k]["Pressure"]
|
|
106
|
+
unit = unit or str(ds.attrs.get("unit", ""))
|
|
107
|
+
pr = np.asarray(ds[()], dtype=np.float64)
|
|
108
|
+
if np.isfinite(pr).any():
|
|
109
|
+
pmax = max(pmax, float(np.nanmax(pr)))
|
|
110
|
+
return pmax / 1e5 if unit.strip().lower() == "pa" else pmax
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
114
|
+
parser = argparse.ArgumentParser(description="Run one AI filling simulation on the Cadmould cloud.")
|
|
115
|
+
# Named rather than positional, and the same spelling in every template: these
|
|
116
|
+
# workflows will grow inputs, and a flag keeps that from reordering anyone's command.
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--part",
|
|
119
|
+
type=Path,
|
|
120
|
+
default=DEFAULT_PART,
|
|
121
|
+
help="STL geometry to run. Defaults to the bundled sample/simple_plate.stl.",
|
|
122
|
+
)
|
|
123
|
+
return parser.parse_args(argv)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main(argv: list[str] | None = None) -> int:
|
|
127
|
+
# Windows consoles default to cp1252 and choke on the non-ASCII glyphs below
|
|
128
|
+
# (…, ≈, →, °, ³) — switch stdout to UTF-8 so the final print can't crash a finished run.
|
|
129
|
+
with contextlib.suppress(Exception):
|
|
130
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
131
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
args = parse_args(argv)
|
|
134
|
+
part = args.part
|
|
135
|
+
if not part.is_file():
|
|
136
|
+
print(f"geometry not found: {part}", file=sys.stderr)
|
|
137
|
+
print("Pass your own STL with --part, or put one back at sample/simple_plate.stl.", file=sys.stderr)
|
|
138
|
+
return 2
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
base_url, oidc, web_app = cloud_target()
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
print(exc, file=sys.stderr)
|
|
144
|
+
return 2
|
|
145
|
+
|
|
146
|
+
BUILD.mkdir(exist_ok=True)
|
|
147
|
+
Mesh, Mesher, CfexWriter = _mesh_classes()
|
|
148
|
+
|
|
149
|
+
# cadmould.cloud needs an ACTIVE licence session carrying the API entitlement
|
|
150
|
+
# (LICENCE_API). Run meshing AND every cloud call — including set_base_url, which is
|
|
151
|
+
# gated too — inside ONE session. Don't nest or close it early: an inner session's
|
|
152
|
+
# exit releases the licence, leaving later cloud calls unlicensed.
|
|
153
|
+
with cadmould.Session.user_based():
|
|
154
|
+
# 1. Mesh the part locally and let the SDK suggest a gate (a real mesh node).
|
|
155
|
+
print("1. meshing the part (local cadmould)…")
|
|
156
|
+
pts, faces = stl.read_stl(part)
|
|
157
|
+
volume_cm3 = stl.volume_mm3(pts, faces) / 1000.0
|
|
158
|
+
meshed = Mesher(Mesh(pts.ravel(), faces.ravel())).with_surface_triangulation().with_thickness_and_w2w().build()
|
|
159
|
+
gate = cadmould.tools.suggest_gates(meshed).points[0]
|
|
160
|
+
gate_mm = [[float(gate.x), float(gate.y), float(gate.z)]]
|
|
161
|
+
cfex = BUILD / "part.cfex"
|
|
162
|
+
CfexWriter().save(meshed, str(cfex))
|
|
163
|
+
print(f" {meshed.node_count()} nodes; suggested gate {[round(v, 1) for v in gate_mm[0]]}")
|
|
164
|
+
|
|
165
|
+
# 2. Point the SDK at the cloud (set_base_url is gated → must be inside the session).
|
|
166
|
+
print("2. connecting to the cloud (cadmould.cloud)…")
|
|
167
|
+
cloud.set_base_url(base_url, oidc)
|
|
168
|
+
|
|
169
|
+
# 3. Upload the geometry once → geometry_id.
|
|
170
|
+
geometry_id = cloud.geometry.upload_geometry(str(cfex), description="run-one-simulation")
|
|
171
|
+
print(f" geometry_id = {geometry_id}")
|
|
172
|
+
|
|
173
|
+
# 4. Resolve the material and its recommended temperatures.
|
|
174
|
+
mats = cloud.material.get_materials(MATERIAL)
|
|
175
|
+
mat = next((m for m in mats if m.name.lower() == MATERIAL.lower()), mats[0])
|
|
176
|
+
ver = cloud.material.get_material_version(mat.versions[0].material_version_id)
|
|
177
|
+
print(
|
|
178
|
+
f" material {mat.name}: melt {ver.suggested_mass_temp_K - 273.15:.0f}°C, "
|
|
179
|
+
f"mould {ver.suggested_wall_temp_K - 273.15:.0f}°C"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# 5. Run an AI filling simulation (synchronous — completes on return).
|
|
183
|
+
print("3. running the AI filling simulation…")
|
|
184
|
+
sim = cloud.simulation.create_filling_simulation(
|
|
185
|
+
geometry_id=geometry_id,
|
|
186
|
+
material_id=mat.material_id,
|
|
187
|
+
melt_temperature_K=ver.suggested_mass_temp_K,
|
|
188
|
+
wall_temperature_K=ver.suggested_wall_temp_K,
|
|
189
|
+
flow_rate_cm3_s=FLOW_CM3_S,
|
|
190
|
+
gate_positions_mm=gate_mm,
|
|
191
|
+
estimated_filling_time_s=volume_cm3 / FLOW_CM3_S,
|
|
192
|
+
)
|
|
193
|
+
simulation_id = sim.id
|
|
194
|
+
print(f" simulation {simulation_id} — status {sim.status}")
|
|
195
|
+
|
|
196
|
+
# 6. Download the result (still inside the session).
|
|
197
|
+
h5_path = sim.download(str(BUILD))
|
|
198
|
+
|
|
199
|
+
# Reading + augmenting the result needs no licence — do it after the session is released.
|
|
200
|
+
# Write back the submitted inputs so the result carries what it was run with.
|
|
201
|
+
write_input_group(
|
|
202
|
+
h5_path,
|
|
203
|
+
gate_mm,
|
|
204
|
+
FLOW_CM3_S,
|
|
205
|
+
ver.suggested_mass_temp_K - 273.15,
|
|
206
|
+
ver.suggested_wall_temp_K - 273.15,
|
|
207
|
+
volume_cm3 / FLOW_CM3_S,
|
|
208
|
+
)
|
|
209
|
+
print(f"4. result → {h5_path}\n peak cavity pressure ≈ {peak_pressure_bar(h5_path):.1f} bar")
|
|
210
|
+
# The SDK exposes no viewer link, so name the id and let the reader open it themselves.
|
|
211
|
+
print(f" view it: open {web_app} and find simulation {simulation_id}")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# What each template is, stated once.
|
|
2
|
+
#
|
|
3
|
+
# The filesystem is the truth about which templates exist; this file holds the
|
|
4
|
+
# metadata a directory cannot carry. tests/test_repository_layout.py fails when the
|
|
5
|
+
# two disagree in either direction, and when a value here names something absent.
|
|
6
|
+
#
|
|
7
|
+
# Every field is derived from the template's own code, not chosen:
|
|
8
|
+
# client "sdk" when the workflow reaches the cloud through the licensed cadmould
|
|
9
|
+
# wheel, "rest" when it calls the platform API over HTTP itself. It records
|
|
10
|
+
# the cloud client only — a "rest" template may still use the wheel for
|
|
11
|
+
# meshing or for reading a licensed material file.
|
|
12
|
+
# needs "licence" when any code path reaches the licensed cadmould wheel — the
|
|
13
|
+
# template's own, or a shared module it imports; "cloud" when it calls the
|
|
14
|
+
# platform. Descriptive, so a reader knows what a run requires. All four
|
|
15
|
+
# need both today: cadmould_geometry.mesh.prepare_geometry opens a licence
|
|
16
|
+
# session, so importing it is enough even where the template never says
|
|
17
|
+
# `import cadmould` itself.
|
|
18
|
+
# heavy true when the template pulls the graphics stack. Nothing does any more:
|
|
19
|
+
# quoting was the last user and its ray-casting moved to trimesh.
|
|
20
|
+
# modules the shared packages the generator copies into the project, each under its
|
|
21
|
+
# own name:
|
|
22
|
+
# those the code imports, plus any the template's own README tells the
|
|
23
|
+
# reader to use.
|
|
24
|
+
# package the folder inside the template holding the workflow's code. Absent when
|
|
25
|
+
# the template is a single script at its root.
|
|
26
|
+
# sample the file under assets/parts/ this template's generated sample/ receives.
|
|
27
|
+
# Two templates share a geometry, which is why it is a mapping here rather
|
|
28
|
+
# than a committed folder.
|
|
29
|
+
# category unused today; the hook for a future menu grouping. Present now because
|
|
30
|
+
# adding it later would mean touching every entry.
|
|
31
|
+
|
|
32
|
+
[templates.run-one-simulation]
|
|
33
|
+
title = "Run one simulation"
|
|
34
|
+
description = "Run one AI filling simulation via the cadmould.cloud SDK"
|
|
35
|
+
category = "quickstart"
|
|
36
|
+
client = "sdk"
|
|
37
|
+
needs = ["licence", "cloud"]
|
|
38
|
+
heavy = false
|
|
39
|
+
# cadmould_results is not imported by the workflow; the template's README instructs the
|
|
40
|
+
# reader to open the result with it, so a generated project without it is broken.
|
|
41
|
+
modules = ["cadmould_geometry", "cadmould_results"]
|
|
42
|
+
sample = "simple_plate.stl"
|
|
43
|
+
|
|
44
|
+
[templates.gate-study]
|
|
45
|
+
title = "Gate study"
|
|
46
|
+
description = "Drive a full gating decision, with its own REST client"
|
|
47
|
+
category = "study"
|
|
48
|
+
client = "rest"
|
|
49
|
+
# The study drives the cloud over REST, but it meshes first: main.py imports
|
|
50
|
+
# prepare_geometry, which opens a licence session.
|
|
51
|
+
needs = ["licence", "cloud"]
|
|
52
|
+
heavy = false
|
|
53
|
+
modules = ["cadmould_cloud", "cadmould_geometry", "cadmould_scoring"]
|
|
54
|
+
# No `package`: the workflow is main.py plus one module at the template root. An inner
|
|
55
|
+
# folder would become a package name in the customer's project, and two files do not
|
|
56
|
+
# earn one.
|
|
57
|
+
sample = "simple_plate.stl"
|
|
58
|
+
|
|
59
|
+
[templates.process-window]
|
|
60
|
+
title = "Process window"
|
|
61
|
+
description = "A rigorous design-of-experiments campaign"
|
|
62
|
+
category = "study"
|
|
63
|
+
client = "rest"
|
|
64
|
+
# The campaign talks to the cloud over REST, but four scripts use the wheel for work the
|
|
65
|
+
# platform exposes no endpoint for: meshing, inspection, the numerical .rm1 path, and
|
|
66
|
+
# reading a licensed .plb material card.
|
|
67
|
+
needs = ["licence", "cloud"]
|
|
68
|
+
heavy = false
|
|
69
|
+
modules = ["cadmould_cloud", "cadmould_geometry", "cadmould_scoring"]
|
|
70
|
+
package = "process_window"
|
|
71
|
+
sample = "simple_plate.stl"
|
|
72
|
+
|
|
73
|
+
[templates.quoting]
|
|
74
|
+
title = "Quoting"
|
|
75
|
+
description = "An agentic quoting flow with its own, self-improving context"
|
|
76
|
+
category = "business"
|
|
77
|
+
client = "rest"
|
|
78
|
+
# Meshing opens a licence session before any cloud call.
|
|
79
|
+
needs = ["licence", "cloud"]
|
|
80
|
+
heavy = false
|
|
81
|
+
modules = ["cadmould_cloud", "cadmould_geometry", "cadmould_scoring"]
|
|
82
|
+
package = "quoting"
|
|
83
|
+
sample = "clip_frame.stl"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""The package managers a project can be set up for, read by both the CLI and the generator.
|
|
2
|
+
|
|
3
|
+
Its own module, importing nothing, because `cli.py` must not import the manifest before it
|
|
4
|
+
has checked the interpreter, and the generator must not import the CLI. One list, so the flag
|
|
5
|
+
and the generator cannot disagree about what is accepted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
PACKAGE_MANAGERS = ("uv", "pip")
|
|
11
|
+
DEFAULT_PACKAGE_MANAGER = "uv"
|