loadpath 0.1.1__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.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-material briefs: a part printed in two (or more) materials, such as a
|
|
3
|
+
rigid core with a TPU grip pad, a bracket with a rubber isolating layer, or
|
|
4
|
+
a PolyJet Vero body with an Agilus30 skin.
|
|
5
|
+
|
|
6
|
+
The agent hands back one body per material (a multi-material 3MF, or one
|
|
7
|
+
STL per material). The scorer reads the bodies and re-analyses them under
|
|
8
|
+
the GOLD study, ignoring whatever the agent claims:
|
|
9
|
+
|
|
10
|
+
pass = the bodies are in materials the brief allows (at least
|
|
11
|
+
pass.minMaterials of them), do not overlap, and form one
|
|
12
|
+
connected part; the part is inside the envelope, clear of every
|
|
13
|
+
gold keep-out and has every gold interface; every gold load case
|
|
14
|
+
reads each material body's safety factor (multimat.
|
|
15
|
+
verify_multimaterial: per-cell material, across-layer strength
|
|
16
|
+
for FDM) >= SF_SLACK x pass.safetyFactor and every interface
|
|
17
|
+
between materials (the bond card from interface_library, face
|
|
18
|
+
by face) >= SF_SLACK x pass.interfaceSafetyFactor; each
|
|
19
|
+
pass.zones entry (where the brief needs a soft or a rigid
|
|
20
|
+
material) is filled and made of the materials it names; each
|
|
21
|
+
pass.isolate entry has no rigid path between its two regions;
|
|
22
|
+
and the mass is under pass.maxMass.
|
|
23
|
+
|
|
24
|
+
The gold answer: brief["reference"], a list of {material, shape} solids
|
|
25
|
+
(earlier ones win where they overlap, keep-outs are cut away), is the
|
|
26
|
+
minimum defensible design; gold(brief) builds it, writes it as a 3MF the
|
|
27
|
+
way loadpath does and scores it, so the gold passes its own scorer.
|
|
28
|
+
|
|
29
|
+
Fields of a multimat brief beyond the default kind:
|
|
30
|
+
gold a study with a multiMaterial block (the materials
|
|
31
|
+
the reading uses, any interface override)
|
|
32
|
+
reference the gold design as [{material, shape}]
|
|
33
|
+
pass.safetyFactor, pass.interfaceSafetyFactor, pass.maxMass
|
|
34
|
+
pass.materials materials the delivery may use (any others fail)
|
|
35
|
+
pass.minMaterials default 2
|
|
36
|
+
pass.zones [{id, region | shape, materials, minShare, minFill?}]
|
|
37
|
+
minShare: of the part inside the zone, the volume
|
|
38
|
+
share in `materials`; minFill: of the zone (less
|
|
39
|
+
keep-outs), the share that is part at all
|
|
40
|
+
pass.isolate [{id, from, to, materials}]: no connected body of
|
|
41
|
+
`materials` touches both regions (a rubber layer
|
|
42
|
+
that must break every rigid path)
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import glob
|
|
47
|
+
import os
|
|
48
|
+
import re
|
|
49
|
+
import tempfile
|
|
50
|
+
import time
|
|
51
|
+
import xml.etree.ElementTree as ET
|
|
52
|
+
import zipfile
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
|
|
56
|
+
from ..briefs import (
|
|
57
|
+
ASSETS,
|
|
58
|
+
MASS_SLACK,
|
|
59
|
+
MAX_INTRUSION,
|
|
60
|
+
MAX_MISSING,
|
|
61
|
+
MAX_OUTSIDE,
|
|
62
|
+
SF_SLACK,
|
|
63
|
+
gold_study,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
DELIVER = """ part.3mf the part as one multi-material 3MF, one body per material, in the coordinates
|
|
67
|
+
the brief uses (loadpath writes it so); or, instead, one STL per material named
|
|
68
|
+
part-<material>.stl (e.g. part-petg.stl, part-tpu.stl), also in the brief's coordinates
|
|
69
|
+
study.json the study that produced it
|
|
70
|
+
notes.md two or three lines: the mass, the safety factor of each material and of the bond
|
|
71
|
+
between them, which material is where, and your assumptions
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
VERIFY_CELLS = 60_000
|
|
75
|
+
MAX_OVERLAP = 30.0 # mm^3 two bodies may share (a mesh's facets, not a double-printed volume)
|
|
76
|
+
MIN_CONNECTED = 0.97 # share of the delivered volume the analysis must keep (attached to the supports)
|
|
77
|
+
VOXEL_CELLS = 400_000 # connectivity grid (pieces, rigid paths)
|
|
78
|
+
INTERLOCK_RATIO = 1.8 # raw / smoothed interface area that reads as an interlocking lattice
|
|
79
|
+
INTERLOCK_SMOOTH = 2.5 # mm: the majority-vote box (a lattice period is two 0.8 mm beams)
|
|
80
|
+
|
|
81
|
+
_UNITS = {"micron": 1e-3, "millimeter": 1.0, "centimeter": 10.0, "inch": 25.4, "foot": 304.8, "meter": 1000.0}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------- reading
|
|
85
|
+
|
|
86
|
+
def _material_key(name):
|
|
87
|
+
"""A material name, alias or file-name fragment as a library key, or None."""
|
|
88
|
+
from ...materials import material_from_spec
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return material_from_spec(str(name).strip()).name
|
|
92
|
+
except (ValueError, TypeError):
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _stem_material(stem):
|
|
97
|
+
"""The material a file name ends with: part-petg, finger_tpu95a, x-pa-cf."""
|
|
98
|
+
from ...materials import _ALIASES, MATERIALS
|
|
99
|
+
|
|
100
|
+
s = stem.lower()
|
|
101
|
+
best = None
|
|
102
|
+
for key in list(MATERIALS) + list(_ALIASES):
|
|
103
|
+
for k in {key, key.replace(" ", "-"), key.replace(" ", "_"), key.replace(" ", "")}:
|
|
104
|
+
hit = s == k or re.search(rf"(^|[-_. ]){re.escape(k)}($|[-_. ]t?\d*$)", s)
|
|
105
|
+
if hit and (best is None or len(k) > len(best[0])):
|
|
106
|
+
best = (k, _material_key(key))
|
|
107
|
+
return best[1] if best else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _matrix(text):
|
|
111
|
+
"""A 3MF transform attribute (12 numbers, row vectors) as a 4x4 column-vector matrix."""
|
|
112
|
+
if not text:
|
|
113
|
+
return np.eye(4)
|
|
114
|
+
v = np.array([float(x) for x in text.split()], float).reshape(4, 3)
|
|
115
|
+
m = np.eye(4)
|
|
116
|
+
m[:3, :3] = v[:3].T
|
|
117
|
+
m[:3, 3] = v[3]
|
|
118
|
+
return m
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _local(tag):
|
|
122
|
+
return tag.rsplit("}", 1)[-1]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def read_3mf(path):
|
|
126
|
+
"""The bodies of a 3MF: [(material key or None, vertices in object space,
|
|
127
|
+
faces, transform from object space through components, build transform)].
|
|
128
|
+
Materials come from core basematerials (object pid/pindex, triangle
|
|
129
|
+
pid/p1), else from the object's name or loadpath:material metadata.
|
|
130
|
+
Production-extension components (p:path into another model file, the
|
|
131
|
+
Bambu layout) are followed."""
|
|
132
|
+
with zipfile.ZipFile(path) as z:
|
|
133
|
+
models = {"/" + n.lstrip("/"): z.read(n) for n in z.namelist() if n.lower().endswith(".model")}
|
|
134
|
+
cfg = z.read("Metadata/Slic3r_PE_model.config") if "Metadata/Slic3r_PE_model.config" in z.namelist() \
|
|
135
|
+
else None
|
|
136
|
+
volumes = {} # PrusaSlicer: object id -> [(first, last) triangle of each volume]
|
|
137
|
+
if cfg is not None:
|
|
138
|
+
for o in ET.fromstring(cfg):
|
|
139
|
+
if _local(o.tag) == "object":
|
|
140
|
+
volumes[o.get("id")] = [(int(v.get("firstid")), int(v.get("lastid"))) for v in o
|
|
141
|
+
if _local(v.tag) == "volume"]
|
|
142
|
+
roots, objects, bases, units = {}, {}, {}, {}
|
|
143
|
+
for name, data in models.items():
|
|
144
|
+
root = ET.fromstring(data)
|
|
145
|
+
roots[name] = root
|
|
146
|
+
units[name] = _UNITS.get(root.get("unit", "millimeter"), 1.0)
|
|
147
|
+
for res in root:
|
|
148
|
+
if _local(res.tag) != "resources":
|
|
149
|
+
continue
|
|
150
|
+
for el in res:
|
|
151
|
+
tag = _local(el.tag)
|
|
152
|
+
if tag == "basematerials":
|
|
153
|
+
bases[(name, el.get("id"))] = [b.get("name") for b in el if _local(b.tag) == "base"]
|
|
154
|
+
elif tag == "object":
|
|
155
|
+
objects[(name, el.get("id"))] = el
|
|
156
|
+
|
|
157
|
+
def mat_of(model, pid, idx, fallback):
|
|
158
|
+
names = bases.get((model, pid))
|
|
159
|
+
if names is not None and idx is not None and 0 <= int(idx) < len(names):
|
|
160
|
+
k = _material_key(names[int(idx)]) or _stem_material(names[int(idx)])
|
|
161
|
+
if k:
|
|
162
|
+
return k
|
|
163
|
+
return fallback
|
|
164
|
+
|
|
165
|
+
out = []
|
|
166
|
+
|
|
167
|
+
def expand(model, oid, tf, build):
|
|
168
|
+
el = objects.get((model, oid))
|
|
169
|
+
if el is None:
|
|
170
|
+
raise ValueError(f"3MF object {oid} not found in {model}")
|
|
171
|
+
meta = {(_local(m.tag), m.get("name")): (m.text or "") for m in el.iter() if _local(m.tag) == "metadata"}
|
|
172
|
+
fallback = None
|
|
173
|
+
for (_, k), v in meta.items():
|
|
174
|
+
if k and k.endswith("material"):
|
|
175
|
+
fallback = _material_key(v) or fallback
|
|
176
|
+
fallback = fallback or _stem_material(el.get("name", "") or "")
|
|
177
|
+
for child in el:
|
|
178
|
+
tag = _local(child.tag)
|
|
179
|
+
if tag == "mesh":
|
|
180
|
+
vs, ts = None, []
|
|
181
|
+
for part in child:
|
|
182
|
+
if _local(part.tag) == "vertices":
|
|
183
|
+
vs = np.array([[float(v.get("x")), float(v.get("y")), float(v.get("z"))] for v in part],
|
|
184
|
+
float) * units[model]
|
|
185
|
+
elif _local(part.tag) == "triangles":
|
|
186
|
+
ts = [(int(t.get("v1")), int(t.get("v2")), int(t.get("v3")),
|
|
187
|
+
t.get("pid") or el.get("pid"), t.get("p1") if t.get("pid") or t.get("p1")
|
|
188
|
+
else el.get("pindex")) for t in part]
|
|
189
|
+
if vs is None or not ts:
|
|
190
|
+
continue
|
|
191
|
+
faces = np.array([t[:3] for t in ts], np.int64)
|
|
192
|
+
keys = np.array([str(mat_of(model, t[3], t[4], fallback)) for t in ts])
|
|
193
|
+
segs = volumes.get(el.get("id")) if model == "/3D/3dmodel.model" else None
|
|
194
|
+
for a, z_ in segs or [(0, len(ts) - 1)]:
|
|
195
|
+
for k in sorted(set(keys[a:z_ + 1])):
|
|
196
|
+
sel = np.zeros(len(ts), bool)
|
|
197
|
+
sel[a:z_ + 1] = keys[a:z_ + 1] == k
|
|
198
|
+
out.append((None if k == "None" else k, vs, faces[sel], tf, build))
|
|
199
|
+
elif tag == "components":
|
|
200
|
+
for c in child:
|
|
201
|
+
ref = None
|
|
202
|
+
for a, v in c.attrib.items():
|
|
203
|
+
if _local(a) == "path":
|
|
204
|
+
ref = "/" + v.lstrip("/")
|
|
205
|
+
expand(ref or model, c.get("objectid"), tf @ _matrix(c.get("transform")), build)
|
|
206
|
+
|
|
207
|
+
for name, root in roots.items():
|
|
208
|
+
for b in root:
|
|
209
|
+
if _local(b.tag) != "build":
|
|
210
|
+
continue
|
|
211
|
+
for item in b:
|
|
212
|
+
if _local(item.tag) == "item":
|
|
213
|
+
expand(name, item.get("objectid"), np.eye(4), _matrix(item.get("transform")))
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _mesh(vs, faces, tf):
|
|
218
|
+
import trimesh
|
|
219
|
+
|
|
220
|
+
used, inv = np.unique(faces.ravel(), return_inverse=True)
|
|
221
|
+
v = vs[used] @ tf[:3, :3].T + tf[:3, 3]
|
|
222
|
+
m = trimesh.Trimesh(v, inv.reshape(-1, 3), process=True)
|
|
223
|
+
if m.volume < 0:
|
|
224
|
+
m.invert()
|
|
225
|
+
return m
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def read_delivery(folder, envelope=None):
|
|
229
|
+
"""[(material key, trimesh)] from the agent's folder: part.3mf (any .3mf
|
|
230
|
+
if there is no part.3mf), else STLs named by material. A 3MF keeps its
|
|
231
|
+
meshes in design coordinates and the print orientation in the build
|
|
232
|
+
item (as loadpath writes it); one that baked the bed placement into the
|
|
233
|
+
vertices is read with the build transform when that lands it inside the
|
|
234
|
+
envelope instead. Returns (bodies, source, warnings)."""
|
|
235
|
+
warn = []
|
|
236
|
+
tmf = [os.path.join(folder, "part.3mf")] if os.path.exists(os.path.join(folder, "part.3mf")) else \
|
|
237
|
+
sorted(glob.glob(os.path.join(folder, "*.3mf")))
|
|
238
|
+
if tmf:
|
|
239
|
+
raw = read_3mf(tmf[0])
|
|
240
|
+
if not raw:
|
|
241
|
+
raise ValueError(f"{os.path.basename(tmf[0])} has no mesh")
|
|
242
|
+
|
|
243
|
+
def place(with_build):
|
|
244
|
+
return [(k, _mesh(v, f, (b @ t) if with_build else t)) for k, v, f, t, b in raw]
|
|
245
|
+
|
|
246
|
+
bodies = place(False)
|
|
247
|
+
if envelope is not None and any(not np.allclose(b, np.eye(4)) for *_, b in raw):
|
|
248
|
+
lo, hi = envelope
|
|
249
|
+
|
|
250
|
+
def inside(bs):
|
|
251
|
+
v = np.vstack([m.vertices for _, m in bs])
|
|
252
|
+
return float(np.all((v >= lo - 1.0) & (v <= hi + 1.0), axis=1).mean())
|
|
253
|
+
|
|
254
|
+
alt = place(True)
|
|
255
|
+
if inside(alt) > inside(bodies) + 0.2:
|
|
256
|
+
bodies = alt
|
|
257
|
+
warn.append("3MF vertices read through the build transform (they are not in design coordinates)")
|
|
258
|
+
return bodies, os.path.basename(tmf[0]), warn
|
|
259
|
+
import trimesh
|
|
260
|
+
|
|
261
|
+
bodies = []
|
|
262
|
+
for p in sorted(glob.glob(os.path.join(folder, "*.stl"))):
|
|
263
|
+
k = _stem_material(os.path.splitext(os.path.basename(p))[0])
|
|
264
|
+
if k is None:
|
|
265
|
+
warn.append(f"{os.path.basename(p)}: no material in the file name, ignored")
|
|
266
|
+
continue
|
|
267
|
+
bodies.append((k, trimesh.load(p, force="mesh")))
|
|
268
|
+
if not bodies:
|
|
269
|
+
raise ValueError("no part.3mf and no STL named by material (part-<material>.stl)")
|
|
270
|
+
return bodies, "stl", warn
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ---------------------------------------------------------------- analysis
|
|
274
|
+
|
|
275
|
+
def _man(mesh):
|
|
276
|
+
from ...shapes import manifold_from_trimesh
|
|
277
|
+
|
|
278
|
+
return manifold_from_trimesh(mesh)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _solids(bodies):
|
|
282
|
+
"""[(material, manifold, trimesh)] for each delivered body with volume.
|
|
283
|
+
Bodies of one material stay separate: two that share a face do not fuse
|
|
284
|
+
into one closed shell, and a shell pair would break ray-parity voxels."""
|
|
285
|
+
from ...shapes import trimesh_from_manifold
|
|
286
|
+
|
|
287
|
+
out = []
|
|
288
|
+
for k, m in bodies:
|
|
289
|
+
man = _man(m)
|
|
290
|
+
if man.volume() > 1e-6:
|
|
291
|
+
out.append((k, man, trimesh_from_manifold(man)))
|
|
292
|
+
return out
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _voxels(solids, cells=VOXEL_CELLS):
|
|
296
|
+
"""The bodies on one grid: per cell the index into `solids` (-1 void),
|
|
297
|
+
earlier bodies first. Returns (label, origin, pitch)."""
|
|
298
|
+
from ...verify import voxelize
|
|
299
|
+
|
|
300
|
+
lo = np.min([m.bounds[0] for _, _, m in solids], axis=0)
|
|
301
|
+
hi = np.max([m.bounds[1] for _, _, m in solids], axis=0)
|
|
302
|
+
pitch = max(float(np.prod(hi - lo) / cells) ** (1.0 / 3.0), 0.1)
|
|
303
|
+
label, origin = None, None
|
|
304
|
+
for i, (_, _, m) in enumerate(solids):
|
|
305
|
+
s, origin = voxelize(m, pitch, box=(lo, hi))
|
|
306
|
+
if label is None:
|
|
307
|
+
label = np.full(s.shape, -1, np.int64)
|
|
308
|
+
label[s & (label < 0)] = i
|
|
309
|
+
return label, origin, pitch
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _interlock_ratio(solids, label, pitch, width=INTERLOCK_SMOOTH):
|
|
313
|
+
"""How much more interface the bodies have than their smoothed layout:
|
|
314
|
+
material faces between different materials on the voxel grid, over the
|
|
315
|
+
same count once each material's indicator is box-averaged over `width`
|
|
316
|
+
mm (a majority vote). About 1 for a flat or curved joint, several times
|
|
317
|
+
that for an interlocking beam lattice. None with one material."""
|
|
318
|
+
from scipy import ndimage
|
|
319
|
+
|
|
320
|
+
if len({k for k, _, _ in solids}) < 2:
|
|
321
|
+
return None
|
|
322
|
+
keys = sorted({k for k, _, _ in solids})
|
|
323
|
+
mat = np.full(label.shape, -1, np.int64)
|
|
324
|
+
for i, (k, _, _) in enumerate(solids):
|
|
325
|
+
mat[label == i] = keys.index(k)
|
|
326
|
+
solid = mat >= 0
|
|
327
|
+
n = max(3, round(width / pitch) | 1)
|
|
328
|
+
den = ndimage.uniform_filter(solid.astype(np.float32), n)
|
|
329
|
+
votes = np.stack([ndimage.uniform_filter((mat == j).astype(np.float32), n) / np.maximum(den, 1e-6)
|
|
330
|
+
for j in range(len(keys))])
|
|
331
|
+
smooth = np.where(solid, votes.argmax(axis=0), -1)
|
|
332
|
+
|
|
333
|
+
def faces(m):
|
|
334
|
+
c = 0
|
|
335
|
+
for ax in range(3):
|
|
336
|
+
a = np.moveaxis(m, ax, 0)
|
|
337
|
+
x, y = a[1:], a[:-1]
|
|
338
|
+
c += int(((x >= 0) & (y >= 0) & (x != y)).sum())
|
|
339
|
+
return c
|
|
340
|
+
|
|
341
|
+
raw, sm = faces(mat), faces(smooth)
|
|
342
|
+
return raw / sm if sm else None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _shape_of(study, z):
|
|
346
|
+
from ...shapes import shape_from_spec
|
|
347
|
+
|
|
348
|
+
return study.region_shape(z["region"])[0] if "region" in z else shape_from_spec(z["shape"], ASSETS)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _zone_shape(study, z):
|
|
352
|
+
from ...sharp import exact_manifold
|
|
353
|
+
|
|
354
|
+
return exact_manifold(_shape_of(study, z))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def analyse(brief, bodies, max_cells=VERIFY_CELLS):
|
|
358
|
+
"""Re-analyse delivered [(material key, trimesh)] under the gold study.
|
|
359
|
+
Returns the score row (without "pass")."""
|
|
360
|
+
import manifold3d as m3d
|
|
361
|
+
|
|
362
|
+
from ...multimat import parse_multimaterial, verify_multimaterial
|
|
363
|
+
from ...verify import _preserve_missing, _union
|
|
364
|
+
|
|
365
|
+
want = brief["pass"]
|
|
366
|
+
study = gold_study(brief)
|
|
367
|
+
gmm = brief["gold"].get("multiMaterial") or {}
|
|
368
|
+
row = {"failures": []}
|
|
369
|
+
f = row["failures"]
|
|
370
|
+
merged = _solids(bodies)
|
|
371
|
+
if not merged:
|
|
372
|
+
f.append("no solid body")
|
|
373
|
+
return row
|
|
374
|
+
unknown = [k for k, _, _ in merged if k is None]
|
|
375
|
+
if unknown:
|
|
376
|
+
f.append(f"{len(unknown)} body/bodies with no material")
|
|
377
|
+
merged = [b for b in merged if b[0] is not None]
|
|
378
|
+
names = list(dict.fromkeys(k for k, _, _ in merged))
|
|
379
|
+
row["materials"] = names
|
|
380
|
+
allowed = [_material_key(m) for m in want.get("materials", [])]
|
|
381
|
+
if allowed:
|
|
382
|
+
bad = [k for k in names if k not in allowed]
|
|
383
|
+
if bad:
|
|
384
|
+
f.append(f"material(s) {bad} not what the brief asks for ({sorted(set(allowed))})")
|
|
385
|
+
if len(names) < want.get("minMaterials", 2):
|
|
386
|
+
f.append(f"{len(names)} material(s); the brief needs {want.get('minMaterials', 2)}")
|
|
387
|
+
|
|
388
|
+
# geometry on the whole part
|
|
389
|
+
part = m3d.Manifold.batch_boolean([m for _, m, _ in merged], m3d.OpType.Add) if len(merged) > 1 \
|
|
390
|
+
else merged[0][1]
|
|
391
|
+
vol = float(part.volume())
|
|
392
|
+
body_vol = sum(float(m.volume()) for _, m, _ in merged)
|
|
393
|
+
row["overlap"] = round(body_vol - vol, 1)
|
|
394
|
+
if row["overlap"] > MAX_OVERLAP:
|
|
395
|
+
f.append(f"bodies overlap by {row['overlap']} mm^3")
|
|
396
|
+
# connectivity on voxels: bodies that only share a face stay apart in manifold3d
|
|
397
|
+
from scipy import ndimage
|
|
398
|
+
|
|
399
|
+
label, origin, vpitch = _voxels(merged)
|
|
400
|
+
comp, _ = ndimage.label(label >= 0)
|
|
401
|
+
sizes = np.bincount(comp.ravel())[1:]
|
|
402
|
+
row["bodies"] = int((sizes * vpitch ** 3 > 1.0).sum())
|
|
403
|
+
if row["bodies"] != 1:
|
|
404
|
+
f.append(f"{row['bodies']} separate pieces")
|
|
405
|
+
design = _union(study.design_space + [r.shape for r in study.preserve])
|
|
406
|
+
keep = _union([r.shape for r in study.keep_out])
|
|
407
|
+
pres = _union([r.shape for r in study.preserve])
|
|
408
|
+
row["keepOutIntrusion"] = round(float((part ^ keep).volume()) if keep is not None else 0.0, 1)
|
|
409
|
+
row["outsideDesignSpace"] = round(float((part - design).volume()), 1)
|
|
410
|
+
if pres is not None:
|
|
411
|
+
from ...shapes import trimesh_from_manifold
|
|
412
|
+
row["preserveMissing"] = round(_preserve_missing(pres - keep if keep is not None else pres, part,
|
|
413
|
+
trimesh_from_manifold(part)), 1)
|
|
414
|
+
else:
|
|
415
|
+
row["preserveMissing"] = 0.0
|
|
416
|
+
if row["outsideDesignSpace"] > MAX_OUTSIDE:
|
|
417
|
+
f.append(f"{row['outsideDesignSpace']} mm^3 outside the envelope")
|
|
418
|
+
if row["keepOutIntrusion"] > MAX_INTRUSION:
|
|
419
|
+
f.append(f"{row['keepOutIntrusion']} mm^3 in keep-outs")
|
|
420
|
+
if row["preserveMissing"] > MAX_MISSING:
|
|
421
|
+
f.append(f"{row['preserveMissing']} mm^3 of interfaces missing")
|
|
422
|
+
|
|
423
|
+
# where each material has to be
|
|
424
|
+
zones = {}
|
|
425
|
+
for z in want.get("zones", []):
|
|
426
|
+
zm = _zone_shape(study, z)
|
|
427
|
+
if keep is not None:
|
|
428
|
+
zm = zm - keep
|
|
429
|
+
ok = {_material_key(m) for m in z["materials"]}
|
|
430
|
+
inside = {}
|
|
431
|
+
for k, m, _ in merged:
|
|
432
|
+
inside[k] = inside.get(k, 0.0) + float((m ^ zm).volume())
|
|
433
|
+
tot = sum(inside.values())
|
|
434
|
+
share = sum(v for k, v in inside.items() if k in ok) / tot if tot > 0 else 0.0
|
|
435
|
+
fill = tot / max(float(zm.volume()), 1e-9)
|
|
436
|
+
zones[z["id"]] = {"share": round(share, 3), "fill": round(fill, 3)}
|
|
437
|
+
if share < z.get("minShare", 0.9):
|
|
438
|
+
f.append(f"zone '{z['id']}': {share:.0%} {'/'.join(sorted(ok))} (needs {z.get('minShare', 0.9):.0%})")
|
|
439
|
+
if z.get("minFill") and fill < z["minFill"]:
|
|
440
|
+
f.append(f"zone '{z['id']}': only {fill:.0%} filled (needs {z['minFill']:.0%})")
|
|
441
|
+
row["zones"] = zones
|
|
442
|
+
centres = None
|
|
443
|
+
for iso in want.get("isolate", []):
|
|
444
|
+
ok = {_material_key(m) for m in iso["materials"]}
|
|
445
|
+
hard = np.isin(label, [i for i, (k, _, _) in enumerate(merged) if k in ok])
|
|
446
|
+
comp, _ = ndimage.label(hard)
|
|
447
|
+
if centres is None:
|
|
448
|
+
idx = np.argwhere(label >= 0)
|
|
449
|
+
centres = (idx, origin + (idx + 0.5) * vpitch)
|
|
450
|
+
idx, pts = centres
|
|
451
|
+
c = comp[tuple(idx.T)]
|
|
452
|
+
a = c[_shape_of(study, iso["from"]).contains(pts) & (c > 0)]
|
|
453
|
+
b = c[_shape_of(study, iso["to"]).contains(pts) & (c > 0)]
|
|
454
|
+
bridged = bool(np.intersect1d(np.unique(a), np.unique(b)).size)
|
|
455
|
+
row.setdefault("isolated", {})[iso["id"]] = not bridged
|
|
456
|
+
if bridged:
|
|
457
|
+
f.append(f"'{iso['id']}': a rigid path joins the two sides ({'/'.join(sorted(ok))})")
|
|
458
|
+
|
|
459
|
+
# the FEA: every material and every bond, per gold case
|
|
460
|
+
mats = list(dict.fromkeys(names + [_material_key(m) for m in gmm.get("materials", [])]))[:4]
|
|
461
|
+
if len(mats) < 2:
|
|
462
|
+
mats.append("tpu" if mats[0] != "tpu" else "petg")
|
|
463
|
+
block = {"materials": mats}
|
|
464
|
+
for k in ("interfaces", "interlock", "interfaceSafetyFactor", "requirements"):
|
|
465
|
+
if gmm.get(k) is not None:
|
|
466
|
+
block[k] = gmm[k]
|
|
467
|
+
# An interlocked joint (the materials interpenetrate in a beam lattice)
|
|
468
|
+
# is credited the way loadpath credits it, but only when the geometry
|
|
469
|
+
# shows one: the agent saying so is not enough.
|
|
470
|
+
ratio = _interlock_ratio(merged, label, vpitch)
|
|
471
|
+
row["interfaceRoughness"] = round(ratio, 2) if ratio else None
|
|
472
|
+
if ratio and ratio >= INTERLOCK_RATIO and "interlock" not in block:
|
|
473
|
+
block["interlock"] = True
|
|
474
|
+
row["interlockDetected"] = True
|
|
475
|
+
mm = parse_multimaterial(block, study, ASSETS)
|
|
476
|
+
order = sorted(range(len(merged)), key=lambda i: -mm.materials[mm.index(merged[i][0])].E)
|
|
477
|
+
vb = [(mm.index(merged[i][0]), merged[i][2]) for i in order] # stiff first: shared cells go rigid
|
|
478
|
+
t0 = time.time()
|
|
479
|
+
try:
|
|
480
|
+
rep = verify_multimaterial(study, mm, vb, max_cells=max_cells)
|
|
481
|
+
except (ValueError, RuntimeError) as exc:
|
|
482
|
+
# verify keeps only what is attached to the supports: a load left on a loose piece lands here
|
|
483
|
+
msg = str(exc).strip().splitlines()[-1] if str(exc).strip() else type(exc).__name__
|
|
484
|
+
f.append(f"analysis failed (is the loaded part attached to the supports?): {msg[:200]}")
|
|
485
|
+
return row
|
|
486
|
+
row["verifySeconds"] = round(time.time() - t0, 1)
|
|
487
|
+
row["analysisPitch"] = round(rep["analysisPitch"], 3)
|
|
488
|
+
kept = rep["cells"] * rep["analysisPitch"] ** 3 / max(vol, 1e-9)
|
|
489
|
+
row["analysedShare"] = round(kept, 3)
|
|
490
|
+
if kept < MIN_CONNECTED:
|
|
491
|
+
f.append(f"only {kept:.0%} of the part is attached to the supports")
|
|
492
|
+
row["mass"] = round(rep["mass"], 2)
|
|
493
|
+
row["materialMass"] = {k: round(v, 2) for k, v in rep["materialMass"].items()}
|
|
494
|
+
row["interfaceArea"] = {k: round(v, 1) for k, v in rep["interfaceArea"].items() if v > 0}
|
|
495
|
+
sf, sfi = want.get("safetyFactor"), want.get("interfaceSafetyFactor", want.get("safetyFactor"))
|
|
496
|
+
row["cases"] = []
|
|
497
|
+
for c in rep["cases"]:
|
|
498
|
+
rc = {"name": c["name"],
|
|
499
|
+
"materials": {k: round(v["safetyFactor"], 2) for k, v in c["materials"].items()},
|
|
500
|
+
"interfaces": {k: round(min(v["safetyFactor"], 1e3), 2) for k, v in c["interfaces"].items()}}
|
|
501
|
+
row["cases"].append(rc)
|
|
502
|
+
for k, v in rc["materials"].items():
|
|
503
|
+
if sf and v < SF_SLACK * sf:
|
|
504
|
+
f.append(f"{k} SF {v:.2f} < {sf} in '{c['name']}'")
|
|
505
|
+
for k, v in rc["interfaces"].items():
|
|
506
|
+
if sfi and v < SF_SLACK * sfi:
|
|
507
|
+
f.append(f"{k} bond SF {v:.2f} < {sfi} in '{c['name']}'")
|
|
508
|
+
for key in ("knock", "indentation"):
|
|
509
|
+
if key in c and not c[key]["meets"]:
|
|
510
|
+
f.append(f"'{c['name']}' {key} requirement not met")
|
|
511
|
+
row["safetyFactor"] = round(rep["safetyFactor"], 3)
|
|
512
|
+
row["interfaceSafetyFactor"] = round(min(rep["interfaceSafetyFactor"], 1e3), 3)
|
|
513
|
+
if len(names) >= 2 and not row["interfaceArea"]:
|
|
514
|
+
f.append("the materials never meet (no interface)")
|
|
515
|
+
mmax = want.get("maxMass")
|
|
516
|
+
if mmax and rep["mass"] > MASS_SLACK * mmax:
|
|
517
|
+
f.append(f"{rep['mass']:.1f} g > {mmax} g")
|
|
518
|
+
return row
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def score(brief, folder, max_cells=VERIFY_CELLS) -> dict:
|
|
522
|
+
"""Read the agent's bodies from `folder` and re-analyse them under the
|
|
523
|
+
gold study (see the module docstring)."""
|
|
524
|
+
study = gold_study(brief)
|
|
525
|
+
lo, hi = study.bounds()
|
|
526
|
+
try:
|
|
527
|
+
bodies, source, warn = read_delivery(folder, (np.asarray(lo), np.asarray(hi)))
|
|
528
|
+
except (ValueError, OSError, zipfile.BadZipFile, ET.ParseError) as exc:
|
|
529
|
+
return {"pass": False, "failures": [f"no multi-material part: {exc}"]}
|
|
530
|
+
row = analyse(brief, bodies, max_cells)
|
|
531
|
+
row["source"] = source
|
|
532
|
+
if warn:
|
|
533
|
+
row["warnings"] = warn
|
|
534
|
+
row["pass"] = not row["failures"]
|
|
535
|
+
return row
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# ---------------------------------------------------------------- the gold
|
|
539
|
+
|
|
540
|
+
def reference_bodies(brief):
|
|
541
|
+
"""brief["reference"] as [(material key, trimesh)]: each solid less the
|
|
542
|
+
earlier ones and the gold keep-outs."""
|
|
543
|
+
import manifold3d as m3d
|
|
544
|
+
|
|
545
|
+
from ...shapes import shape_from_spec, trimesh_from_manifold
|
|
546
|
+
from ...sharp import exact_manifold
|
|
547
|
+
from ...verify import _union
|
|
548
|
+
|
|
549
|
+
study = gold_study(brief)
|
|
550
|
+
keep = _union([r.shape for r in study.keep_out])
|
|
551
|
+
taken, out = None, []
|
|
552
|
+
for r in brief["reference"]:
|
|
553
|
+
m = exact_manifold(shape_from_spec(r["shape"], ASSETS))
|
|
554
|
+
if keep is not None:
|
|
555
|
+
m = m - keep
|
|
556
|
+
if taken is not None:
|
|
557
|
+
m = m - taken
|
|
558
|
+
taken = m if taken is None else m3d.Manifold.batch_boolean([taken, m], m3d.OpType.Add)
|
|
559
|
+
if m.volume() > 1e-6:
|
|
560
|
+
out.append((_material_key(r["material"]), trimesh_from_manifold(m)))
|
|
561
|
+
return out
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def write_delivery(bodies, folder, name="part", process=None, direction=None, slicer="all"):
|
|
565
|
+
"""Bodies as the agent would leave them: part.3mf (loadpath's writer)."""
|
|
566
|
+
from ...threemf import write_multimaterial_3mf
|
|
567
|
+
|
|
568
|
+
os.makedirs(folder, exist_ok=True)
|
|
569
|
+
spec = [{"name": f"{name} {k}", "mesh": m, "material": k} for k, m in bodies]
|
|
570
|
+
return write_multimaterial_3mf(os.path.join(folder, "part.3mf"), spec, direction=direction, name=name,
|
|
571
|
+
process=process, slicer=slicer)
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def gold(brief, max_cells=VERIFY_CELLS) -> dict:
|
|
575
|
+
"""The gold study lints and builds, and the reference design, written as
|
|
576
|
+
a 3MF and read back, passes the scorer."""
|
|
577
|
+
from ...tasks import check_study
|
|
578
|
+
|
|
579
|
+
res = check_study(brief["gold"], base_dir=ASSETS)
|
|
580
|
+
if not res["ok"]:
|
|
581
|
+
return res
|
|
582
|
+
if not brief.get("reference"):
|
|
583
|
+
return {"ok": False, "error": "multimat brief has no reference design"}
|
|
584
|
+
study = gold_study(brief)
|
|
585
|
+
with tempfile.TemporaryDirectory() as d:
|
|
586
|
+
write_delivery(reference_bodies(brief), d, process=study.process, direction=study.print_direction)
|
|
587
|
+
row = score(brief, d, max_cells)
|
|
588
|
+
keys = ("mass", "safetyFactor", "interfaceSafetyFactor", "zones", "verifySeconds")
|
|
589
|
+
out = {"ok": row["pass"], "cells": res.get("cells", ""), "warnings": res.get("warnings", []),
|
|
590
|
+
"score": {k: row.get(k) for k in keys}}
|
|
591
|
+
if not row["pass"]:
|
|
592
|
+
out["error"] = "reference fails its own scorer: " + "; ".join(row["failures"])
|
|
593
|
+
return out
|