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,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lattice briefs: a part that is a lattice for a reason the structural scorer
|
|
3
|
+
alone cannot see. The brief's pass block says which rules apply, and every
|
|
4
|
+
rule is judged on the delivered part.stl, not on what the agent says about it:
|
|
5
|
+
|
|
6
|
+
(gold study) part.stl re-verified under the gold study (briefs.score: one
|
|
7
|
+
body, envelope, keep-outs, interfaces, safety factor, mass)
|
|
8
|
+
cellular it really is a lattice: in a probe box inside the part, the
|
|
9
|
+
filled share is at most maxFill, lines through it cross at
|
|
10
|
+
least minWallsPer10mm walls per 10 mm (a lattice crosses a
|
|
11
|
+
wall every few mm, a solid web or a ribbed part hardly
|
|
12
|
+
ever), optionally a share of straight lines along X and Y
|
|
13
|
+
that see through (minSeeThrough: an open strut lattice), and
|
|
14
|
+
optionally the outside kept (minHull: convex hull volume over
|
|
15
|
+
the design space box)
|
|
16
|
+
drains no sealed cavity over maxEnclosed mm^3 (powder or resin
|
|
17
|
+
trapped inside)
|
|
18
|
+
print sliced along the build direction (printcheck.check_print with
|
|
19
|
+
the brief's bridge span): at most maxSupportArea mm^2 that
|
|
20
|
+
needs support (islands included)
|
|
21
|
+
impact the delivered pad, voxelized and homogenized (each voxel's
|
|
22
|
+
relative density is the part's local filled share, smoothed
|
|
23
|
+
over a few mm so the cells average out, solid face sheets
|
|
24
|
+
kept solid), stopped with the GOLD mass, speed and force
|
|
25
|
+
limit by loadpath's own impact model (impact.impact_report's
|
|
26
|
+
layer model, with the material's crush law and TPU's rate
|
|
27
|
+
stiffening, on a finely resampled curve; see _dense):
|
|
28
|
+
peak force under the limit, crush under maxCrush, inside the
|
|
29
|
+
pad's box, face sheets top and bottom. A field.json with an
|
|
30
|
+
impact block is re-run with the gold numbers too and
|
|
31
|
+
reported (not judged: its envelope and density are the
|
|
32
|
+
agent's claims about the part)
|
|
33
|
+
vibration natural frequencies of the delivered part (modal.py on the
|
|
34
|
+
part voxelized at `pitch`, each voxel at its filled share of
|
|
35
|
+
the modulus and density) clamped in the gold `fixed` box with
|
|
36
|
+
the gold point masses on the nodes in their boxes: first mode
|
|
37
|
+
at least FREQ_SLACK x minFrequency
|
|
38
|
+
|
|
39
|
+
Boxes in the pass block are {"min": [x, y, z], "max": [x, y, z]} in the
|
|
40
|
+
brief's coordinates.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import json
|
|
45
|
+
import os
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
|
|
49
|
+
DELIVER = """ part.stl the part to print (your final, verified outcome)
|
|
50
|
+
field.json the field graph that produced it, if a field graph did (with its impact or vibration block)
|
|
51
|
+
study.json the study, if a study produced or checked it
|
|
52
|
+
notes.md two or three lines: the outcome's mass and how it meets the brief's numbers, and your assumptions
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
FREQ_SLACK = 0.95 # the agent's analysis grid and ours differ
|
|
56
|
+
FORCE_SLACK = 1.05 # homogenized crush from a voxelized part: a few percent either way
|
|
57
|
+
CRUSH_SLACK = 1.1
|
|
58
|
+
BOX_TOL = 0.3 # mm a delivered pad may stick out of its box (mesh noise)
|
|
59
|
+
MAX_ENCLOSED = 50.0 # mm^3 sealed inside before it counts as trapped powder
|
|
60
|
+
IMPACT_PITCH = 0.25 # mm voxels for the crush model (TPU walls are ~0.5 mm)
|
|
61
|
+
IMPACT_SMOOTH = 2.5 # mm Gaussian over which the local density is averaged (cells up to ~14 mm)
|
|
62
|
+
|
|
63
|
+
KNOWN = {"safetyFactor", "maxDeflection", "maxMass", "maxSolidFraction", "cellular", "drains", "print",
|
|
64
|
+
"impact", "vibration"}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load(folder):
|
|
68
|
+
import trimesh
|
|
69
|
+
|
|
70
|
+
stl = os.path.join(folder, "part.stl")
|
|
71
|
+
if not os.path.exists(stl):
|
|
72
|
+
return stl, None
|
|
73
|
+
return stl, trimesh.load(stl, force="mesh")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _material(brief, spec):
|
|
77
|
+
from ...materials import material_from_spec
|
|
78
|
+
|
|
79
|
+
name = spec.get("material") or (brief.get("gold") or {}).get("material")
|
|
80
|
+
return material_from_spec(name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _box(b):
|
|
84
|
+
return np.asarray(b["min"], float), np.asarray(b["max"], float)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- cellular, drains, print --------------------------------------------------
|
|
88
|
+
|
|
89
|
+
def cellular(mesh, spec: dict, design_box=None) -> dict:
|
|
90
|
+
"""Filled share, walls crossed per 10 mm along X, Y and Z, and the share
|
|
91
|
+
of X and Y lines that see through, in the probe box (0.4 mm voxels)."""
|
|
92
|
+
from ...verify import voxelize
|
|
93
|
+
|
|
94
|
+
lo, hi = _box(spec["probe"])
|
|
95
|
+
pitch = float(spec.get("pitch", 0.4))
|
|
96
|
+
solid, _ = voxelize(mesh, pitch, box=(lo, hi))
|
|
97
|
+
walls, see = [], []
|
|
98
|
+
for ax in range(3):
|
|
99
|
+
a = np.moveaxis(solid, ax, -1)
|
|
100
|
+
starts = (a[..., 1:] & ~a[..., :-1]).sum(axis=-1) + a[..., 0]
|
|
101
|
+
walls.append(float(starts.mean() / (a.shape[-1] * pitch) * 10.0))
|
|
102
|
+
if ax < 2:
|
|
103
|
+
see.append(float((~a.any(axis=-1)).mean()))
|
|
104
|
+
out = {"fill": round(float(solid.mean()), 3), "wallsPer10mm": [round(w, 2) for w in walls],
|
|
105
|
+
"seeThrough": round(float(np.mean(see)), 3)}
|
|
106
|
+
if spec.get("minHull") is not None and design_box is not None:
|
|
107
|
+
dlo, dhi = design_box
|
|
108
|
+
out["hull"] = round(float(mesh.convex_hull.volume / np.prod(dhi - dlo)), 3)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _cellular_failures(c, spec):
|
|
113
|
+
f = []
|
|
114
|
+
if c["fill"] > spec.get("maxFill", 0.6):
|
|
115
|
+
f.append(f"{c['fill']:.0%} solid inside: not a lattice (at most {spec.get('maxFill', 0.6):.0%})")
|
|
116
|
+
need = spec.get("minWallsPer10mm", 0.6)
|
|
117
|
+
if min(c["wallsPer10mm"]) < need:
|
|
118
|
+
f.append(f"{min(c['wallsPer10mm']):.2f} walls per 10 mm through the inside: not a lattice (at least {need})")
|
|
119
|
+
if spec.get("minSeeThrough") is not None and c["seeThrough"] < spec["minSeeThrough"]:
|
|
120
|
+
f.append(f"{c['seeThrough']:.0%} of lines see through the inside: not an open lattice "
|
|
121
|
+
f"(at least {spec['minSeeThrough']:.0%})")
|
|
122
|
+
if spec.get("minHull") is not None and c.get("hull") is not None and c["hull"] < spec["minHull"]:
|
|
123
|
+
f.append(f"the outside fills {c['hull']:.0%} of the block: its shape was not kept")
|
|
124
|
+
return f
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def enclosed_volume(mesh) -> float:
|
|
128
|
+
"""mm^3 sealed inside the part (manifold's negative-volume pieces)."""
|
|
129
|
+
from ...shapes import manifold_from_trimesh
|
|
130
|
+
|
|
131
|
+
return float(-sum(v for v in (b.volume() for b in manifold_from_trimesh(mesh).decompose()) if v < 0))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _print(mesh, spec, material):
|
|
135
|
+
from ...printcheck import check_print
|
|
136
|
+
|
|
137
|
+
d = spec.get("direction", "+z")
|
|
138
|
+
direction = {"+x": (0, 1), "-x": (0, -1), "+y": (1, 1), "-y": (1, -1), "+z": (2, 1), "-z": (2, -1)}[d]
|
|
139
|
+
pr = check_print(mesh, direction, "fdm", pitch=spec.get("pitch"), bridge_span=float(spec["bridgeSpan"]),
|
|
140
|
+
material=material)
|
|
141
|
+
row = {k: pr[k] for k in ("pitch", "supportArea", "supportVolume", "islands", "islandArea", "bridges",
|
|
142
|
+
"maxBridgeSpan")}
|
|
143
|
+
f = []
|
|
144
|
+
# islands are in the support area too; a few mm^2 of strut tips cut by a
|
|
145
|
+
# hole or the envelope is voxel noise, not a print that fails
|
|
146
|
+
if pr["supportArea"] > spec.get("maxSupportArea", 25.0):
|
|
147
|
+
f.append(f"{pr['supportArea']:.0f} mm^2 needs support printed {d} ({pr['islands']} island(s); "
|
|
148
|
+
f"bridges up to {spec['bridgeSpan']:g} mm)")
|
|
149
|
+
return {k: (round(v, 2) if isinstance(v, float) else v) for k, v in row.items()}, f
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# --- impact ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def measure_core(mesh, pitch: float = IMPACT_PITCH, axis: int = 2, smooth: float = IMPACT_SMOOTH):
|
|
155
|
+
"""The delivered part as a homogenized core: (inside, rho, lo, fill per
|
|
156
|
+
layer, solid layer flags), crush axis last. `inside` is each layer's
|
|
157
|
+
footprint (convex hull of the material within 4 mm of it), rho the
|
|
158
|
+
filled share smoothed over `smooth` mm (normalized over the footprint,
|
|
159
|
+
face sheets left out of the average and kept solid)."""
|
|
160
|
+
from scipy import ndimage
|
|
161
|
+
from skimage.morphology import convex_hull_image
|
|
162
|
+
|
|
163
|
+
from ...verify import voxelize
|
|
164
|
+
|
|
165
|
+
solid, origin = voxelize(mesh, pitch)
|
|
166
|
+
occ = np.moveaxis(solid, axis, 2).astype(float)
|
|
167
|
+
nz = occ.shape[2]
|
|
168
|
+
win = max(1, int(round(4.0 / pitch)))
|
|
169
|
+
inside = np.zeros(occ.shape, bool)
|
|
170
|
+
for k in range(nz):
|
|
171
|
+
pr = occ[:, :, max(0, k - win):k + win + 1].any(axis=2)
|
|
172
|
+
if pr.sum() >= 3:
|
|
173
|
+
inside[:, :, k] = convex_hull_image(pr)
|
|
174
|
+
n = inside.sum(axis=(0, 1))
|
|
175
|
+
fill = np.where(n > 0, (occ * inside).sum(axis=(0, 1)) / np.maximum(n, 1), 0.0)
|
|
176
|
+
solid_layer = fill > 0.9
|
|
177
|
+
w = inside & ~solid_layer[None, None, :]
|
|
178
|
+
s = smooth / pitch
|
|
179
|
+
num = ndimage.gaussian_filter(occ * w, s, mode="constant")
|
|
180
|
+
den = ndimage.gaussian_filter(w.astype(float), s, mode="constant")
|
|
181
|
+
rho = np.where(w, num / np.maximum(den, 1e-9), 0.0)
|
|
182
|
+
rho[:, :, solid_layer] = 1.0
|
|
183
|
+
rho = np.where(inside, np.clip(rho, 0.0, 1.0), 0.0)
|
|
184
|
+
origin = np.asarray(origin, float)
|
|
185
|
+
lo = np.r_[np.delete(origin, axis), origin[axis]]
|
|
186
|
+
return inside, rho, lo, fill, solid_layer
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _impact(brief, mesh, spec, folder):
|
|
190
|
+
mat = _material(brief, spec)
|
|
191
|
+
axis = "xyz".index(spec.get("axis", "z"))
|
|
192
|
+
f = []
|
|
193
|
+
row = {}
|
|
194
|
+
lo, hi = _box(spec["box"])
|
|
195
|
+
blo, bhi = np.asarray(mesh.bounds, float)
|
|
196
|
+
out = float(max(np.max(lo - blo), np.max(bhi - hi)))
|
|
197
|
+
if out > BOX_TOL:
|
|
198
|
+
f.append(f"the pad sticks {out:.1f} mm out of its {np.round(hi - lo, 1).tolist()} mm box")
|
|
199
|
+
inside, rho, _, fill, solid_layer = measure_core(mesh, IMPACT_PITCH, axis)
|
|
200
|
+
core = ~solid_layer & (fill > 0)
|
|
201
|
+
row["coreDensity"] = [round(float(fill[core].min()), 3), round(float(fill[core].max()), 3)] if core.any() else None
|
|
202
|
+
if not core.any() or fill[core].max() > 0.7:
|
|
203
|
+
f.append("no lattice core: it is solid through its height")
|
|
204
|
+
if spec.get("faces"):
|
|
205
|
+
ends = [fill[np.flatnonzero(fill > 0)[0]], fill[np.flatnonzero(fill > 0)[-1]]]
|
|
206
|
+
row["faceFill"] = [round(float(e), 2) for e in ends]
|
|
207
|
+
if min(ends) < 0.85:
|
|
208
|
+
f.append("no solid face sheet on the " + ("bottom" if ends[0] < 0.85 else "top"))
|
|
209
|
+
gold = {"envelope": "envelope", "density": "density", "lattice": spec.get("lattice", "gyroid"), "axis": "z",
|
|
210
|
+
"mass": float(spec["mass"]), "velocity": float(spec["velocity"]),
|
|
211
|
+
"forceLimit": float(spec["forceLimit"]), "pitch": IMPACT_PITCH}
|
|
212
|
+
r, rate = stop_impact(inside, rho, IMPACT_PITCH, gold["lattice"], mat, gold["mass"], gold["velocity"])
|
|
213
|
+
row.update(energy=r["energy"], peakForce=r["peakForce"], crush=r["crush"], peakG=r.get("peakG"),
|
|
214
|
+
bottomedOut=r["bottomedOut"], rate=rate)
|
|
215
|
+
if r["bottomedOut"] or r["peakForce"] is None:
|
|
216
|
+
f.append(f"bottoms out: absorbs {r['capacity'] / 1000:.2f} J of {r['energy'] / 1000:.2f} J")
|
|
217
|
+
else:
|
|
218
|
+
if r["peakForce"] > FORCE_SLACK * spec["forceLimit"]:
|
|
219
|
+
f.append(f"peak force {r['peakForce']:.0f} N > {spec['forceLimit']:g} N")
|
|
220
|
+
if spec.get("maxCrush") and r["crush"] > CRUSH_SLACK * spec["maxCrush"]:
|
|
221
|
+
f.append(f"crushes {r['crush']:.1f} mm > {spec['maxCrush']:g} mm")
|
|
222
|
+
row["graph"] = _graph_impact(folder, gold, mat)
|
|
223
|
+
return row, f
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _dense(cv, step=0.01):
|
|
227
|
+
"""The crush curve resampled every `step` mm. impact_response inverts
|
|
228
|
+
the energy by linear interpolation between the curve's corners, which on
|
|
229
|
+
an elastic ramp (energy quadratic in crush) reads far too little force
|
|
230
|
+
when the stop ends on it: a solid TPU block came out at 80 N instead of
|
|
231
|
+
about 2 kN. Fine sampling makes the inversion exact enough."""
|
|
232
|
+
from ...impact import CrushCurve
|
|
233
|
+
|
|
234
|
+
d = np.unique(np.r_[cv.d, np.arange(0.0, float(cv.d[-1]), step)])
|
|
235
|
+
return CrushCurve(d, np.interp(d, cv.d, cv.f), cv.stroke, cv.layer_force, cv.layer_area, cv.layer_rho,
|
|
236
|
+
cv.layer_stroke)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def stop_impact(inside, rho, h, kind, material, mass, velocity):
|
|
240
|
+
"""impact.impact_report's layer model on a voxel core (crush along the
|
|
241
|
+
grid's last axis): the material's crush law, each layer's force the sum
|
|
242
|
+
of its cells' plateau, an elastomer stiffened for the crush time, and
|
|
243
|
+
the stop found on a finely resampled curve. Returns (response, rate
|
|
244
|
+
factor or None)."""
|
|
245
|
+
from ...impact import (
|
|
246
|
+
crush_curve,
|
|
247
|
+
crush_law,
|
|
248
|
+
impact_response,
|
|
249
|
+
layer_profile,
|
|
250
|
+
plateau_stress,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
law, sy = crush_law(kind, material)
|
|
254
|
+
E = float(material.E)
|
|
255
|
+
area, mean = layer_profile(inside, rho, h, 2)
|
|
256
|
+
lf = (plateau_stress(law, rho, sy) * inside).sum(axis=(0, 1)) * h ** 2
|
|
257
|
+
energy = 1000.0 * 0.5 * mass * velocity ** 2
|
|
258
|
+
resp = impact_response(_dense(crush_curve(law, area, mean, h, sy, E, layer_force=lf)), energy, mass)
|
|
259
|
+
fac = None
|
|
260
|
+
if law.endswith(":elastic"):
|
|
261
|
+
from ...viscoelastic import creep_params, event_factor
|
|
262
|
+
|
|
263
|
+
if creep_params(material) is not None:
|
|
264
|
+
for _ in range(4):
|
|
265
|
+
t = 2.0 * float(resp["crush"]) * 1e-3 / velocity
|
|
266
|
+
fac = float(event_factor(material, t))
|
|
267
|
+
cv = crush_curve(law, area, mean, h, sy * fac, E * fac, layer_force=lf * fac)
|
|
268
|
+
resp = impact_response(_dense(cv), energy, mass)
|
|
269
|
+
return resp, (round(fac, 3) if fac else None)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _graph_impact(folder, gold, mat):
|
|
273
|
+
"""The agent's own field graph's impact block, re-run with the gold
|
|
274
|
+
mass, speed, force limit and axis (reported, not judged)."""
|
|
275
|
+
path = os.path.join(folder, "field.json")
|
|
276
|
+
if not os.path.exists(path):
|
|
277
|
+
return None
|
|
278
|
+
try:
|
|
279
|
+
from ...fields import FieldGraph
|
|
280
|
+
from ...impact import impact_report
|
|
281
|
+
|
|
282
|
+
with open(path) as fh:
|
|
283
|
+
doc = json.load(fh)
|
|
284
|
+
spec = doc.get("impact")
|
|
285
|
+
if not spec:
|
|
286
|
+
return {"note": "field.json has no impact block"}
|
|
287
|
+
spec = {k: v for k, v in spec.items() if k not in ("energy", "grade")}
|
|
288
|
+
spec.update({k: gold[k] for k in ("mass", "velocity", "forceLimit", "axis")})
|
|
289
|
+
r = impact_report(FieldGraph(doc, base_dir=folder), spec, mat)
|
|
290
|
+
return {"peakForce": r["response"]["peakForce"], "crush": r["response"]["crush"], "ok": r.get("ok")}
|
|
291
|
+
except Exception as exc: # noqa: BLE001 - the agent's graph is information here
|
|
292
|
+
return {"error": str(exc).strip().splitlines()[-1][:200] if str(exc).strip() else type(exc).__name__}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --- vibration ---------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _vibration(brief, mesh, spec):
|
|
298
|
+
from ...fem import HexMesh
|
|
299
|
+
from ...modal import modal_analysis
|
|
300
|
+
from ...verify import voxelize
|
|
301
|
+
|
|
302
|
+
mat = _material(brief, spec)
|
|
303
|
+
h = float(spec.get("pitch", 1.0))
|
|
304
|
+
vol = float(abs(mesh.volume))
|
|
305
|
+
h = max(h, (vol / 30_000) ** (1.0 / 3.0)) # at most ~30k cells: a direct eigensolve
|
|
306
|
+
sub = int(spec.get("sub", 3))
|
|
307
|
+
lo = np.floor(np.asarray(mesh.bounds[0]) / h) * h - h
|
|
308
|
+
hi = np.ceil(np.asarray(mesh.bounds[1]) / h) * h + h
|
|
309
|
+
solid, origin = voxelize(mesh, h / sub, box=(lo, hi))
|
|
310
|
+
n = np.array(solid.shape) // sub
|
|
311
|
+
share = solid[:n[0] * sub, :n[1] * sub, :n[2] * sub].reshape(n[0], sub, n[1], sub, n[2], sub).mean(axis=(1, 3, 5))
|
|
312
|
+
keep = share >= 0.1
|
|
313
|
+
hm = HexMesh(np.argwhere(keep), h, origin)
|
|
314
|
+
s = share[keep]
|
|
315
|
+
xyz = hm.node_xyz
|
|
316
|
+
|
|
317
|
+
def nodes_in(b):
|
|
318
|
+
blo, bhi = _box(b)
|
|
319
|
+
return np.flatnonzero(np.all((xyz >= blo - 1e-6) & (xyz <= bhi + 1e-6), axis=1))
|
|
320
|
+
|
|
321
|
+
f = []
|
|
322
|
+
fn = nodes_in(spec["fixed"])
|
|
323
|
+
if len(fn) == 0:
|
|
324
|
+
return {}, ["nothing where the part is held"]
|
|
325
|
+
fixed = np.zeros(hm.n_dof, bool)
|
|
326
|
+
for a in range(3):
|
|
327
|
+
fixed[3 * fn + a] = True
|
|
328
|
+
pm = []
|
|
329
|
+
for p in spec.get("pointMasses", []):
|
|
330
|
+
nn = nodes_in(p)
|
|
331
|
+
if len(nn) == 0:
|
|
332
|
+
return {}, [f"nothing where the {p['mass']:g} kg mass sits"]
|
|
333
|
+
pm.append((nn, float(p["mass"])))
|
|
334
|
+
# parts not connected to the clamp float: drop them (their modes are 0 Hz)
|
|
335
|
+
from scipy import ndimage
|
|
336
|
+
|
|
337
|
+
lbl, _ = ndimage.label(keep)
|
|
338
|
+
held = np.unique(lbl[tuple(np.argwhere(keep)[np.isin(hm.enode, fn).any(axis=1)].T)])
|
|
339
|
+
if not np.isin(lbl[keep], held).all():
|
|
340
|
+
f.append("material not connected to the held face")
|
|
341
|
+
res = modal_analysis(hm, mat.E * s, mat.density * s, mat.nu, fixed, int(spec.get("modes", 3)), pm)
|
|
342
|
+
freqs = [round(float(x), 1) for x in res.frequencies]
|
|
343
|
+
row = {"frequencies": freqs, "cells": int(hm.n_elem), "pitch": round(h, 3)}
|
|
344
|
+
if freqs[0] < FREQ_SLACK * spec["minFrequency"]:
|
|
345
|
+
f.append(f"first mode {freqs[0]:.0f} Hz < {spec['minFrequency']:g} Hz")
|
|
346
|
+
return row, f
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# --- the kind's interface ----------------------------------------------------
|
|
350
|
+
|
|
351
|
+
def score(brief: dict, folder: str, max_cells: int | None = None) -> dict:
|
|
352
|
+
from .. import briefs
|
|
353
|
+
|
|
354
|
+
want = brief["pass"]
|
|
355
|
+
stl, mesh = _load(folder)
|
|
356
|
+
if mesh is None:
|
|
357
|
+
return {"pass": False, "failures": ["no part.stl"]}
|
|
358
|
+
if "gold" in brief:
|
|
359
|
+
row = briefs.score(brief, stl, max_cells or briefs.VERIFY_CELLS)
|
|
360
|
+
row.pop("pass", None)
|
|
361
|
+
else:
|
|
362
|
+
row = {"failures": []}
|
|
363
|
+
mat = _material(brief, want.get("impact") or want.get("vibration") or {})
|
|
364
|
+
row["mass"] = round(float(abs(mesh.volume)) * mat.density / 1000.0, 2)
|
|
365
|
+
row["bodies"] = len(mesh.split(only_watertight=False))
|
|
366
|
+
if row["bodies"] != 1:
|
|
367
|
+
row["failures"].append(f"{row['bodies']} bodies")
|
|
368
|
+
if want.get("maxMass") and row["mass"] > briefs.MASS_SLACK * want["maxMass"]:
|
|
369
|
+
row["failures"].append(f"{row['mass']:.1f} g > {want['maxMass']} g")
|
|
370
|
+
f = row["failures"]
|
|
371
|
+
mat = _material(brief, {}) if "gold" in brief else None
|
|
372
|
+
if want.get("cellular"):
|
|
373
|
+
row["cellular"] = cellular(mesh, want["cellular"], _design_box(brief) if "gold" in brief else None)
|
|
374
|
+
f += _cellular_failures(row["cellular"], want["cellular"])
|
|
375
|
+
if want.get("drains"):
|
|
376
|
+
row["enclosedVolume"] = round(enclosed_volume(mesh), 1)
|
|
377
|
+
lim = want["drains"].get("maxEnclosed", MAX_ENCLOSED)
|
|
378
|
+
if row["enclosedVolume"] > lim:
|
|
379
|
+
f.append(f"{row['enclosedVolume']:.0f} mm^3 sealed inside: the powder cannot get out")
|
|
380
|
+
if want.get("print"):
|
|
381
|
+
row["print"], ff = _print(mesh, want["print"], mat)
|
|
382
|
+
f += ff
|
|
383
|
+
if want.get("impact"):
|
|
384
|
+
row["impact"], ff = _impact(brief, mesh, want["impact"], folder)
|
|
385
|
+
f += ff
|
|
386
|
+
if want.get("vibration"):
|
|
387
|
+
row["vibration"], ff = _vibration(brief, mesh, want["vibration"])
|
|
388
|
+
f += ff
|
|
389
|
+
row["pass"] = not f
|
|
390
|
+
return row
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def gold(brief: dict) -> dict:
|
|
394
|
+
"""The gold study builds (if there is one), the pass block is well formed,
|
|
395
|
+
and an impact brief is feasible: loadpath's own graded core for the
|
|
396
|
+
pad's box (less 1 mm face sheets) meets the force and crush limits."""
|
|
397
|
+
from .. import briefs
|
|
398
|
+
|
|
399
|
+
want = brief.get("pass") or {}
|
|
400
|
+
unknown = set(want) - KNOWN
|
|
401
|
+
if unknown:
|
|
402
|
+
return {"ok": False, "error": f"unknown pass keys {sorted(unknown)}"}
|
|
403
|
+
res = {"ok": True}
|
|
404
|
+
if "gold" in brief:
|
|
405
|
+
from ...tasks import check_study
|
|
406
|
+
|
|
407
|
+
res = check_study(brief["gold"], base_dir=briefs.ASSETS)
|
|
408
|
+
if not res["ok"]:
|
|
409
|
+
return res
|
|
410
|
+
elif not (want.get("impact") or want.get("vibration")):
|
|
411
|
+
return {"ok": False, "error": "no gold study and no impact or vibration block to judge by"}
|
|
412
|
+
try:
|
|
413
|
+
if want.get("cellular"):
|
|
414
|
+
_box(want["cellular"]["probe"])
|
|
415
|
+
if want.get("print") and "bridgeSpan" not in want["print"]:
|
|
416
|
+
return {"ok": False, "error": "print needs bridgeSpan"}
|
|
417
|
+
if want.get("vibration"):
|
|
418
|
+
v = want["vibration"]
|
|
419
|
+
_box(v["fixed"])
|
|
420
|
+
for p in v.get("pointMasses", []):
|
|
421
|
+
_box(p)
|
|
422
|
+
float(p["mass"])
|
|
423
|
+
float(v["minFrequency"])
|
|
424
|
+
if want.get("impact"):
|
|
425
|
+
res = {**res, **_impact_feasible(brief, want["impact"])}
|
|
426
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
427
|
+
return {"ok": False, "error": f"pass block: {exc}"}
|
|
428
|
+
return res
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _impact_feasible(brief, spec):
|
|
432
|
+
from ...fields import FieldGraph
|
|
433
|
+
from ...impact import impact_report
|
|
434
|
+
|
|
435
|
+
lo, hi = _box(spec["box"])
|
|
436
|
+
core_lo, core_hi = lo.copy(), hi.copy()
|
|
437
|
+
core_lo[2] += 1.0
|
|
438
|
+
core_hi[2] -= 1.0
|
|
439
|
+
doc = {"nodes": {"envelope": {"op": "shape", "shape": {"type": "box", "min": core_lo.tolist(),
|
|
440
|
+
"max": core_hi.tolist()}}}}
|
|
441
|
+
g = FieldGraph(doc)
|
|
442
|
+
s = {"envelope": "envelope", "density": 0.3, "lattice": spec.get("lattice", "gyroid"), "axis": "z",
|
|
443
|
+
"mass": spec["mass"], "velocity": spec["velocity"], "forceLimit": spec["forceLimit"], "grade": True,
|
|
444
|
+
"cell": spec.get("cell", 8.0), "minWall": spec.get("minWall", 0.5), "pitch": 0.5}
|
|
445
|
+
r = impact_report(g, s, _material(brief, spec))["graded"]
|
|
446
|
+
resp = r["response"]
|
|
447
|
+
ok = bool(r["ok"]) and resp["peakForce"] is not None and resp["crush"] <= spec.get("maxCrush", np.inf)
|
|
448
|
+
out = {"ok": ok, "gradedPeakForce": resp["peakForce"], "gradedCrush": resp["crush"],
|
|
449
|
+
"gradedCoreMass": r["coreMass"]}
|
|
450
|
+
if not ok:
|
|
451
|
+
out["error"] = f"no graded core meets it: {resp}"
|
|
452
|
+
return out
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# --- reference deliveries (tests, and a sanity check of a brief's numbers) ---
|
|
456
|
+
|
|
457
|
+
def _mbox(lo, hi):
|
|
458
|
+
import manifold3d as m3d
|
|
459
|
+
|
|
460
|
+
lo, hi = np.asarray(lo, float), np.asarray(hi, float)
|
|
461
|
+
return m3d.Manifold.cube(tuple(hi - lo)).translate(tuple(lo))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _mcyl(p0, p1, r, sections=24):
|
|
465
|
+
import trimesh
|
|
466
|
+
|
|
467
|
+
from ...shapes import manifold_from_trimesh
|
|
468
|
+
|
|
469
|
+
return manifold_from_trimesh(trimesh.creation.cylinder(radius=r, segment=[np.asarray(p0, float),
|
|
470
|
+
np.asarray(p1, float)],
|
|
471
|
+
sections=sections))
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _union(parts):
|
|
475
|
+
import manifold3d as m3d
|
|
476
|
+
|
|
477
|
+
return m3d.Manifold.batch_boolean(list(parts), m3d.OpType.Add)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _prism_x(pts, x0, x1):
|
|
481
|
+
"""A prism along X over the (y, z) polygon `pts` (counter-clockwise)."""
|
|
482
|
+
import trimesh
|
|
483
|
+
|
|
484
|
+
from ...shapes import manifold_from_trimesh
|
|
485
|
+
|
|
486
|
+
pts = np.asarray(pts, float)
|
|
487
|
+
n = len(pts)
|
|
488
|
+
V = np.vstack([np.c_[np.full(n, x0), pts], np.c_[np.full(n, x1), pts]])
|
|
489
|
+
F = [[0, i + 1, i] for i in range(1, n - 1)] + [[n, n + i, n + i + 1] for i in range(1, n - 1)]
|
|
490
|
+
for i in range(n):
|
|
491
|
+
j = (i + 1) % n
|
|
492
|
+
F += [[i, j, n + j], [i, n + j, n + i]]
|
|
493
|
+
t = trimesh.Trimesh(V, np.array(F), process=True)
|
|
494
|
+
if t.volume < 0:
|
|
495
|
+
t.invert()
|
|
496
|
+
return manifold_from_trimesh(t)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _design_box(brief):
|
|
500
|
+
ds = brief["gold"]["designSpace"]
|
|
501
|
+
return np.min([s["min"] for s in ds], axis=0), np.max([s["max"] for s in ds], axis=0)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def bcc_struts(lo, hi, cell, cz, d, phase, sections=8):
|
|
505
|
+
"""A BCC strut lattice (cell x cell x cz, strut diameter d) clipped to the
|
|
506
|
+
box, its body-centre nodes on `phase` + (cell, cell, cz) / 2."""
|
|
507
|
+
import itertools
|
|
508
|
+
|
|
509
|
+
lo, hi, o = (np.asarray(v, float) for v in (lo, hi, phase))
|
|
510
|
+
c = np.array([cell, cell, cz])
|
|
511
|
+
first = np.floor((lo - o) / c).astype(int) - 1
|
|
512
|
+
last = np.ceil((hi - o) / c).astype(int) + 1
|
|
513
|
+
struts = []
|
|
514
|
+
for idx in itertools.product(*(range(first[i], last[i]) for i in range(3))):
|
|
515
|
+
mid = o + (np.array(idx) + 0.5) * c
|
|
516
|
+
for s in itertools.product((-1, 1), repeat=3):
|
|
517
|
+
struts.append(_mcyl(mid, mid + 0.5 * np.array(s) * c, d / 2, sections))
|
|
518
|
+
return _union(struts) ^ _mbox(lo, hi)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def reference(brief: dict, folder: str, quick: bool = True) -> str:
|
|
522
|
+
"""Write a reference delivery (part.stl) for one of this kind's briefs
|
|
523
|
+
into `folder`: a defensible answer, not the lightest. `quick` uses
|
|
524
|
+
coarser lattice and mesh settings (tests)."""
|
|
525
|
+
from ...shapes import trimesh_from_manifold
|
|
526
|
+
|
|
527
|
+
os.makedirs(folder, exist_ok=True)
|
|
528
|
+
bid = brief["id"]
|
|
529
|
+
if bid == "shoulder-riser-mjf":
|
|
530
|
+
from ...lattice import LatticeOptions, envelope_mesh, lattice_infill
|
|
531
|
+
from .. import briefs
|
|
532
|
+
|
|
533
|
+
study = briefs.gold_study(brief)
|
|
534
|
+
o = LatticeOptions.from_spec({"type": "gyroid", "cell": 12, "skin": 0, "minWall": 0.8, "maxWall": 3,
|
|
535
|
+
"collar": 1.0, "safetyFactor": 3.5}, study.process, 3.5)
|
|
536
|
+
o.analysis_cells, o.iterations = (12_000, 4) if quick else (30_000, 6)
|
|
537
|
+
if quick:
|
|
538
|
+
o.step = 0.45
|
|
539
|
+
mesh, _ = lattice_infill(study, envelope_mesh(study), None, o)
|
|
540
|
+
elif bid == "elbow-stop-pad-tpu":
|
|
541
|
+
from ...fields import FieldGraph
|
|
542
|
+
|
|
543
|
+
sh = lambda lo, hi: {"op": "shape", "shape": {"type": "box", "min": lo, "max": hi}}
|
|
544
|
+
doc = {"nodes": {"core": {"op": "lattice", "type": "gyroid", "cell": 8, "density": 0.19},
|
|
545
|
+
"env": sh([-15, -15, 1], [15, 15, 14]),
|
|
546
|
+
"clip": {"op": "intersect", "inputs": ["core", "env"]},
|
|
547
|
+
"part": {"op": "union", "inputs": ["clip", sh([-15, -15, 0], [15, 15, 1.2]),
|
|
548
|
+
sh([-15, -15, 13.8], [15, 15, 15])]}},
|
|
549
|
+
"output": "part"}
|
|
550
|
+
mesh = FieldGraph(doc).mesh(pitch=0.25)
|
|
551
|
+
elif bid == "tray-arm-lattice-fdm":
|
|
552
|
+
lat = bcc_struts([4, -15, 0], [74, 15, 40], 8.0, 12.0, 2.0, [4, -4, 0], 8 if quick else 12)
|
|
553
|
+
keel = _prism_x([(0, 30), (10, 40), (-10, 40)], 4, 74) # a 45-degree V top chord
|
|
554
|
+
body = _union([lat, keel, _mbox([0, -15, 0], [4, 15, 40]), _mbox([74, -15, 0], [80, 15, 40]),
|
|
555
|
+
_mbox([0, -15, 0], [80, 15, 2.5])])
|
|
556
|
+
holes = ([_mcyl([-1, 0, z], [5, 0, z], 2.2) for z in (10, 30)]
|
|
557
|
+
+ [_mcyl([4, 0, z], [12, 0, z], 4.0) for z in (10, 30)]
|
|
558
|
+
+ [_mcyl([73.5, y, 30], [81, y, 30], 2.0) for y in (-8, 8)])
|
|
559
|
+
mesh = trimesh_from_manifold(body - _union(holes))
|
|
560
|
+
elif bid == "camera-boom-mjf":
|
|
561
|
+
w = 1.2 # an open-ended square tube
|
|
562
|
+
tube = _mbox([3, -11, -11], [95, 11, 11]) - _mbox([3 + w, -11 + w, -11 + w], [100, 11 - w, 11 - w])
|
|
563
|
+
body = _union([_mbox([0, -18, -18], [3, 18, 18]), tube, _mbox([70, -11, 7], [95, 11, 11])])
|
|
564
|
+
holes = ([_mcyl([-1, y, z], [5, y, z], 1.7) for y in (-14, 14) for z in (-14, 14)]
|
|
565
|
+
+ [_mcyl([x, 0, 5], [x, 0, 12], 1.1) for x in (72, 93)])
|
|
566
|
+
mesh = trimesh_from_manifold(body - _union(holes))
|
|
567
|
+
else:
|
|
568
|
+
raise ValueError(f"no reference delivery for {bid!r}")
|
|
569
|
+
path = os.path.join(folder, "part.stl")
|
|
570
|
+
mesh.export(path)
|
|
571
|
+
return path
|