simcon-toolkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. simcon_toolkit/__init__.py +40 -0
  2. simcon_toolkit/__main__.py +7 -0
  3. simcon_toolkit/_kit/LICENSE +202 -0
  4. simcon_toolkit/_kit/NOTICE +37 -0
  5. simcon_toolkit/_kit/assets/parts/clip_frame.stl +0 -0
  6. simcon_toolkit/_kit/assets/parts/simple_plate.stl +0 -0
  7. simcon_toolkit/_kit/packages/.ruff.toml +10 -0
  8. simcon_toolkit/_kit/packages/cadmould_cloud/__init__.py +8 -0
  9. simcon_toolkit/_kit/packages/cadmould_cloud/auth.py +681 -0
  10. simcon_toolkit/_kit/packages/cadmould_cloud/client.py +235 -0
  11. simcon_toolkit/_kit/packages/cadmould_geometry/__init__.py +5 -0
  12. simcon_toolkit/_kit/packages/cadmould_geometry/mesh.py +210 -0
  13. simcon_toolkit/_kit/packages/cadmould_geometry/stl.py +168 -0
  14. simcon_toolkit/_kit/packages/cadmould_results/__init__.py +30 -0
  15. simcon_toolkit/_kit/packages/cadmould_results/loader.py +288 -0
  16. simcon_toolkit/_kit/packages/cadmould_scoring/__init__.py +7 -0
  17. simcon_toolkit/_kit/packages/cadmould_scoring/metrics.py +519 -0
  18. simcon_toolkit/_kit/pyproject.toml +232 -0
  19. simcon_toolkit/_kit/templates/_shared/AGENTS.base.md +101 -0
  20. simcon_toolkit/_kit/templates/gate-study/.gitignore +18 -0
  21. simcon_toolkit/_kit/templates/gate-study/AGENTS.md +46 -0
  22. simcon_toolkit/_kit/templates/gate-study/GATING_STUDY_PLAYBOOK.md +219 -0
  23. simcon_toolkit/_kit/templates/gate-study/INITIAL_PROMPT.md +26 -0
  24. simcon_toolkit/_kit/templates/gate-study/README.md +137 -0
  25. simcon_toolkit/_kit/templates/gate-study/main.py +344 -0
  26. simcon_toolkit/_kit/templates/gate-study/pipeline.py +281 -0
  27. simcon_toolkit/_kit/templates/process-window/.gitignore +20 -0
  28. simcon_toolkit/_kit/templates/process-window/AGENTS.md +49 -0
  29. simcon_toolkit/_kit/templates/process-window/METHOD.md +155 -0
  30. simcon_toolkit/_kit/templates/process-window/README.md +176 -0
  31. simcon_toolkit/_kit/templates/process-window/configs/simple-plate.yaml +116 -0
  32. simcon_toolkit/_kit/templates/process-window/doe_spec.schema.md +249 -0
  33. simcon_toolkit/_kit/templates/process-window/main.py +82 -0
  34. simcon_toolkit/_kit/templates/process-window/process_window/__init__.py +5 -0
  35. simcon_toolkit/_kit/templates/process-window/process_window/centre.py +298 -0
  36. simcon_toolkit/_kit/templates/process-window/process_window/design.py +144 -0
  37. simcon_toolkit/_kit/templates/process-window/process_window/economics.py +367 -0
  38. simcon_toolkit/_kit/templates/process-window/process_window/emit.py +591 -0
  39. simcon_toolkit/_kit/templates/process-window/process_window/guardrails.py +153 -0
  40. simcon_toolkit/_kit/templates/process-window/process_window/harness.py +360 -0
  41. simcon_toolkit/_kit/templates/process-window/process_window/identity.py +92 -0
  42. simcon_toolkit/_kit/templates/process-window/process_window/inspect_part.py +184 -0
  43. simcon_toolkit/_kit/templates/process-window/process_window/kpis.py +355 -0
  44. simcon_toolkit/_kit/templates/process-window/process_window/material_card.py +163 -0
  45. simcon_toolkit/_kit/templates/process-window/process_window/probe_proxy.py +169 -0
  46. simcon_toolkit/_kit/templates/process-window/process_window/run_confirm.py +403 -0
  47. simcon_toolkit/_kit/templates/process-window/process_window/run_epsilon_floor.py +198 -0
  48. simcon_toolkit/_kit/templates/process-window/process_window/run_feedback.py +322 -0
  49. simcon_toolkit/_kit/templates/process-window/process_window/run_refine.py +279 -0
  50. simcon_toolkit/_kit/templates/process-window/process_window/run_screening.py +370 -0
  51. simcon_toolkit/_kit/templates/process-window/process_window/run_sweep.py +166 -0
  52. simcon_toolkit/_kit/templates/process-window/process_window/setup_campaign.py +312 -0
  53. simcon_toolkit/_kit/templates/process-window/process_window/surrogate.py +201 -0
  54. simcon_toolkit/_kit/templates/process-window/process_window/test_centre.py +169 -0
  55. simcon_toolkit/_kit/templates/process-window/process_window/test_design.py +113 -0
  56. simcon_toolkit/_kit/templates/process-window/process_window/test_guardrails.py +157 -0
  57. simcon_toolkit/_kit/templates/process-window/process_window/test_surrogate.py +127 -0
  58. simcon_toolkit/_kit/templates/process-window/process_window/units.py +152 -0
  59. simcon_toolkit/_kit/templates/quoting/.gitignore +24 -0
  60. simcon_toolkit/_kit/templates/quoting/AGENTS.md +58 -0
  61. simcon_toolkit/_kit/templates/quoting/INTERVIEW.md +147 -0
  62. simcon_toolkit/_kit/templates/quoting/METHOD.md +256 -0
  63. simcon_toolkit/_kit/templates/quoting/PROMPT.md +46 -0
  64. simcon_toolkit/_kit/templates/quoting/QUOTING_PLAYBOOK.md +245 -0
  65. simcon_toolkit/_kit/templates/quoting/README.md +158 -0
  66. simcon_toolkit/_kit/templates/quoting/main.py +484 -0
  67. simcon_toolkit/_kit/templates/quoting/parts/.gitkeep +0 -0
  68. simcon_toolkit/_kit/templates/quoting/quoting/__init__.py +11 -0
  69. simcon_toolkit/_kit/templates/quoting/quoting/costing.py +725 -0
  70. simcon_toolkit/_kit/templates/quoting/quoting/geometry.py +398 -0
  71. simcon_toolkit/_kit/templates/quoting/quoting/shop.py +193 -0
  72. simcon_toolkit/_kit/templates/quoting/quoting/state.py +260 -0
  73. simcon_toolkit/_kit/templates/quoting/quoting/study.py +577 -0
  74. simcon_toolkit/_kit/templates/quoting/quoting/toolkit.py +50 -0
  75. simcon_toolkit/_kit/templates/quoting/shop/README.md +43 -0
  76. simcon_toolkit/_kit/templates/quoting/shop/commercial.md +86 -0
  77. simcon_toolkit/_kit/templates/quoting/shop/lessons.md +94 -0
  78. simcon_toolkit/_kit/templates/quoting/shop/machines.md +68 -0
  79. simcon_toolkit/_kit/templates/quoting/shop/materials.md +92 -0
  80. simcon_toolkit/_kit/templates/quoting/shop/shop-profile.md +87 -0
  81. simcon_toolkit/_kit/templates/quoting/shop/tooling.md +145 -0
  82. simcon_toolkit/_kit/templates/run-one-simulation/.gitignore +16 -0
  83. simcon_toolkit/_kit/templates/run-one-simulation/AGENTS.md +41 -0
  84. simcon_toolkit/_kit/templates/run-one-simulation/README.md +133 -0
  85. simcon_toolkit/_kit/templates/run-one-simulation/main.py +216 -0
  86. simcon_toolkit/_kit/templates.toml +83 -0
  87. simcon_toolkit/choices.py +11 -0
  88. simcon_toolkit/cli.py +381 -0
  89. simcon_toolkit/generate.py +590 -0
  90. simcon_toolkit/instructions.py +152 -0
  91. simcon_toolkit/manifest.py +86 -0
  92. simcon_toolkit/project.py +356 -0
  93. simcon_toolkit/wizard.py +160 -0
  94. simcon_toolkit-0.1.0.dist-info/METADATA +48 -0
  95. simcon_toolkit-0.1.0.dist-info/RECORD +97 -0
  96. simcon_toolkit-0.1.0.dist-info/WHEEL +4 -0
  97. simcon_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,288 @@
