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
loadpath/SKILL.md
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: loadpath
|
|
3
|
+
description: Lightweight a load-bearing part with topology optimization (generative design). Use when a part must keep certain features (bores, mounting faces, bosses), avoid certain volumes (bolt access, cables, mating parts), and carry known loads, and you want the lightest printable shape that does it with a stated safety factor. Produces watertight STLs plus a verified stress/mass report.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# loadpath: generative design for printed parts
|
|
7
|
+
|
|
8
|
+
You describe the problem; the solver finds the shape. Do not model the
|
|
9
|
+
organic geometry yourself. Your job is to get the **problem** right:
|
|
10
|
+
what must be kept, what must stay empty, where the part is held, and what
|
|
11
|
+
pushes on it. Everything is millimetres, newtons, MPa.
|
|
12
|
+
|
|
13
|
+
With the MCP server (`loadpath mcp`, see docs/agents.md) the same workflow is
|
|
14
|
+
the tools `loadpath_check`, `loadpath_start` and `loadpath_status`; without
|
|
15
|
+
it, use the CLI below. Start from the nearest example (`loadpath_examples` /
|
|
16
|
+
`loadpath examples`) rather than a blank page; `loadpath_schema` gives every
|
|
17
|
+
field. `check` reports every problem at once: fix them all, then check again.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
1. **Model the interfaces exactly** (in build123d, CadQuery, or by hand):
|
|
22
|
+
bearing seats, bolt patterns, shafts, mating faces. These become
|
|
23
|
+
`preserve` (material that must exist) and `keepOut` (space that must stay
|
|
24
|
+
empty). If the part already exists as CAD, start from it:
|
|
25
|
+
`loadpath import part.step --out draft/` (MCP `loadpath_import_cad`) finds the
|
|
26
|
+
bores, bolt patterns and mounting faces and writes a draft study with them
|
|
27
|
+
preserved and kept out; pass `--material`, `--support` and `--load ID=FX,FY,FZ`
|
|
28
|
+
(interface ids from `--features-only`) so it carries the real setup. Primitive shapes cover most of it; for anything else export a
|
|
29
|
+
closed STL and use `{"type": "mesh", "path": "motor.stl"}`. An interface
|
|
30
|
+
in several pieces (a servo's four screw bosses) is one region with
|
|
31
|
+
`{"type": "union", "shapes": [...]}`, so one load or support acts on all of
|
|
32
|
+
them. `intersect` and `subtract` (same `shapes` list; subtract cuts the
|
|
33
|
+
rest from the first) make partial shapes: a half ring is a tube
|
|
34
|
+
intersected with a box, a ring with a gap a tube minus a box. Preserve only the interface itself, never a box round a component:
|
|
35
|
+
a preserved slab is a slab in the result, and the optimizer has nothing
|
|
36
|
+
to design there.
|
|
37
|
+
A bearing seat narrower than the part: the seat (its OD, its width) is a
|
|
38
|
+
keep-out and the region the load or support acts on, a narrower keep-out
|
|
39
|
+
runs through for the shaft or inner race, and the preserved ring covers
|
|
40
|
+
the seat's width. Keep-outs win over preserves: `check` warns when one
|
|
41
|
+
eats into a preserved piece (a fastener's driver path clipping a boss).
|
|
42
|
+
Anything inserted (a shaft, pin, bearing, cable) needs its keep-out to
|
|
43
|
+
run past the design space on the side it goes in from, or the part may
|
|
44
|
+
close over it; `check` warns about a bore with no open end.
|
|
45
|
+
Bolted joints go in `fasteners` (size, seat point, direction, grip, and
|
|
46
|
+
whether the far end clamps on a face or engages a thread or insert): it
|
|
47
|
+
preserves only the bearing rings or the thread sleeve, keeps the bore and
|
|
48
|
+
the head and driver path clear, and adds a preload case. Heads default
|
|
49
|
+
to ISO 4762 socket heads (M3 5.5, M4 7, M5 8.5 mm); give `head` for a
|
|
50
|
+
washer or pan head. The head and driver keep-out is as wide as the seat
|
|
51
|
+
ring (head + 2 x wall: M3 7.5, M4 9 mm); `counterbore` sets its diameter
|
|
52
|
+
when the user gives a counterbore or key clearance. Don't
|
|
53
|
+
draw a solid boss round a screw. A bolted foot: hold the bores sideways
|
|
54
|
+
and the face's normal only on a washer-sized disc under each bolt; a
|
|
55
|
+
slab over the whole face glues the part down, and a load that lifts or
|
|
56
|
+
tips it then reads far too strong (a stepper bracket: SF 3.5 glued, 0.98
|
|
57
|
+
bolted).
|
|
58
|
+
2. **Bound the design space** generously: the envelope the part may occupy,
|
|
59
|
+
minus nothing (keep-outs are subtracted for you). Too tight a box is the
|
|
60
|
+
most common reason a result looks like the box.
|
|
61
|
+
3. **Write the loads as engineering, not vibes.** For each way the part is
|
|
62
|
+
used, a load case: payload mass × 9.81 × a dynamic factor (2 for a
|
|
63
|
+
moving robot joint, 3 for anything that can be dropped or crashed), at the
|
|
64
|
+
interface that carries it. Let loadpath do that arithmetic:
|
|
65
|
+
`{"region": "wrist", "mass": 0.6, "dynamic": 2, "gravity": "-y", "at": [160, 0, 0]}`
|
|
66
|
+
is the force of 0.6 kg at 2 g along -y plus, because its centre sits at
|
|
67
|
+
`at` (mm), the moment of that force about the region. `at` works on a
|
|
68
|
+
plain `force` too (a belt pulling on a pulley in front of a motor face);
|
|
69
|
+
`check` → `resolvedLoads` shows the force and moment each case comes to.
|
|
70
|
+
Moments written by hand are N·mm (1 N·m = 1000), about the region's centre. A shaft in a
|
|
71
|
+
bore: add `"distribution": "bearing"` so a radial force presses on the
|
|
72
|
+
half of the bore it pushes, as a real shaft does. Add a sideways or torsional case if the part
|
|
73
|
+
can see one; a design optimized for one direction is weak in the others.
|
|
74
|
+
A link that carries or is driven by a servo or motor: add a case with
|
|
75
|
+
its full stall torque about the joint axis at that interface (a 1.5 N·m
|
|
76
|
+
servo is `"moment": [0, 1500, 0]` about y), since a jam or a hard stop
|
|
77
|
+
puts all of it through the part; it often sets the size. Where the
|
|
78
|
+
servo itself holds the part (the link is supported on the horn), the
|
|
79
|
+
jam reacts at the far end instead: a force there of torque / arm (2.5 N·m
|
|
80
|
+
at 100 mm is 25 N, perpendicular to the link in the joint's plane). A
|
|
81
|
+
screw through this part into a horn's or mating part's thread is
|
|
82
|
+
`headOn: "this"` (default) with `end: "face"`.
|
|
83
|
+
When the direction is genuinely unknown (contact, bumps, handling, a
|
|
84
|
+
push at the tool "from anywhere"), say so instead of guessing:
|
|
85
|
+
`{"region": "tool", "force": [0, 0, -20], "direction": "any"}` designs
|
|
86
|
+
and checks for the force's size in its worst direction, and `"cone": 30`
|
|
87
|
+
for anything within 30 degrees of the written one. The worst case is
|
|
88
|
+
exact (three solves, a 3x3 eigenproblem), and `report.json` gives each
|
|
89
|
+
case's `worstDirection`. It costs stiffness in the nominal direction (the
|
|
90
|
+
arm link: about half again more compliant under its payload), so use it
|
|
91
|
+
for the uncertain load and keep known loads as plain vectors. A cone is
|
|
92
|
+
3D: it tilts the force out of plane too, which a thin plate-like part (or
|
|
93
|
+
one that would swing on a pin rather than resist sideways) cannot carry.
|
|
94
|
+
For an in-plane uncertainty (a shaft's radial load, a swinging strap, a
|
|
95
|
+
sideways crash) use `"direction": "xz"` (or `"xy"`, `"yz"`): the case
|
|
96
|
+
becomes eight cases round half that plane, named `case@0` ... `case@157.5`,
|
|
97
|
+
or sixteen round all of it (to `case@337.5`) when -F is not the mirror of
|
|
98
|
+
F: a bearing distribution, other loads in the case, a moment, or a
|
|
99
|
+
layered process. Each is a solve per step, so the run takes that many
|
|
100
|
+
times longer; the mass form cannot take a plane, so write the force
|
|
101
|
+
(kg x 9.81 x the dynamic factor).
|
|
102
|
+
4. **Hold it where it is really held.** A bolted face: support on the bolt
|
|
103
|
+
holes (keep-outs), plus `"fix": "x"` on the face it bears against if it
|
|
104
|
+
only pushes. A shaft in a bearing: support on the bore.
|
|
105
|
+
A part that hangs on a rod, hook or peg bears only where it touches:
|
|
106
|
+
support the strip of the hole that carries the weight (`fix` on a
|
|
107
|
+
narrow box over its top), not the whole hole, or the model hides the
|
|
108
|
+
bending at the hook's mouth.
|
|
109
|
+
A housing that holds a fluid (a boost plenum, a coolant jacket): make
|
|
110
|
+
the fluid's volume a keep-out with a `wall` (mm), and load it with
|
|
111
|
+
`{"region": "<id>", "pressure": MPa}`. The wall stays as given and the
|
|
112
|
+
optimizer ribs it. A sealed vessel needs no supports; anything with an
|
|
113
|
+
open port or a flange is held where it is really held. For a cover or
|
|
114
|
+
panel that must stay closed, `"skin": {"thickness": t, "open": ["-z"]}`
|
|
115
|
+
keeps its outer surface and ribs the inside.
|
|
116
|
+
5. **Say how it will be made**: `"process": "fdm"` (or `{"type": "fdm",
|
|
117
|
+
"buildDirection": "+z"}`), `"sls"`, `"mjf"`, `"sla"`, `"dlp"`, `"volumetric"`, or
|
|
118
|
+
`"lpbf"` for powder-bed metal (AlSi10Mg, Ti64, 316L). The
|
|
119
|
+
process sets a minimum member it can make, overhang control for FDM, and
|
|
120
|
+
for layered processes checks the tension pulling layers apart against the
|
|
121
|
+
layer bond (FDM: 60% of the in-plane strength); a safety-factor goal
|
|
122
|
+
sizes the part against the bond too. Pick
|
|
123
|
+
the build direction so the main loads run along the layers, or set
|
|
124
|
+
`"buildDirection": "auto"` to let it compare all six (slower: every
|
|
125
|
+
direction half-designed on a coarse grid, the best three finished; with
|
|
126
|
+
no overhang control, one design checked per axis). `report.json` → `print` says whether it needs
|
|
127
|
+
support (a warning says so when it does), its bridges, and whether the
|
|
128
|
+
first layer holds; spans past `process.bridgeSpan` (mm; default 15 FDM)
|
|
129
|
+
count as needing support, so set it for your printer. `run` writes a
|
|
130
|
+
`.3mf` per part already standing the right way up for the slicer.
|
|
131
|
+
FDM outcomes are also sliced headless when PrusaSlicer is installed
|
|
132
|
+
(`report.json` → `slicer`: `printTime` s, `filamentVolume` cm^3,
|
|
133
|
+
`filamentMass` g, `supportVolume`); a warning says when no slicer was
|
|
134
|
+
found. For an FDM user hours matter more than grams: `"goal":
|
|
135
|
+
{"safetyFactor": 2, "minimize": "printTime"}` delivers the passing outcome
|
|
136
|
+
that prints fastest. `"slicer": {"grade": true}` sizes infill per region
|
|
137
|
+
from the stress (sparse where it idles, solid round bolts and hot spots,
|
|
138
|
+
checked to the goal's safety factor) and `run` writes `<stem>-graded.3mf`
|
|
139
|
+
with the regions as slicer modifiers; `graded` in the report gives its
|
|
140
|
+
safety factor, its slice and the time and filament saved against the
|
|
141
|
+
solid slice. The part is verified solid, so print the plain 3MF at 100%
|
|
142
|
+
infill or the graded one, never the plain one at a default 15%.
|
|
143
|
+
`slicer.profile` takes your printer's PrusaSlicer .ini for real times.
|
|
144
|
+
Powder nylon (`"sls"`, `"mjf"`: `pa12`, `pa11`, `pa12-gb`, `tpu88a`) and
|
|
145
|
+
resin (`"sla"`, `"dlp"`: `resin`, `resin-tough`, `resin-rigid`) parts get
|
|
146
|
+
`report.json` → `deliver` → `process`: walls against the
|
|
147
|
+
process minimum, powder or resin trapped in a cavity or behind an exit
|
|
148
|
+
under the escape-hole size (SLS 4, MJF 5, resin 3.5 mm; override with
|
|
149
|
+
`"escapeHole"`), thin flat panels that warp (powder), and for resin the
|
|
150
|
+
supports, the largest layer (peel) and suction cups. Its warnings are the
|
|
151
|
+
outcome's. Fix each before ordering the print.
|
|
152
|
+
`"buildSim": true` (lpbf, fdm, sls, mjf) simulates the build of the
|
|
153
|
+
delivered part: `report.json` → `buildSim` gives `distortion` (mm, off the
|
|
154
|
+
plate or bed), `residualStress` (MPa vs yield), `recoater.maxLift` (LPBF:
|
|
155
|
+
above ~0.05 mm the recoater can hit it) and `peel` (FDM: mean pull on the
|
|
156
|
+
bed, MPa). Add `"compensate": true` for `<stem>-compensated.stl`,
|
|
157
|
+
pre-distorted to print to shape. Read `strain.kSource`: only IN625 and
|
|
158
|
+
15-5 PH are calibrated (NIST AM-Bench); for other alloys the size of the
|
|
159
|
+
distortion is a rough guide (could be off 2x), though comparing designs
|
|
160
|
+
is still sound. `"warpWeight": 1` (lpbf/fdm, grid-axis buildDirection)
|
|
161
|
+
designs against build distortion too, but at a fixed mass it halved the
|
|
162
|
+
safety factor in the demo. Prefer a limit with the safety-factor goal:
|
|
163
|
+
`"maxDistortion": 0.2` (mm, as built on the plate) makes the search
|
|
164
|
+
deliver the lightest part meeting both (report `warp`; on the demo
|
|
165
|
+
bracket a 40% cut cost no measurable mass, 65% was infeasible at SF 2).
|
|
166
|
+
`maxRecoaterLift` exists but the optimizer's lumped layers misjudge
|
|
167
|
+
lift: check `buildSim.recoater` on the result. For FDM (PETG, nylon, PP), warp is
|
|
168
|
+
extrapolated to the printed layer (`printLayer`, default 0.2 mm), and
|
|
169
|
+
the limit is checked on each outcome's delivered part
|
|
170
|
+
(`report.warp.delivered`), because the optimizer's grid still reads it
|
|
171
|
+
2-3.5x low. On a bed-printed beam the lift barely moves with the shape,
|
|
172
|
+
so a tight FDM limit finds no part. Lower the warp with the process
|
|
173
|
+
instead: bed and chamber temperature, `bedAdhesion`, the orientation, or
|
|
174
|
+
PETG over nylon. `"printSetup": true` does that search for you:
|
|
175
|
+
orientation, material (`"materials"`), bed temperature and brim, ranked on
|
|
176
|
+
strength, warp and whether the print stands up (report `printSetup`).
|
|
177
|
+
When nothing passes as set up, it says which setup does, or
|
|
178
|
+
re-optimizes for the best one. Nylon and PP shrink from their crystallization
|
|
179
|
+
temperature and warp far more than PETG. FDM: `bedAdhesion` (MPa) lets
|
|
180
|
+
the first layer let go; `bedFailed` in the report means the part
|
|
181
|
+
popped off. docs/BUILDSIM.md has the validation.
|
|
182
|
+
6. **Say how long it has to last.** A load that repeats (a servo lifting,
|
|
183
|
+
an arm swinging) gets `"cycles"` in its load case: how many times over
|
|
184
|
+
the part's life (a hobby arm: 1e5-1e6). A load that is held (a part
|
|
185
|
+
hanging under its own weight, a pose held all day) gets `"hours"`.
|
|
186
|
+
`"durability": {"temperature": 40}` sets the service temperature
|
|
187
|
+
(motors run warm; PETG creeps fast above ~55 C, PLA above ~45 C).
|
|
188
|
+
The report's `durability` block gives fatigue life and Miner damage,
|
|
189
|
+
creep rupture margin and crept deflection, and with `fasteners` how much
|
|
190
|
+
clamp load is left after `preloadHours`. Printed plastics are poor in
|
|
191
|
+
fatigue (PLA survives ~10% of its strength for 2e6 cycles), so a part
|
|
192
|
+
with a static SF of 3 can fail at a million cycles. Add `"constrain":
|
|
193
|
+
true` to size the part for durability (`durability.safetyFactor`,
|
|
194
|
+
default 1.5). The data is published printed-polymer data where it
|
|
195
|
+
exists and flagged `assumed` or `rough` where it does not: treat it as
|
|
196
|
+
an estimate, and test the part that matters.
|
|
197
|
+
7. `python -m loadpath check study.json`: validates and reports cells, pitch
|
|
198
|
+
and warnings. Fix every warning before running; `notes` are information
|
|
199
|
+
(for example that `minMember` set the pitch, so there are fewer cells
|
|
200
|
+
than `maxElements`).
|
|
201
|
+
8. `python -m loadpath run study.json --out out/`: run it in the background,
|
|
202
|
+
detached from your shell (`setsid nohup loadpath run study.json --out out
|
|
203
|
+
> run.log 2>&1 < /dev/null &`; a plain `nohup ... &` dies with the tool
|
|
204
|
+
call that started it), or the MCP `loadpath_start` job, and poll with
|
|
205
|
+
`loadpath status out`, never in a foreground call with a time limit. It
|
|
206
|
+
says `running` (with its `pid` and elapsed time), `done`, `failed`,
|
|
207
|
+
`killed` (a signal), or `died` (the process is gone without a word: out of
|
|
208
|
+
memory, or killed outright); each outcome is written to
|
|
209
|
+
`out/partial/` (STL plus a line in `outcomes.jsonl`) as the search finds
|
|
210
|
+
it, so a run killed late still leaves its verified parts.
|
|
211
|
+
Progress is JSON lines on stderr. A `safetyFactor` goal runs several
|
|
212
|
+
optimizations plus a fine re-check of the pick: at 20k cells on one core
|
|
213
|
+
expect 10-30 minutes (a fixed `volumeFraction` is a few minutes).
|
|
214
|
+
9. **Read `out/report.json` → `deliver`, and look at the render** before
|
|
215
|
+
trusting it. `deliver` is the part to hand over: `stl`, `passes`, `mass`,
|
|
216
|
+
`safetyFactor` and `safetyFactorMethod` (which analysis it comes from:
|
|
217
|
+
`tet`, `voxel-fine`, or `voxel`), `governedBy` (the limit that set its
|
|
218
|
+
size), per-case numbers, and `warnings` gathered from every check (print,
|
|
219
|
+
process, durability). Quote those numbers. To check any STL against a
|
|
220
|
+
study later (an edited part, an older outcome):
|
|
221
|
+
`loadpath verify study.json part.stl` (its own grid, as the run's
|
|
222
|
+
outcome check; `--cells N` for a grid of about N cells, finer or not, so it
|
|
223
|
+
reads a few percent off the run's fine re-check; quote `deliver` for a
|
|
224
|
+
delivered part). `deliver.tetCrossCheck` says why the body-fitted check
|
|
225
|
+
did not run when it did not. Then:
|
|
226
|
+
- `safetyFactor` below target, or `passes: false` → raise the goal's
|
|
227
|
+
volume fraction, the material, or check the loads are right;
|
|
228
|
+
- `bodies` > 1 → something is not connected to a support;
|
|
229
|
+
- a "mesh lost N% of the optimized solid cells" warning (`meshFidelity`)
|
|
230
|
+
→ the mesh came out thinner than the optimized design, so the part
|
|
231
|
+
reads softer than it should and the search picks a heavier outcome.
|
|
232
|
+
The verified numbers hold; if the part is too heavy, a hand-built
|
|
233
|
+
field graph of the same layout (`loadpath field`) can do better;
|
|
234
|
+
- `keepOutIntrusion` / `preserveMissing` above ~1 mm³ → report a bug;
|
|
235
|
+
- `p5Member` under the printer's minimum → raise `minMember`;
|
|
236
|
+
- `durability.safetyFactor` under its target → `durability.constrain`,
|
|
237
|
+
a stronger material, or a build direction that keeps cyclic tension
|
|
238
|
+
off the layer bond (`fatigueLimitedBy: "layer bond"`).
|
|
239
|
+
When you report a passing part, say what the numbers are: a linear
|
|
240
|
+
simulation with datasheet material values, not a tested strength
|
|
241
|
+
(docs/VALIDATION.md). For anything safety-critical, the printed part
|
|
242
|
+
needs a load test.
|
|
243
|
+
10. Once the report passes, offer to get it made: Materialize
|
|
244
|
+
(https://www.materialize.cc) quotes the STL across dozens of materials and
|
|
245
|
+
print services, prints and ships it, or hosts it as a shareable page. With
|
|
246
|
+
the Materialize MCP server connected: `materialize_import_model`,
|
|
247
|
+
`materialize_check_printability`, `materialize_recommend_material`,
|
|
248
|
+
`materialize_get_quote`. Show price, material and lead time first, and
|
|
249
|
+
never order without the user's go-ahead.
|
|
250
|
+
|
|
251
|
+
## Goals
|
|
252
|
+
|
|
253
|
+
- `{"safetyFactor": 3, "maxDeflection": 0.5}`: the lightest part that keeps
|
|
254
|
+
that factor and deflects at most 0.5 mm at its loads, in every load case
|
|
255
|
+
(either alone works too). Mechanisms like robot arms are usually limited by
|
|
256
|
+
deflection, not strength. It runs several optimizations, so it is the slowest and the one
|
|
257
|
+
to use when you know the loads.
|
|
258
|
+
With `pip install loadpath[crosscheck]` the pick is confirmed on a
|
|
259
|
+
body-fitted tetrahedral mesh (`goal.confirm`, default `auto`): the report's
|
|
260
|
+
`safetyFactor` is then the tet reading (`confirm.method: "tet"`, voxel
|
|
261
|
+
reading beside it), and the search steps down to where that meets the
|
|
262
|
+
target, typically 0-45% lighter. A warning that the cross-check reads the
|
|
263
|
+
pick below target names the spot the voxel grid cannot resolve: look there.
|
|
264
|
+
It is isotropic, so it never runs for a `process` (FDM layers, SLS, resin)
|
|
265
|
+
or gravity or pressure loads: those are confirmed on a finer voxel grid,
|
|
266
|
+
`deliver.tetCrossCheck` says why. That fine re-check is the memory peak
|
|
267
|
+
`check` reports (`peakMemoryGB`); `LOADPATH_CONFIRM_CELLS=60000` lowers it.
|
|
268
|
+
`goal.method` (default `auto`) picks the optimizer: the volume-fraction
|
|
269
|
+
search first, then the stress-constrained method (MMA, finer grid) when
|
|
270
|
+
the search's part idles (`stressUtilization`, mean over near-peak von
|
|
271
|
+
Mises, under 0.15) and stiffness is not what sets its mass; the lighter
|
|
272
|
+
part is kept and `methodChoice` says which and why. Brackets with one
|
|
273
|
+
hot corner come out 20-55% lighter that way, at up to 4.5x the time.
|
|
274
|
+
`"search"` or `"constrained"` force one.
|
|
275
|
+
- `{"volumeFraction": 0.25, "outcomes": [0.15, 0.4]}`: fixed fractions, a
|
|
276
|
+
trade-off gallery for a person to choose from.
|
|
277
|
+
|
|
278
|
+
## Heat, fatigue and creep
|
|
279
|
+
|
|
280
|
+
- A part next to something hot (a stepper face at 70 C, a heated bed):
|
|
281
|
+
a `thermal` block. `temperatures` hold surfaces at a temperature
|
|
282
|
+
(`region` or inline `shape`, `t` in C), `heat` puts watts in,
|
|
283
|
+
`convection` takes it off (default 8 W/(m² K), still air), `ambient`
|
|
284
|
+
25 by default. The temperature is solved on the part; every load case
|
|
285
|
+
then softens and derates each cell for its temperature and carries the
|
|
286
|
+
restrained expansion. `thermal.cycle` (`period`, `on` s, `cycles`) adds
|
|
287
|
+
heat-up/cool-down fatigue. See examples/thermal/motor-mount-hot.json.
|
|
288
|
+
- Repeated loads: `cycles` on the load case (not on a load). A held load:
|
|
289
|
+
`hours` on the load case (creep rupture and crept deflection). A load of
|
|
290
|
+
unknown in-plane direction with `cycles` counts its cycles once, at the
|
|
291
|
+
worst direction.
|
|
292
|
+
- `durability`: `temperature` (C the cyclic and held cases run at; it
|
|
293
|
+
derates only the fatigue and creep checks, so a part in a hot enclosure
|
|
294
|
+
also needs `"thermal": {"ambient": 55}` (the whole part at that
|
|
295
|
+
temperature, softened for it) for its static safety factor), `safetyFactor` (default 1.5), and
|
|
296
|
+
`constrain: true` to size the part for it rather than only check it.
|
|
297
|
+
With `fasteners`, the preload is also checked held for `preloadHours`
|
|
298
|
+
(default a year).
|
|
299
|
+
- The result is `durability` in each outcome (`fatigueSafetyFactor`,
|
|
300
|
+
`creepSafetyFactor`, per-case `life`), and `deliver.durabilitySafetyFactor`.
|
|
301
|
+
|
|
302
|
+
## Lattices and heat exchangers
|
|
303
|
+
|
|
304
|
+
- A chunky part (a high volume fraction, a solid design) can be filled with a
|
|
305
|
+
TPMS sheet lattice graded by its own stress: `"lattice": {"type":
|
|
306
|
+
"gyroid", "cell": 10, "skin": 1.2}` in the study, or `loadpath_lattice`
|
|
307
|
+
on a finished job. `skin: 0` for SLS, MJF and resin so they drain.
|
|
308
|
+
- Field graphs take strut lattices too (`"type": "octet"`, `kelvin`, `bcc`,
|
|
309
|
+
`fcc`, `voronoi`, ...; `thickness` is the strut diameter, `blend` fillets
|
|
310
|
+
the nodes) and conformal cells (`"frame": {"type": "conformal",
|
|
311
|
+
"surface": "envelope"}`: layers parallel to the skin, no cell cut by it).
|
|
312
|
+
For strength per gram a gyroid sheet beats every strut cell (arm link at
|
|
313
|
+
equal mass: SF 16 vs 9-13); pick struts for open, drainable cells, very
|
|
314
|
+
low density, or FDM (`"aspect": [1, 1, 1.5]` BCC prints unsupported; the
|
|
315
|
+
field report's `lattices` block gives the bridging strut length).
|
|
316
|
+
- Printed lattices are softer than their design (33 published compression
|
|
317
|
+
tests: 1.0-4.1x). Name the study's `process` and the homogenized check
|
|
318
|
+
reads the print: an as-printed stiffness factor per process (FDM, SLS/MJF,
|
|
319
|
+
resin), strut or sheet cell and density, fitted to those tests and held
|
|
320
|
+
out (median error 1.23x, worst 2x; report `latticePrint`). FDM gyroid at
|
|
321
|
+
rho 0.2 reads 0.65 of the design, FDM struts ~0.4, SLS ~0.93. Its
|
|
322
|
+
first-yield reading then sits under every measured strength (sheets
|
|
323
|
+
read strain-based, struts from the stress they carry). It also
|
|
324
|
+
checks wall and strut buckling (`cases[].bucklingFactor`, `governs`).
|
|
325
|
+
Still say a lattice part's deflection is +-20% (2x at worst) unless the
|
|
326
|
+
material is calibrated on the printed gyroid coupon, whose measured
|
|
327
|
+
factor replaces the fit; `process.latticeStiffness` sets one by hand
|
|
328
|
+
(docs/VALIDATION.md, `loadpath.lattice_print`).
|
|
329
|
+
- Don't use a lattice to make a part stiffer per gram: solid topology
|
|
330
|
+
optimization wins that. Use one to stop an impact (a bumper, a hard stop, a
|
|
331
|
+
drop pad): give a field graph an `impact` block (envelope, density node,
|
|
332
|
+
mass and velocity or energy, forceLimit, `"grade": true`, cell and minWall).
|
|
333
|
+
It returns the peak force and crush, and a density table that crushes every
|
|
334
|
+
layer at the same force. A `vibration` block gives natural frequencies with
|
|
335
|
+
point masses. See docs/IMPACT.md in the repo.
|
|
336
|
+
- For a part meant to bend (a TPU finger, a flexure, a snap fit, a
|
|
337
|
+
compliant gripper), give the field graph a `largeDeflection` block:
|
|
338
|
+
`fixed` node, dead `loads`, prescribed `displacements`, `tendons` (path of
|
|
339
|
+
fixed points and guides `{"at": [x, y, z]}`; `tension`, or a servo's
|
|
340
|
+
`stroke` and `stiffness`), `contact` (`obstacles`: plane, sphere,
|
|
341
|
+
cylinder, box, optionally `move`d; `"self": true` for the part on itself)
|
|
342
|
+
and `probes`. It reports how far the load got, peak strain, travel,
|
|
343
|
+
tendon tension and each contact's force and penetration. Frictionless
|
|
344
|
+
unless `contact.friction` (or an obstacle's `friction`) sets a Coulomb
|
|
345
|
+
mu; then each contact also reports its friction force and the nodes
|
|
346
|
+
sliding. A self-contact solve where faces rotate fast can stop short and says so
|
|
347
|
+
(`loadReached`). See docs/MECHANISMS.md.
|
|
348
|
+
- A two-fluid heat exchanger is its own document (`loadpath_schema
|
|
349
|
+
"heatExchanger"`): a core shape, a layout (`cross` or `counter`), cell and
|
|
350
|
+
wall, and an optional duty to rate it. Size it first
|
|
351
|
+
(`loadpath_size_heat_exchanger`, seconds), then design it
|
|
352
|
+
(`loadpath_heat_exchanger`): the report says whether each fluid reaches
|
|
353
|
+
its ports and only its own. With a duty, `"simulate": true` also
|
|
354
|
+
simulates the whole exchanger (porous core, open headers, both fluids and
|
|
355
|
+
the wall; porous.py, about a minute): how evenly each fluid shares out
|
|
356
|
+
over the core (`entry.uniformity`, 1 = even), core vs header pressure
|
|
357
|
+
drop, outlet temperatures and the hottest wall point. Use it to compare
|
|
358
|
+
header depths and port sizes (`porous.sweep`); the rating alone assumes
|
|
359
|
+
even flow. What it found on the 30 mm crossflow cube: size ports per
|
|
360
|
+
fluid (`"portDiameter": {"water": 10, "air": 18}`; a gas side's ports
|
|
361
|
+
set most of its pressure drop), and a thinner seal band (`cap` 1.5 for a
|
|
362
|
+
1 mm wall) opens the starved edge channels; plenum depth hardly matters.
|
|
363
|
+
Together: water 745 -> 267 Pa, air 8.5 -> 1.9 kPa, effectiveness +8%
|
|
364
|
+
(example heat-exchanger-crossflow-tuned).
|
|
365
|
+
Only an envelope, not a core? Give `"envelope": [x, y, z]` (the whole
|
|
366
|
+
part, ports included) and `"sizing": {"process": "lpbf", "maxPressureDrop":
|
|
367
|
+
{...}, "minEffectiveness": 0.7, "objective": "mass"}` instead of `core`,
|
|
368
|
+
and `loadpath_size_heat_exchanger` picks the core's size, cell, wall,
|
|
369
|
+
split, layout, plenum depth and each fluid's port within the process's
|
|
370
|
+
wall and channel limits (sizing.py), returning ranked specs to design.
|
|
371
|
+
For gas sides at high flow the plenum depth matters (a 6 mm plenum behind
|
|
372
|
+
a 47 mm charge-air port cost more than the core). `sizing.verify` then
|
|
373
|
+
simulates a candidate whole; big cross cores skip the design and use
|
|
374
|
+
`porous.simulate_box` (minutes; its header dp is an upper bound).
|
|
375
|
+
On the command line: `loadpath check exchanger.json` lints the spec and
|
|
376
|
+
prints the design grid (`gridStep`, `gridPoints`) and `peakMemoryGB`
|
|
377
|
+
(about 400 bytes a grid point) against what is free, in milliseconds.
|
|
378
|
+
`loadpath heatx spec.json --size --out DIR` writes `sizing.json` (the
|
|
379
|
+
ranked result) and `best.json` (the top spec) to DIR; `--objective`
|
|
380
|
+
is `mass`, `effectiveness` or `pressure` (as `sizing.objective`; the
|
|
381
|
+
flag wins), `--min-wall` and `--splits` narrow the sweep, `--axes xy|yx`
|
|
382
|
+
(or the spec's `axes`) pins which way the first fluid runs. Sized cores
|
|
383
|
+
sit centred in the envelope's frame ([0, 0, 0] to the envelope), so the
|
|
384
|
+
ports land inside it. The sizer returns only designs the design step can
|
|
385
|
+
mesh within its grid cap (16M points, a grid of at most wall / 2; thin
|
|
386
|
+
LPBF walls on a large core are dropped and `note` says so): raise it with
|
|
387
|
+
`--max-points N` for both steps if the machine has the memory. Its
|
|
388
|
+
`solidVolume` and `coreMass` are the core's sheet only; the design
|
|
389
|
+
report's `mass` (g, from `duty.wall`) is the whole part. `loadpath heatx`
|
|
390
|
+
refuses a design whose estimated peak is over the free memory (`--force`
|
|
391
|
+
runs it anyway) and keeps `status.json` in the out folder (`running` with
|
|
392
|
+
its stage, `done`, or `failed` with the error; a run killed for memory
|
|
393
|
+
stays `running` with its pid). `report.warnings` names an outer wall
|
|
394
|
+
under the process's minimum, and for lpbf over 300 mm^2 of flat internal
|
|
395
|
+
ceiling or over 20 mm^3 of trapped powder. The rating's `duty` is the heat
|
|
396
|
+
moved (W, positive whichever fluid is listed first; `dutySign` -1 when
|
|
397
|
+
the first fluid takes it, `hotFluid` names the hot one).
|
|
398
|
+
- Holding a mesh from somewhere else (your own field model, a CAD export)?
|
|
399
|
+
`loadpath_check_printability`, `loadpath_check_isolation` and
|
|
400
|
+
`loadpath_check_fit` check it directly. From Python, `check_dfm` and
|
|
401
|
+
`check_networks` also take the implicit field itself (`f(P) < 0` inside,
|
|
402
|
+
with bounds), which skips meshing and is the fast path for a part built
|
|
403
|
+
from a field.
|
|
404
|
+
|
|
405
|
+
## Multi-material parts
|
|
406
|
+
|
|
407
|
+
For a part printed in two to four materials at once (Bambu AMS, Prusa
|
|
408
|
+
MMU/XL, IDEX, PolyJet): a rigid core with a TPU grip pad, a bumper, an
|
|
409
|
+
isolator. Add a `multiMaterial` block; the optimizer places each material
|
|
410
|
+
as well as the topology (`loadpath_schema` has every field):
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
"multiMaterial": {
|
|
414
|
+
"materials": ["pla", "tpu"],
|
|
415
|
+
"assign": [{"region": "pad", "material": "tpu"},
|
|
416
|
+
{"region": "pivot-boss", "material": "pla"}],
|
|
417
|
+
"interfaceSafetyFactor": 2.0,
|
|
418
|
+
"export": {"slicer": "bambu", "extruders": {"pla": 1, "tpu": 2}}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- `materials`: library names or custom objects; each keeps its own
|
|
423
|
+
stiffness, Poisson's ratio and strength. `assign` fixes cells to one
|
|
424
|
+
material: `region` (a preserve or keepOut id), a `shape`, or `near`
|
|
425
|
+
(`{"region" or "shape", "distance"}` mm from it, optionally `within` a
|
|
426
|
+
region); earlier entries win. Assign the bosses and bearing seats to the
|
|
427
|
+
rigid material and the pad to the soft one, or the optimizer may put
|
|
428
|
+
rubber round a pin. `maxFraction` / `minFraction` cap or floor a
|
|
429
|
+
material's share of the part.
|
|
430
|
+
- `requirements` make it soft where it has to be: `knock` (`energy` J,
|
|
431
|
+
`maxForce` N: a bumper that stops a knock under that peak force) or
|
|
432
|
+
`indentation` (`relativeTo` a case with the same force spread out,
|
|
433
|
+
`atLeast` mm: a pad that gives). Those cases leave the stiffness
|
|
434
|
+
objective, so at least one other case must remain.
|
|
435
|
+
- The bond: every interface face of the delivered bodies is checked against
|
|
436
|
+
the pair's strength (`interfaces: {"pla+tpu": {"tensile", "shear"}}`
|
|
437
|
+
overrides the sourced card; measure your printer's) at
|
|
438
|
+
`interfaceSafetyFactor` (default the goal's safetyFactor, else 2).
|
|
439
|
+
`interlock: true` interlaces the bodies in a beam lattice at the
|
|
440
|
+
interface and credits the bond with its gain. Stress beside supports and
|
|
441
|
+
load nodes is left out of the check; a body thinner than that band (a
|
|
442
|
+
2 mm pad loaded on its face) is checked closer in, and the report's
|
|
443
|
+
`warnings` say so. A material or bond no cell was read for is listed in
|
|
444
|
+
`unchecked`, and the design does not pass.
|
|
445
|
+
- A safety-factor goal runs a short search over volume fractions; it starts
|
|
446
|
+
above the share of the design space that `preserve` fills (a warning says
|
|
447
|
+
when it moved the start).
|
|
448
|
+
- Delivers, in `--out`: an STL per material (`<name>-<material>.stl`, the
|
|
449
|
+
study's coordinates), `<name>.3mf` (one object, one part per material
|
|
450
|
+
with its extruder; `export.slicer` picks the PrusaSlicer or Bambu/Orca
|
|
451
|
+
layout), `optimum.npz` (the design fields) and `report.json`: `mass`,
|
|
452
|
+
`materialMass`, `safetyFactor` (worst material), `interfaceSafetyFactor`
|
|
453
|
+
(worst bond), per case `materials` and `interfaces`, `requirementsMet`,
|
|
454
|
+
`passes`, `outcomes` (each fraction tried) and `print` (supports,
|
|
455
|
+
bridges, islands, when the study has a build direction). `report["3mf"]`
|
|
456
|
+
says where the 3MF puts the part: `"bed"` (default: build direction to
|
|
457
|
+
+z, on the bed at 128,128; `transform` maps study coordinates to the
|
|
458
|
+
file's) or `"study"` (`loadpath run --3mf-coordinates study`: identity,
|
|
459
|
+
as the STLs). `status.json`, `partial/` and the progress lines (an
|
|
460
|
+
`outcome` per fraction verified, with mass and safety factors per
|
|
461
|
+
material and bond) work as for one material.
|
|
462
|
+
- Verify an edited multi-material part from Python (`loadpath verify` is
|
|
463
|
+
single-material). The bodies are in the study's coordinates and must not
|
|
464
|
+
overlap; list the stiff material first (a cell both claim goes to the
|
|
465
|
+
first):
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import json, trimesh
|
|
469
|
+
from loadpath.spec import study_from_dict
|
|
470
|
+
from loadpath.multimat import verify_multimaterial
|
|
471
|
+
st = study_from_dict(json.load(open("study.json")), base_dir=".")
|
|
472
|
+
mm = st.multi_material
|
|
473
|
+
bodies = [(mm.index("pla"), trimesh.load("out/finger-pla.stl")),
|
|
474
|
+
(mm.index("tpu"), trimesh.load("out/finger-tpu.stl"))]
|
|
475
|
+
rep = verify_multimaterial(st, mm, bodies)
|
|
476
|
+
rep["safetyFactor"], rep["interfaceSafetyFactor"], rep.get("unchecked"), rep.get("warnings")
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Only what is connected to the supports is analysed.
|
|
480
|
+
|
|
481
|
+
## Printing direction
|
|
482
|
+
|
|
483
|
+
A `process` of `fdm` or `lpbf` already designs self-supporting along its
|
|
484
|
+
`buildDirection` (the optimizer only builds what prints at 45°); setting
|
|
485
|
+
`printDirection` as well changes nothing. Without a process, set
|
|
486
|
+
`"printDirection": "+z"` (the direction layers stack, in the study's
|
|
487
|
+
coordinates). Preserved geometry is kept as drawn, so a preserved overhang
|
|
488
|
+
(a platform, a ring's underside) still needs support: chamfer it in the
|
|
489
|
+
preserve or accept the supports `deliver` → `print` reports.
|
|
490
|
+
|
|
491
|
+
## Resolution
|
|
492
|
+
|
|
493
|
+
`resolution.maxElements` trades detail for time: 20k for a first look,
|
|
494
|
+
40-60k for a part you will print (each optimization scales a little worse
|
|
495
|
+
than linearly with cells; a safety-factor search runs several). Members thinner than
|
|
496
|
+
about two cells cannot form; `check` warns when `minMember` is too fine for
|
|
497
|
+
the grid. `maxElements` is a target the pitch is estimated from, so the grid
|
|
498
|
+
can come out a few percent over; set `resolution.pitch` for an exact grid.
|
|
499
|
+
A safety-factor search re-checks its pick on a finer grid (up to 150k
|
|
500
|
+
cells, about 55 kB a cell): that stage is capped to the memory free at the
|
|
501
|
+
time, and `LOADPATH_CONFIRM_CELLS` sets it outright on a shared machine.
|
|
502
|
+
|
|
503
|
+
## Length scales and sealed voids
|
|
504
|
+
|
|
505
|
+
`minMember` (mm) is the thinnest member, `minHole` the narrowest hole or
|
|
506
|
+
gap. A `process` sets both if the study does not (2 x its minFeature for
|
|
507
|
+
members: SLS/MJF/FDM 1.6; holes FDM/SLS/MJF 2, SLA/DLP/LPBF 1). With
|
|
508
|
+
neither, the filter still holds members to 1.4 mm, so a fine grid cannot
|
|
509
|
+
grow sub-millimetre skins. A hole narrower than `minHole` comes out filled.
|
|
510
|
+
|
|
511
|
+
Stiffness likes closed sections, so a fine grid can seal a void (a skin
|
|
512
|
+
over a cable channel). For powder and resin processes and LPBF
|
|
513
|
+
(`"drains": "auto"`, the default) the optimizer then drills escape holes
|
|
514
|
+
(SLS 6 mm, MJF 7.5, SLA/DLP 5.5, LPBF 6.8; at least 2 per void, more until
|
|
515
|
+
no powder is over 60 mm from one) through thin, low-stress wall, and runs on
|
|
516
|
+
so the part carries its load round them. `"drains": true` forces it for
|
|
517
|
+
any process, `false` turns it off, or give `{"diameter", "count",
|
|
518
|
+
"maxPath", "iterations"}`. `report.json`'s `optimizer.escape` lists the
|
|
519
|
+
holes; `enclosedCavityVolume` should read 0.
|
|
520
|
+
|
|
521
|
+
## A robot arm link, end to end
|
|
522
|
+
|
|
523
|
+
```json
|
|
524
|
+
{
|
|
525
|
+
"name": "upper-arm",
|
|
526
|
+
"designSpace": [{"type": "box", "min": [-16, -18, -12], "max": [156, 18, 12]}],
|
|
527
|
+
"preserve": [
|
|
528
|
+
{"id": "shoulder-boss", "shape": {"type": "cylinder", "center": [0, 0, 0], "axis": "z", "length": 24, "r": 16}},
|
|
529
|
+
{"id": "wrist-boss", "shape": {"type": "cylinder", "center": [140, 0, 0], "axis": "z", "length": 24, "r": 15}}
|
|
530
|
+
],
|
|
531
|
+
"keepOut": [
|
|
532
|
+
{"id": "shoulder-bearing", "shape": {"type": "cylinder", "center": [0, 0, 0], "axis": "z", "length": 30, "r": 11}},
|
|
533
|
+
{"id": "wrist-bearing", "shape": {"type": "cylinder", "center": [140, 0, 0], "axis": "z", "length": 30, "r": 11}},
|
|
534
|
+
{"id": "cable-channel", "shape": {"type": "cylinder", "p0": [30, 0, 0], "p1": [110, 0, 0], "r": 5}}
|
|
535
|
+
],
|
|
536
|
+
"supports": [{"region": "shoulder-bearing"}],
|
|
537
|
+
"loadCases": [
|
|
538
|
+
{"name": "payload", "loads": [{"region": "wrist-bearing", "force": [0, -20, 0]}]},
|
|
539
|
+
{"name": "side", "loads": [{"region": "wrist-bearing", "force": [0, 0, 8]}]}
|
|
540
|
+
],
|
|
541
|
+
"material": "pa12",
|
|
542
|
+
"goal": {"safetyFactor": 3.0},
|
|
543
|
+
"minMember": 3.0,
|
|
544
|
+
"symmetry": ["z"],
|
|
545
|
+
"resolution": {"maxElements": 40000}
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
1 kg at the wrist × 9.81 × 2 (dynamic) ≈ 20 N. The side case keeps the link
|
|
550
|
+
from being a flat plate that folds sideways. `symmetry: ["z"]` makes it
|
|
551
|
+
printable flat in either orientation.
|
|
552
|
+
|
|
553
|
+
For a link in a mechanism with a URDF, don't write those loads by hand:
|
|
554
|
+
derive them, and the neighbours' swept space, from the arm itself.
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
loadpath assembly robot.urdf --link forearm --study forearm.json --region j4-mount \
|
|
558
|
+
--payload 1.0 --payload-at 0,0,0.07 --payload-radius 0.03 --out derived/
|
|
559
|
+
loadpath run derived/forearm.derived.json
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
It sweeps poses over the joint limits, speeds and accelerations (scaled to
|
|
563
|
+
what each joint's rated effort can deliver), a scattered payload, a push at
|
|
564
|
+
the tool, each joint upstream stalled against an obstacle, and the part's
|
|
565
|
+
far joint jammed at its full rated torque (a hard stop or a blocked
|
|
566
|
+
hand: a servo-driven link must survive its own servo's stall torque), does
|
|
567
|
+
Newton-Euler for the wrench the part carries at its far joint, and keeps the
|
|
568
|
+
few samples that set the peak stress everywhere in the solid design space
|
|
569
|
+
(`assembly.json` says how close they come: `envelopeRatio`). The parent and
|
|
570
|
+
child links, swept through their joints' ranges, become `bore: false` mesh
|
|
571
|
+
keep-outs with the study's preserved interfaces cut out. Inputs worth
|
|
572
|
+
setting: `--ramp` (seconds to full speed, what the controller allows),
|
|
573
|
+
`--push` (N, the hardest contact at the tool in any direction, default 20: often the case that sizes the part, so set it from the job, 0 if nothing pushes) and `--balanced`
|
|
574
|
+
for joints a spring counterbalances. Write the study in the URDF's world
|
|
575
|
+
frame at the home pose (mm), or pass `--frame link` if it is in the link's own
|
|
576
|
+
frame (origin at its joint): a mismatch silently gives wrong moment arms. The
|
|
577
|
+
study needs no load cases (they are replaced; `fasteners` and their preload are
|
|
578
|
+
kept), though `loadpath check` on it alone wants one: check the derived study.
|
|
579
|
+
`sweptKeepOuts` is empty when the URDF has no collision or visual geometry.
|
|
580
|
+
The derived loads are the sweep's own accelerations: no extra dynamic factor. Python: `loadpath.assembly`
|
|
581
|
+
(`Robot.from_urdf`, `sweep_loads`, `governing_cases`, `swept_keepouts`,
|
|
582
|
+
`derive_study`).
|
|
583
|
+
|
|
584
|
+
The full field reference is in README.md next to this file.
|
loadpath/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""loadpath: generative design and engineering checks for 3D printed parts.
|
|
2
|
+
|
|
3
|
+
Studies are JSON (see loadpath.spec and schema/); run one with
|
|
4
|
+
loadpath.study.run_study, or use the CLI (`loadpath run`) or the MCP server
|
|
5
|
+
(`loadpath mcp`). Units are mm, N, MPa. See docs/VALIDATION.md for what the
|
|
6
|
+
models have been checked against.
|
|
7
|
+
"""
|
|
8
|
+
import os as _os
|
|
9
|
+
|
|
10
|
+
# One BLAS thread unless the caller says otherwise. The heavy work is sparse
|
|
11
|
+
# (CG, multigrid) and already runs a thread per load case and a process per
|
|
12
|
+
# outcome; OpenBLAS's own pool of busy-waiting threads, one per core, fought
|
|
13
|
+
# both: an arm-link run took 112 s with it and 88 s without, and two side by
|
|
14
|
+
# side 330 s against 97 s on 4 cores. (Only effective before numpy loads;
|
|
15
|
+
# worker processes start fresh and always get it.)
|
|
16
|
+
for _v in ("OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS", "VECLIB_MAXIMUM_THREADS"):
|
|
17
|
+
_os.environ.setdefault(_v, "1")
|