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.
Files changed (220) hide show
  1. loadpath/README.md +294 -0
  2. loadpath/SKILL.md +584 -0
  3. loadpath/__init__.py +17 -0
  4. loadpath/__main__.py +1202 -0
  5. loadpath/am.py +316 -0
  6. loadpath/amgfast.py +326 -0
  7. loadpath/assembly.py +985 -0
  8. loadpath/benchmarks/__init__.py +0 -0
  9. loadpath/benchmarks/beams.py +266 -0
  10. loadpath/benchmarks/cases/ge-bracket.json +380 -0
  11. loadpath/benchmarks/evaluate.py +172 -0
  12. loadpath/benchmarks/mechanisms.py +416 -0
  13. loadpath/benchmarks/problems.py +140 -0
  14. loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
  15. loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
  16. loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
  17. loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
  18. loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
  19. loadpath/benchmarks/reference.py +138 -0
  20. loadpath/benchmarks/run.py +301 -0
  21. loadpath/brep.py +1470 -0
  22. loadpath/bucklingopt.py +300 -0
  23. loadpath/buildsim.py +1490 -0
  24. loadpath/cadimport.py +904 -0
  25. loadpath/calibration.py +885 -0
  26. loadpath/cavities.py +211 -0
  27. loadpath/checks/__init__.py +0 -0
  28. loadpath/checks/dfm.py +354 -0
  29. loadpath/checks/exchanger.py +7 -0
  30. loadpath/checks/fea.py +212 -0
  31. loadpath/checks/fit.py +475 -0
  32. loadpath/checks/networks.py +260 -0
  33. loadpath/checks/opening.py +67 -0
  34. loadpath/checks/sdf_kit.py +7 -0
  35. loadpath/conjugate.py +267 -0
  36. loadpath/constrained.py +329 -0
  37. loadpath/contact.py +691 -0
  38. loadpath/coopt.py +342 -0
  39. loadpath/cores.py +163 -0
  40. loadpath/crease.py +1138 -0
  41. loadpath/critic.py +212 -0
  42. loadpath/crosscheck.py +1107 -0
  43. loadpath/cutcell.py +510 -0
  44. loadpath/data/lattice_buckling.json +439 -0
  45. loadpath/data/lattice_tensors.json +27449 -0
  46. loadpath/data/published_experiments.json +71 -0
  47. loadpath/decimate.py +177 -0
  48. loadpath/delivery.py +190 -0
  49. loadpath/directions.py +262 -0
  50. loadpath/domain.py +630 -0
  51. loadpath/drains.py +615 -0
  52. loadpath/durability.py +413 -0
  53. loadpath/editor.html +3135 -0
  54. loadpath/editor.py +610 -0
  55. loadpath/escape.py +354 -0
  56. loadpath/evals/__init__.py +0 -0
  57. loadpath/evals/baselines/coarse.json +1543 -0
  58. loadpath/evals/baselines/full.json +1497 -0
  59. loadpath/evals/brief_kinds/__init__.py +1 -0
  60. loadpath/evals/brief_kinds/heatx.py +387 -0
  61. loadpath/evals/brief_kinds/lattice.py +571 -0
  62. loadpath/evals/brief_kinds/multimat.py +593 -0
  63. loadpath/evals/briefs/assets/bracket.step +1201 -0
  64. loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
  65. loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
  66. loadpath/evals/briefs/assets/motor-body.stl +0 -0
  67. loadpath/evals/briefs/assets/sketch.png +0 -0
  68. loadpath/evals/briefs/base-adapter-fdm.json +492 -0
  69. loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
  70. loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
  71. loadpath/evals/briefs/camera-boom-mjf.json +339 -0
  72. loadpath/evals/briefs/camera-mount-pla.json +203 -0
  73. loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
  74. loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
  75. loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
  76. loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
  77. loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
  78. loadpath/evals/briefs/forearm-fdm.json +146 -0
  79. loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
  80. loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
  81. loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
  82. loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
  83. loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
  84. loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
  85. loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
  86. loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
  87. loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
  88. loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
  89. loadpath/evals/briefs/lighten-step-bracket.json +220 -0
  90. loadpath/evals/briefs/nema17-mount-stl.json +429 -0
  91. loadpath/evals/briefs/pillow-block-sls.json +220 -0
  92. loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
  93. loadpath/evals/briefs/rod-hook-pla.json +154 -0
  94. loadpath/evals/briefs/sensor-arm-resin.json +150 -0
  95. loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
  96. loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
  97. loadpath/evals/briefs/sketch-clevis.json +229 -0
  98. loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
  99. loadpath/evals/briefs/tool-flange-ti64.json +280 -0
  100. loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
  101. loadpath/evals/briefs/turret-fdm.json +281 -0
  102. loadpath/evals/briefs/upper-arm-sls.json +179 -0
  103. loadpath/evals/briefs/urdf-forearm.json +233 -0
  104. loadpath/evals/briefs/wrist-link-mjf.json +278 -0
  105. loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
  106. loadpath/evals/briefs.py +393 -0
  107. loadpath/evals/cases/arm-link-cast.json +157 -0
  108. loadpath/evals/cases/arm-link-driven.json +171 -0
  109. loadpath/evals/cases/arm-link-sandwich.json +315 -0
  110. loadpath/evals/cases/arm-link.json +111 -0
  111. loadpath/evals/cases/bell-crank.json +116 -0
  112. loadpath/evals/cases/grip-turret.json +479 -0
  113. loadpath/evals/cases/grip-upper-arm.json +253 -0
  114. loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
  115. loadpath/evals/cases/grip-wrist-yoke.json +831 -0
  116. loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
  117. loadpath/evals/cases/mbb-beam.json +61 -0
  118. loadpath/evals/cases/motor-body.stl +0 -0
  119. loadpath/evals/cases/motor-mount.json +265 -0
  120. loadpath/evals/cases/printed-hook.json +77 -0
  121. loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
  122. loadpath/evals/cases/wall-bracket.json +126 -0
  123. loadpath/evals/corpus.py +179 -0
  124. loadpath/evals/pareto.py +90 -0
  125. loadpath/evals/run.py +860 -0
  126. loadpath/examples/arm-link.json +25 -0
  127. loadpath/examples/bumpered-bracket-compression.json +225 -0
  128. loadpath/examples/bumpered-bracket-lattice.json +204 -0
  129. loadpath/examples/bumpered-bracket.json +34 -0
  130. loadpath/examples/gripper-finger-mm.json +185 -0
  131. loadpath/examples/heat-exchanger-counterflow.json +21 -0
  132. loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
  133. loadpath/examples/heat-exchanger-crossflow.json +20 -0
  134. loadpath/examples/pedal-lattice.json +23 -0
  135. loadpath/examples/pressure-plenum.json +12 -0
  136. loadpath/examples/shelf-bracket.json +27 -0
  137. loadpath/exchanger.py +831 -0
  138. loadpath/fasteners.py +199 -0
  139. loadpath/fem.py +668 -0
  140. loadpath/femops.py +70 -0
  141. loadpath/fieldjit.py +981 -0
  142. loadpath/fieldops.py +119 -0
  143. loadpath/fields.py +3697 -0
  144. loadpath/fieldtorch.py +464 -0
  145. loadpath/flow.py +984 -0
  146. loadpath/flowtopo.py +300 -0
  147. loadpath/gpu.py +521 -0
  148. loadpath/heatx.py +1067 -0
  149. loadpath/heatx_thermal.py +432 -0
  150. loadpath/homogenized.py +886 -0
  151. loadpath/ibrep.py +552 -0
  152. loadpath/impact.py +693 -0
  153. loadpath/infill.py +376 -0
  154. loadpath/interface_library.py +340 -0
  155. loadpath/interlock.py +186 -0
  156. loadpath/isolator.py +149 -0
  157. loadpath/knn.py +204 -0
  158. loadpath/lattice.py +1065 -0
  159. loadpath/lattice3mf.py +760 -0
  160. loadpath/lattice_buckling.py +245 -0
  161. loadpath/lattice_grade.py +325 -0
  162. loadpath/lattice_orient.py +595 -0
  163. loadpath/lattice_print.py +159 -0
  164. loadpath/lattice_tensors.py +558 -0
  165. loadpath/latticestep.py +1210 -0
  166. loadpath/manufacture.py +446 -0
  167. loadpath/material_library.py +489 -0
  168. loadpath/materials.py +144 -0
  169. loadpath/mcp_server.py +786 -0
  170. loadpath/mechanism.py +218 -0
  171. loadpath/mechanism_nl.py +811 -0
  172. loadpath/meshops.py +452 -0
  173. loadpath/mg.py +853 -0
  174. loadpath/mma.py +214 -0
  175. loadpath/modal.py +210 -0
  176. loadpath/multimat.py +1788 -0
  177. loadpath/narrowband.py +418 -0
  178. loadpath/nlkernel.py +241 -0
  179. loadpath/nonlinear.py +1095 -0
  180. loadpath/orient.py +334 -0
  181. loadpath/porous.py +994 -0
  182. loadpath/pressure.py +171 -0
  183. loadpath/preview.py +335 -0
  184. loadpath/printcheck.py +263 -0
  185. loadpath/printsetup.py +207 -0
  186. loadpath/processcheck.py +376 -0
  187. loadpath/quick.py +113 -0
  188. loadpath/recall.py +406 -0
  189. loadpath/reconstruct.py +797 -0
  190. loadpath/render.py +436 -0
  191. loadpath/resolved.py +189 -0
  192. loadpath/schema.py +1701 -0
  193. loadpath/sdf_kit.py +843 -0
  194. loadpath/selfx.py +292 -0
  195. loadpath/shapes.py +584 -0
  196. loadpath/sharp.py +717 -0
  197. loadpath/simp.py +1170 -0
  198. loadpath/sizing.py +312 -0
  199. loadpath/slicer.py +321 -0
  200. loadpath/spec.py +1072 -0
  201. loadpath/step.py +806 -0
  202. loadpath/struts.py +639 -0
  203. loadpath/study.py +1937 -0
  204. loadpath/tasks.py +822 -0
  205. loadpath/thermal.py +1691 -0
  206. loadpath/threemf.py +487 -0
  207. loadpath/tune.py +437 -0
  208. loadpath/validation.py +145 -0
  209. loadpath/verify.py +1215 -0
  210. loadpath/viewer.html +694 -0
  211. loadpath/viewer.py +446 -0
  212. loadpath/viscoelastic.py +106 -0
  213. loadpath/voxelprint.py +582 -0
  214. loadpath/voxels.py +402 -0
  215. loadpath-0.1.1.dist-info/METADATA +319 -0
  216. loadpath-0.1.1.dist-info/RECORD +220 -0
  217. loadpath-0.1.1.dist-info/WHEEL +5 -0
  218. loadpath-0.1.1.dist-info/entry_points.txt +2 -0
  219. loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
  220. 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")