1
+ """Read and slice the AI-Solver injection-molding prediction (HDF5).
2
+
3
+ The prediction file is a 2.5D filling simulation produced by the Simcon AI
4
+ solver. Everything lives in a single ``.h5`` file with this layout::
5
+
6
+ / root attrs: geometry, material, model_id, ...
7
+ /TimeStep_0001 ... _0053 one group per time step
8
+ Time, dt, filling_level (group attrs)
9
+ Degree_of_filling (N,) per-node fill fraction (NaN = not filled yet)
10
+ Pressure (N,) per-node cavity pressure [bar]
11
+ Temperature (N, L) per-node, per-layer melt temperature [K]
12
+ Log(1 + Shear_rate)(N, L) per-node, per-layer log1p of shear rate
13
+ /geometry3DF/Coordinates (N, 3) midplane mesh node coordinates [mm]
14
+ /supporting_points/NodePoints (N, L, 3) through-thickness point cloud [mm]
15
+ /flowrate0 (53,) global injection curve (time, flowrate, p, switch)
16
+ /input process settings + gate location(s)
17
+ /material_data PP material model (pvt / viscosity / thermal)
18
+
19
+ Here ``N`` = number of mesh nodes (6201) and ``L`` = number of layers across
20
+ the wall thickness (41). ``Pressure`` and ``Degree_of_filling`` are stored per
21
+ node only; ``Temperature`` and the shear field additionally resolve the wall
22
+ thickness, so layer 0 is one surface, layer ``L-1`` the other, and the middle
23
+ index is the core.
24
+
25
+ The class below is the one object you work with. It opens the file lazily and
26
+ hands you NumPy arrays via small, well-named slicing methods ("chunks") so the
27
+ template scripts never touch h5py directly.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from dataclasses import dataclass
33
+ from pathlib import Path
34
+
35
+ import h5py
36
+ import numpy as np
37
+
38
+ # --- canonical field names as stored in each TimeStep group -----------------
39
+ DEGREE_OF_FILLING = "Degree_of_filling"
40
+ PRESSURE = "Pressure"
41
+ TEMPERATURE = "Temperature"
42
+ SHEAR_LOG = "Log(1 + Shear_rate)" # legacy: log-encoded shear (de-log with expm1)
43
+ SHEAR_RATE = "Shear_rate" # current: straight shear rate [1/s]
44
+
45
+ #: fields stored as (N,) -> one value per node
46
+ NODE_FIELDS = (DEGREE_OF_FILLING, PRESSURE)
47
+ #: fields stored as (N, L) -> one value per node and wall layer (both shear
48
+ #: spellings listed so membership tests work whichever the file carries)
49
+ LAYERED_FIELDS = (TEMPERATURE, SHEAR_LOG, SHEAR_RATE)
50
+ ALL_FIELDS = NODE_FIELDS + LAYERED_FIELDS
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class ProcessInputs:
55
+ """Process settings the prediction was run with (from /input attrs)."""
56
+
57
+ filling_time_s: float
58
+ flowrate: float
59
+ melt_temperature_c: float
60
+ wall_temperature_c: float
61
+ gate_xyz: np.ndarray # (G, 3) gate location(s) in mm
62
+
63
+
64
+ class PredictionResult:
65
+ """A read-only view over one AI-Solver prediction file.
66
+
67
+ Use it as a context manager so the underlying HDF5 handle is closed::
68
+
69
+ with PredictionResult("part_prediction.h5") as res:
70
+ snap = res.snapshot(step=29) # all fields at one time step
71
+ p_gate = res.node_history(PRESSURE, node=res.gate_node)
72
+ """
73
+
74
+ def __init__(self, path: str | Path) -> None:
75
+ """Open a prediction file and read its time axis.
76
+
77
+ Args:
78
+ path: The prediction ``.h5`` to open. It stays open until ``close``.
79
+ """
80
+ self.path = Path(path)
81
+ self._f = h5py.File(self.path, "r")
82
+
83
+ # Ordered list of TimeStep group names (the file is already sorted, but
84
+ # never rely on dict order in HDF5 -- sort explicitly).
85
+ self.timestep_names: list[str] = sorted(k for k in self._f if k.startswith("TimeStep_"))
86
+
87
+ # The geometry group name has a trailing space in the source file; find
88
+ # it robustly instead of hard-coding the quirk.
89
+ self._geom_key = next(k for k in self._f if k.strip() == "geometry3DF")
90
+
91
+ # Time axis (seconds), one entry per time step.
92
+ self.times: np.ndarray = np.array(
93
+ [float(self._f[name].attrs["Time"]) for name in self.timestep_names],
94
+ dtype=np.float64,
95
+ )
96
+
97
+ # -- lifecycle -----------------------------------------------------------
98
+ def close(self) -> None:
99
+ """Close the underlying HDF5 file."""
100
+ self._f.close()
101
+
102
+ def __enter__(self) -> PredictionResult:
103
+ """Enter a ``with`` block; the file is already open."""
104
+ return self
105
+
106
+ def __exit__(self, *exc: object) -> None:
107
+ """Leave a ``with`` block, closing the file. Exceptions propagate."""
108
+ self.close()
109
+
110
+ # -- basic dimensions / metadata ----------------------------------------
111
+ @property
112
+ def meta(self) -> dict:
113
+ """Root attributes: geometry name, material, solver/model id, ..."""
114
+ return {k: _decode(v) for k, v in self._f.attrs.items()}
115
+
116
+ @property
117
+ def n_timesteps(self) -> int:
118
+ """How many time steps the file recorded."""
119
+ return len(self.timestep_names)
120
+
121
+ @property
122
+ def n_nodes(self) -> int:
123
+ """How many midplane mesh nodes the geometry has."""
124
+ return self._f[self._geom_key]["Coordinates"].shape[0]
125
+
126
+ @property
127
+ def n_layers(self) -> int:
128
+ """How many through-thickness layers each layered field carries."""
129
+ return self._f[self.timestep_names[0]][TEMPERATURE].shape[1]
130
+
131
+ @property
132
+ def coordinates(self) -> np.ndarray:
133
+ """Midplane mesh node coordinates, shape (N, 3), in mm."""
134
+ return np.asarray(self._f[self._geom_key]["Coordinates"][()], dtype=np.float32)
135
+
136
+ @property
137
+ def node_points(self) -> np.ndarray:
138
+ """Through-thickness point cloud, shape (N, L, 3), in mm."""
139
+ return np.asarray(self._f["supporting_points"]["NodePoints"][()], dtype=np.float32)
140
+
141
+ @property
142
+ def has_inputs(self) -> bool:
143
+ """Whether the file carries an `/input` group (process settings + gate location).
144
+
145
+ Cloud results omit it unless augmented — see the run-one-simulation template.
146
+ """
147
+ return "input" in self._f
148
+
149
+ @property
150
+ def inputs(self) -> ProcessInputs:
151
+ """The process settings and gate location the run was submitted with.
152
+
153
+ Raises:
154
+ KeyError: If the file carries no ``/input`` group; check ``has_inputs``.
155
+ """
156
+ g = self._f["input"]
157
+ gates = np.asarray(g["gate_locations"][()])
158
+ gate_xyz = np.column_stack([gates["x"], gates["y"], gates["z"]]).astype(np.float32)
159
+ return ProcessInputs(
160
+ filling_time_s=float(g.attrs["filling_time"]),
161
+ flowrate=float(g.attrs["flowrate"]),
162
+ melt_temperature_c=float(g.attrs["melt_temperature"]),
163
+ wall_temperature_c=float(g.attrs["wall_temperature"]),
164
+ gate_xyz=gate_xyz,
165
+ )
166
+
167
+ @property
168
+ def gate_node(self) -> int:
169
+ """Index of the mesh node closest to the (first) gate location."""
170
+ gate = self.inputs.gate_xyz[0]
171
+ d = np.linalg.norm(self.coordinates - gate, axis=1)
172
+ return int(np.argmin(d))
173
+
174
+ @property
175
+ def flowrate_curve(self) -> np.ndarray:
176
+ """Global injection curve, as a structured array.
177
+
178
+ Its fields are ``time``, ``flowrate_calculated``, ``pressure`` and ``switch``.
179
+ """
180
+ return np.asarray(self._f["flowrate0"][()])
181
+
182
+ def field_unit(self, field: str) -> str:
183
+ """Unit string stored on a field (e.g. 'bar', 'Pa', 'K', '1/s', 'relative')."""
184
+ ds = self._f[self.timestep_names[0]][field]
185
+ return _decode(ds.attrs.get("unit", ""))
186
+
187
+ def has_field(self, field: str) -> bool:
188
+ """True if ``field`` exists in the time-step groups (the result format varies)."""
189
+ return field in self._f[self.timestep_names[0]]
190
+
191
+ def shear_name(self) -> str | None:
192
+ """Name of the shear field this file carries, or None if it carries neither.
193
+
194
+ Either ``Shear_rate`` (current, straight 1/s) or ``Log(1 + Shear_rate)`` (legacy,
195
+ log-encoded).
196
+ """
197
+ for n in (SHEAR_RATE, SHEAR_LOG):
198
+ if self.has_field(n):
199
+ return n
200
+ return None
201
+
202
+ def pressure_series_bar(self) -> np.ndarray:
203
+ """Pressure over all steps in **bar**, regardless of stored unit (Pa or bar)."""
204
+ s = self.field_series(PRESSURE)
205
+ return s / 1.0e5 if self.field_unit(PRESSURE).strip().lower() == "pa" else s
206
+
207
+ # -- time indexing -------------------------------------------------------
208
+ def step_at_time(self, t_seconds: float) -> int:
209
+ """Return the time-step index whose Time is closest to ``t_seconds``."""
210
+ return int(np.argmin(np.abs(self.times - t_seconds)))
211
+
212
+ def _group(self, step: int) -> h5py.Group:
213
+ return self._f[self.timestep_names[step]]
214
+
215
+ # -- CHUNK 1: a single time step (a "snapshot") --------------------------
216
+ def field_at(self, field: str, step: int) -> np.ndarray:
217
+ """One field at one time step.
218
+
219
+ Shape is (N,) for node fields and (N, L) for layered fields.
220
+ Unfilled nodes are NaN. float16 storage is promoted to float32.
221
+ """
222
+ return np.asarray(self._group(step)[field][()], dtype=np.float32)
223
+
224
+ def snapshot(self, step: int) -> dict:
225
+ """All fields at one time step, plus that step's time/fill metadata."""
226
+ g = self._group(step)
227
+ data = {f: self.field_at(f, step) for f in ALL_FIELDS if self.has_field(f)}
228
+ data["time"] = float(g.attrs["Time"])
229
+ data["filled_fraction"] = float(np.isfinite(data[DEGREE_OF_FILLING]).mean())
230
+ return data
231
+
232
+ # -- CHUNK 2: a field across the whole simulation ------------------------
233
+ def field_series(self, field: str) -> np.ndarray:
234
+ """Stack one field over every time step.
235
+
236
+ Shape is (T, N) for node fields and (T, N, L) for layered fields.
237
+ """
238
+ return np.stack([self.field_at(field, s) for s in range(self.n_timesteps)])
239
+
240
+ # -- CHUNK 3: the time history at a single node (a virtual sensor) -------
241
+ def node_history(self, field: str, node: int, layer: int | None = None) -> np.ndarray:
242
+ """Time history of ``field`` at one mesh node, shape (T,).
243
+
244
+ For layered fields pass ``layer`` (defaults to the core, L//2).
245
+ """
246
+ if field in LAYERED_FIELDS:
247
+ if layer is None:
248
+ layer = self.n_layers // 2
249
+ return np.array([self.field_at(field, s)[node, layer] for s in range(self.n_timesteps)])
250
+ return np.array([self.field_at(field, s)[node] for s in range(self.n_timesteps)])
251
+
252
+ # -- CHUNK 4: one through-thickness layer over time ----------------------
253
+ def layer_series(self, field: str, layer: int) -> np.ndarray:
254
+ """A single wall layer of a layered field over time, shape (T, N)."""
255
+ if field not in LAYERED_FIELDS:
256
+ raise ValueError(f"{field!r} has no layers; it is stored per node only.")
257
+ return np.stack([self.field_at(field, s)[:, layer] for s in range(self.n_timesteps)])
258
+
259
+ # -- derived helpers -----------------------------------------------------
260
+ def fill_progress(self) -> np.ndarray:
261
+ """Fraction of nodes reached by the flow front at each step, shape (T,)."""
262
+ return np.array([np.isfinite(self.field_at(DEGREE_OF_FILLING, s)).mean() for s in range(self.n_timesteps)])
263
+
264
+ def shear_rate(self, step: int) -> np.ndarray:
265
+ """Shear rate at a step, regardless of how the file stores it.
266
+
267
+ Current files carry a straight ``Shear_rate`` field [1/s] -> returned as-is.
268
+ Legacy files carry ``Log(1 + Shear_rate)`` -> de-logged with ``expm1`` (the
269
+ legacy unit label is '100s', i.e. units of 100 1/s).
270
+ """
271
+ name = self.shear_name()
272
+ if name is None:
273
+ raise KeyError("no shear field (Shear_rate / Log(1 + Shear_rate)) in this file")
274
+ s = self.field_at(name, step)
275
+ return np.expm1(s) if name == SHEAR_LOG else s
276
+
277
+
278
+ def _decode(v: object) -> object:
279
+ """Turn HDF5 bytes/0-d arrays into plain Python values for printing.
280
+
281
+ The input is an arbitrary HDF5 attribute value, so the body narrows by type rather
282
+ than assuming one.
283
+ """
284
+ if isinstance(v, bytes):
285
+ return v.decode("utf-8", "replace")
286
+ if isinstance(v, np.ndarray) and v.shape == ():
287
+ return v.item()
288
+ return v
@@ -0,0 +1,7 @@
1
+ """Score a filling result: pressure, fill evenness and weld-line detection.
2
+
3
+ from cadmould_scoring import metrics
4
+
5
+ Nothing is imported here on purpose, so a file that wants one module pays for one
6
+ module. See ``cadmould_scoring.metrics`` for the documented measures.
7
+ """