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/fasteners.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ Fasteners as interfaces, not bosses.
3
+
4
+ A bolted joint needs very little of the part to be fixed: a flat ring for
5
+ the head or washer to bear on, a flat ring where it clamps against the
6
+ mating part (or a sleeve of material for the thread, an insert or a nut), a
7
+ clear hole, and a clear path for the head and the driver. Drawing a solid
8
+ boss round all of that (the old habit) preserves material the joint does not
9
+ need; drawing only the hole loses the clamp. This expands a `fasteners`
10
+ entry into exactly those preserve and keep-out regions, plus a self-balanced
11
+ preload case that squeezes the seat towards the far end, so the optimizer
12
+ grows its own compression cone between them instead of inheriting a solid
13
+ cylinder.
14
+
15
+ "fasteners": [{
16
+ "id": "top", "size": "M5",
17
+ "seat": [6, 0, -20], # centre of the face the head bears on
18
+ "direction": "-x", # from the seat into the part, the way the screw goes
19
+ "grip": 6, # mm from the seat to the far end
20
+ "end": "face", # "face" (clamps on a mating part), "thread", or "none"
21
+ "headOn": "this", # "mating": the head is on the mating part, which bears on
22
+ # this part at the seat (no head or driver path to keep clear)
23
+ "threadLength": 8, # end = "thread": engagement length past the grip
24
+ "insert": 6.4, # optional: heat-set insert or nut OD; else a tapped hole
25
+ "preload": 600 # N; default from the seat's bearing area
26
+ }]
27
+
28
+ A screw driven from the mating part into this one is `"headOn": "mating"`,
29
+ `"end": "thread"`: the seat is then where the mating part clamps down, and
30
+ only the bore and the tap are kept clear.
31
+
32
+ Regions it adds, by id: `<id>-bore`, `<id>-head` (unless the head is on
33
+ the other part; and `<id>-tap` for a thread) as keep-outs; `<id>-seat` and `<id>-face` or `<id>-thread` as
34
+ preserves. Supports and loads can name any of them. Hold a bore across
35
+ the screw only (`"fix": "yz"` for a screw along x): fixed along it too,
36
+ the bore would hold the preload ring and the preload would do nothing.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import copy
41
+
42
+ import numpy as np
43
+
44
+ __all__ = ["expand_fasteners", "METRIC"]
45
+
46
+ # ISO metric: (medium clearance hole, ISO 4762 socket head diameter), mm.
47
+ # The heads were a mix of pan, socket and washer sizes (M4 8.5 mm): a brief
48
+ # with 8 mm M4 counterbores read 678 mm^3 of keep-out intrusion against the
49
+ # default's 10.5 mm head keep-out (2026-11-01). Give `head` for a washer or a
50
+ # pan head, `counterbore` for the pocket the head sits in.
51
+ METRIC = {"M2": (2.4, 3.8), "M2.5": (2.9, 4.5), "M3": (3.4, 5.5), "M4": (4.5, 7.0),
52
+ "M5": (5.5, 8.5), "M6": (6.6, 10.0), "M8": (9.0, 13.0), "M10": (11.0, 16.0)}
53
+ _AXES = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}
54
+
55
+
56
+ def _direction(v, label):
57
+ if isinstance(v, str):
58
+ s = v.strip().lower()
59
+ sign = -1.0 if s.startswith("-") else 1.0
60
+ ax = s.lstrip("+-")
61
+ if ax not in _AXES:
62
+ raise ValueError(f"{label}.direction must be like '-x' or a vector, got {v!r}")
63
+ return sign * np.asarray(_AXES[ax], float)
64
+ a = np.asarray(v, float).reshape(-1)
65
+ if a.shape != (3,) or not np.linalg.norm(a) > 0:
66
+ raise ValueError(f"{label}.direction must be like '-x' or a non-zero vector")
67
+ return a / np.linalg.norm(a)
68
+
69
+
70
+ def _tube(p0, p1, r, r_inner=0.0):
71
+ out = {"type": "cylinder", "p0": np.round(p0, 4).tolist(), "p1": np.round(p1, 4).tolist(), "r": float(r)}
72
+ if r_inner:
73
+ out["rInner"] = float(r_inner)
74
+ return out
75
+
76
+
77
+ def _exit_distance(d: dict, p, direction) -> float:
78
+ """How far from p along `direction` the ray leaves the bounding box of
79
+ the design space's boxes (0 when it has none)."""
80
+ boxes = [s for s in d.get("designSpace") or [] if isinstance(s, dict) and s.get("type") == "box"]
81
+ if not boxes:
82
+ return 0.0
83
+ lo = np.min([b["min"] for b in boxes], axis=0)
84
+ hi = np.max([b["max"] for b in boxes], axis=0)
85
+ p, u = np.asarray(p, float), np.asarray(direction, float)
86
+ t = np.inf
87
+ for k in range(3):
88
+ if u[k] > 1e-12:
89
+ t = min(t, (hi[k] - p[k]) / u[k])
90
+ elif u[k] < -1e-12:
91
+ t = min(t, (lo[k] - p[k]) / u[k])
92
+ return float(max(t, 0.0)) if np.isfinite(t) else 0.0
93
+
94
+
95
+ def expand_fasteners(d: dict) -> dict:
96
+ """The study with each `fasteners` entry turned into regions and a
97
+ preload case. Returns d itself when there are none."""
98
+ fs = d.get("fasteners")
99
+ if not fs:
100
+ return d
101
+ from .materials import material_from_spec
102
+
103
+ d = copy.deepcopy(d)
104
+ preserve = list(d.get("preserve") or [])
105
+ keep_out = list(d.get("keepOut") or [])
106
+ mat_in = d.get("material", "pla")
107
+ mat = material_from_spec(mat_in[0] if isinstance(mat_in, list) else mat_in)
108
+ preload_loads = []
109
+ joints = []
110
+ for i, f in enumerate(fs):
111
+ label = f"fasteners[{i}]"
112
+ fid = f.get("id") or f"fastener-{i}"
113
+ size = f.get("size")
114
+ if size is not None and size not in METRIC:
115
+ raise ValueError(f"{label}.size must be one of {sorted(METRIC)} (or give diameter and head)")
116
+ hole, head = METRIC[size] if size else (None, None)
117
+ hole = float(f.get("hole", hole or 0.0)) or (1.1 * float(f["diameter"]) if "diameter" in f else None)
118
+ head = float(f.get("head", head or 0.0)) or None
119
+ if not hole or not head:
120
+ raise ValueError(f"{label} needs a size (e.g. 'M5') or diameter and head (mm)")
121
+ nominal = float(f.get("diameter", hole / 1.1))
122
+ if "seat" not in f or "direction" not in f or "grip" not in f:
123
+ raise ValueError(f"{label} needs seat (point), direction (into the part) and grip (mm)")
124
+ seat = np.asarray(f["seat"], float)
125
+ a = _direction(f["direction"], label)
126
+ grip = float(f["grip"])
127
+ if grip < 0 or (grip == 0 and str(f.get("end", "face")) != "thread"):
128
+ raise ValueError(f"{label}.grip is mm from the seat to the far end: > 0, or 0 for a thread or "
129
+ "insert that starts right at the seat")
130
+ end_kind = str(f.get("end", "face"))
131
+ if end_kind not in ("face", "thread", "none"):
132
+ raise ValueError(f"{label}.end must be 'face', 'thread' or 'none'")
133
+ head_on = str(f.get("headOn", "this"))
134
+ if head_on not in ("this", "mating"):
135
+ raise ValueError(f"{label}.headOn must be 'this' or 'mating'")
136
+ # the rings: a little wider than the head, a couple of layers thick;
137
+ # the thread sleeve's wall about a third of the screw's diameter.
138
+ # (The optimizer adds what the loads need around them.)
139
+ wall = float(f.get("wall", max(1.0, 0.15 * nominal)))
140
+ ring = float(f.get("ring", max(2.0, 0.4 * nominal)))
141
+ sleeve = float(f.get("sleeveWall", max(1.5, 0.35 * nominal)))
142
+ # The driver comes in from outside the part, so by default its path
143
+ # runs on until it leaves the design space: a fixed 60 mm path ended
144
+ # inside the wall bracket's 120 mm arm, the optimizer built on along
145
+ # the driver's line past it, and verify read the bore closed there
146
+ # (wall-bracket-fasteners, every fraction from 4% to 19%).
147
+ driver = float(f.get("driver", max(60.0, _exit_distance(d, seat, -a) + 2.0)))
148
+ end = seat + a * grip
149
+ r_ring = 0.5 * head + wall
150
+ keep_out.append({"id": f"{fid}-bore", "shape": _tube(seat - a * 1.0, end + a * 2.0, 0.5 * hole)})
151
+ if head_on == "this":
152
+ # as wide as the seat ring: a head path narrower than the ring
153
+ # leaves a sub-millimetre lip of ring standing round it
154
+ cb = f.get("counterbore")
155
+ if cb is not None and float(cb) <= head:
156
+ raise ValueError(f"{label}.counterbore ({float(cb):g} mm) must be wider than the head ({head:g} mm)")
157
+ r_head = 0.5 * float(cb) if cb is not None else max(0.5 * head + 0.5, r_ring)
158
+ keep_out.append({"id": f"{fid}-head", "shape": _tube(seat, seat - a * driver, r_head)})
159
+ preserve.append({"id": f"{fid}-seat", "shape": _tube(seat, seat + a * ring, r_ring, 0.5 * hole)})
160
+ far = None
161
+ if end_kind == "face":
162
+ far = f"{fid}-face"
163
+ preserve.append({"id": far, "shape": _tube(end - a * ring, end, r_ring, 0.5 * hole)})
164
+ elif end_kind == "thread":
165
+ tl = float(f.get("threadLength", 2.0 * nominal))
166
+ insert = f.get("insert")
167
+ r_hole = 0.5 * float(insert) if insert else 0.42 * nominal # insert bore, or tap drill
168
+ r_sleeve = (0.5 * float(insert) if insert else 0.5 * nominal) + sleeve
169
+ far = f"{fid}-thread"
170
+ keep_out.append({"id": f"{fid}-tap", "shape": _tube(end - a * 0.5, end + a * (tl + 1.0), r_hole)})
171
+ preserve.append({"id": far, "shape": _tube(end, end + a * tl, r_sleeve, r_hole)})
172
+ if far is not None:
173
+ if "preload" in f:
174
+ p = float(f["preload"])
175
+ else:
176
+ # a quarter of what the seat can bear: plastics creep under
177
+ # sustained bearing stress
178
+ area = 0.25 * np.pi * (head ** 2 - hole ** 2)
179
+ p = 0.25 * mat.strength * area
180
+ # ...and no more than a class 8.8 screw is tightened to (about
181
+ # 60% of its proof load on the stress area): a titanium seat
182
+ # read 10 kN for an M4, three times what the screw carries
183
+ screw = 0.6 * 600.0 * 0.25 * np.pi * (0.83 * nominal) ** 2
184
+ p = min(p, screw)
185
+ if p > 0:
186
+ joints.append({"id": fid, "preload": p, "area": 0.25 * np.pi * (head ** 2 - hole ** 2),
187
+ "axis": a.tolist()})
188
+ preload_loads.append({"region": f"{fid}-seat", "force": (a * p).tolist()})
189
+ preload_loads.append({"region": far, "force": (-a * p).tolist()})
190
+ d["preserve"] = preserve
191
+ d["keepOut"] = keep_out
192
+ if preload_loads:
193
+ cases = list(d.get("loadCases") or [])
194
+ cases.append({"name": "preload", "loads": preload_loads})
195
+ d["loadCases"] = cases
196
+ d.pop("fasteners")
197
+ # for the durability check's preload relaxation (spec pops it)
198
+ d["_joints"] = joints
199
+ return d