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
File without changes
@@ -0,0 +1,266 @@
1
+ """
2
+ Thin-member check of the voxel hex8 against CalculiX.
3
+
4
+ Strips 1, 2, 3 and 5 cells thick, at 0, 30 and 45 degrees to the voxel grid,
5
+ are clamped at one end and loaded at the other by a tip shear (bending) or a
6
+ tip pull (tension). The voxel model is the production one: cells whose centre
7
+ is inside the strip, `fem.Stiffness` with the element in force (incompatible
8
+ modes by default, LOADPATH_HEX8=plain for the old trilinear hex8) and
9
+ `verify.nodal_stress`. The reference is CalculiX on the exact strip, a
10
+ structured quadratic hex mesh (C3D20, four elements through the thickness).
11
+
12
+ Both are scored the way a peak-stress check reads them: the largest von Mises
13
+ over a window away from the clamp and the load (30-70% of the length),
14
+ divided pointwise by beam theory (6 F (s_F - s) / (w t^2), or F / (w t)).
15
+ `ratio` is voxel / CalculiX on that peak: under 1 is the unsafe direction.
16
+ `stiffness` is voxel / CalculiX on the load-point displacement.
17
+ "cut" is the cut-cell model (cutcell.py, verify's resolution.cutCells).
18
+
19
+ python -m loadpath.benchmarks.beams [--quick] [--out beams.json]
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import os
26
+ import shutil
27
+ import subprocess
28
+ import tempfile
29
+
30
+ import numpy as np
31
+ from scipy.sparse.linalg import spsolve
32
+
33
+ from ..fem import HexMesh, Stiffness, von_mises
34
+
35
+ E, NU, F = 2000.0, 0.35, 1.0
36
+ W = 3.0 # strip width (out of the bending plane), in cells
37
+ OFFSET = np.array([0.37, 0.21, 0.0])
38
+ ZONE = 1.5 # the end load is a body force over the last 1.5 cells
39
+
40
+
41
+ class Strip:
42
+ """A t x W strip along direction theta in the xy plane, clamped over
43
+ s in [-c, 0], loaded over s in [L - ZONE, L]."""
44
+
45
+ def __init__(self, t: float, theta_deg: float):
46
+ self.t = float(t)
47
+ self.L = float(max(12.0 * t, 24.0))
48
+ self.c = 2.0
49
+ th = np.radians(theta_deg)
50
+ self.a = np.array([np.cos(th), np.sin(th), 0.0])
51
+ self.n = np.array([-np.sin(th), np.cos(th), 0.0])
52
+
53
+ def local(self, p):
54
+ d = np.asarray(p, float) - OFFSET
55
+ return d @ self.a, d @ self.n, d[..., 2]
56
+
57
+ def world(self, s, y, z):
58
+ return OFFSET + np.outer(s, self.a) + np.outer(y, self.n) + np.outer(z, [0.0, 0.0, 1.0])
59
+
60
+ def inside(self, p):
61
+ s, y, z = self.local(p)
62
+ return (s >= -self.c) & (s <= self.L) & (np.abs(y) <= self.t / 2) & (z >= 0) & (z <= W)
63
+
64
+ def beam(self, load, s, s_f):
65
+ if load == "tension":
66
+ return np.full_like(s, F / (W * self.t))
67
+ return 6.0 * F * (s_f - s) / (W * self.t ** 2)
68
+
69
+ def direction(self, load):
70
+ return self.a if load == "tension" else self.n
71
+
72
+ @property
73
+ def window(self):
74
+ return 0.3 * self.L, 0.7 * self.L
75
+
76
+
77
+ def voxel(strip: Strip, load: str, element: str | None = None, pitch: float = 1.0) -> dict:
78
+ """The production voxel model of the strip at `pitch` (cells; the strip
79
+ is t cells thick at pitch 1)."""
80
+ lo3 = np.array([-strip.c, -strip.t / 2, 0.0])
81
+ hi3 = np.array([strip.L, strip.t / 2, W])
82
+ corners = strip.world(*np.array(np.meshgrid(*zip(lo3, hi3))).reshape(3, -1))
83
+ h = float(pitch)
84
+ origin = (np.floor(corners.min(0) / h) - 1.0) * h
85
+ origin[2] = 0.0
86
+ dims = np.ceil((corners.max(0) - origin) / h).astype(int) + 1
87
+ dims[2] = int(round(W / h))
88
+ ijk = np.argwhere(np.ones(dims, bool))
89
+ centres = origin + (ijk + 0.5) * h
90
+ ijk = ijk[strip.inside(centres)]
91
+ mesh = HexMesh(ijk, h, origin)
92
+ s_node = strip.local(mesh.node_xyz)[0]
93
+ fixed = np.repeat(s_node < 0, 3)
94
+ # end load: a uniform body force over the cells of the end zone
95
+ s_cell = strip.local(mesh.origin + (mesh.elem_ijk + 0.5) * h)[0]
96
+ zone = s_cell > strip.L - ZONE
97
+ wn = np.bincount(mesh.enode[zone].ravel(), minlength=mesh.n_nodes) / (8.0 * zone.sum())
98
+ f = (F * wn[:, None] * strip.direction(load)[None]).ravel()
99
+ s_f = float(s_cell[zone].mean())
100
+ st = Stiffness(mesh, NU, fixed, element=element)
101
+ k = st.assemble(np.full(mesh.n_elem, E))
102
+ u = np.zeros(mesh.n_dof)
103
+ free = ~fixed
104
+ u[free] = spsolve(k[free][:, free].tocsc(), f[free])
105
+ from ..verify import nodal_stress
106
+
107
+ vm = von_mises(nodal_stress(st, u, E))
108
+ lo, hi = strip.window
109
+ win = (s_node >= lo) & (s_node <= hi)
110
+ return {"peak": float(np.max(vm[win] / strip.beam(load, s_node[win], s_f))),
111
+ "displacement": float(f @ u) / F, "cells": int(mesh.n_elem), "element": st.element}
112
+
113
+
114
+ def cut(strip: Strip, load: str, pitch: float = 1.0, n: int = 4) -> dict:
115
+ """The cut-cell model (cutcell.py) of the strip: sub-cells from the
116
+ exact strip, the stress read at the surface near it."""
117
+ from ..cutcell import CutGrid
118
+
119
+ h = float(pitch)
120
+ lo3 = np.array([-strip.c, -strip.t / 2, 0.0])
121
+ hi3 = np.array([strip.L, strip.t / 2, W])
122
+ corners = strip.world(*np.array(np.meshgrid(*zip(lo3, hi3))).reshape(3, -1))
123
+ origin = (np.floor(corners.min(0) / h) - 1.0) * h
124
+ origin[2] = 0.0
125
+ dims = np.ceil((corners.max(0) - origin) / h).astype(int) + 1
126
+ dims[2] = int(round(W / h))
127
+ fd = dims * n
128
+ g = [origin[a] + (np.arange(fd[a]) + 0.5) * h / n for a in range(3)]
129
+ pts = np.stack(np.meshgrid(*g, indexing="ij"), -1).reshape(-1, 3)
130
+ fine = strip.inside(pts).reshape(fd)
131
+ cg = CutGrid(fine, n, origin, h, NU)
132
+ mesh = cg.mesh
133
+ s_node = strip.local(mesh.node_xyz)[0]
134
+ fixed = np.repeat(s_node < 0, 3)
135
+ s_cell = strip.local(mesh.origin + (mesh.elem_ijk + 0.5) * h)[0]
136
+ zone = s_cell > strip.L - ZONE
137
+ wz = np.where(zone, cg.fill, 0.0)
138
+ wn = np.zeros(mesh.n_nodes)
139
+ np.add.at(wn, mesh.enode.ravel(), np.repeat(wz / 8.0, 8))
140
+ wn /= wz.sum()
141
+ f = (F * wn[:, None] * strip.direction(load)[None]).ravel()
142
+ s_f = float((s_cell * wz).sum() / wz.sum())
143
+ st = Stiffness(mesh, NU, fixed)
144
+ k = cg.assemble(st, E)
145
+ u = np.zeros(mesh.n_dof)
146
+ free = ~fixed
147
+ u[free] = spsolve(k[free][:, free].tocsc(), f[free])
148
+ vm = von_mises(cg.nodal_stress(u, E))
149
+ lo, hi = strip.window
150
+ win = (s_node >= lo) & (s_node <= hi)
151
+ return {"peak": float(np.max(vm[win] / strip.beam(load, s_node[win], s_f))),
152
+ "displacement": float(f @ u) / F, "cells": int(mesh.n_elem), "cutCells": int(cg.cut.sum())}
153
+
154
+
155
+ # C3D20 node order on the doubled grid: corners, bottom edges, top edges, verticals
156
+ _HEX20 = [(0, 0, 0), (2, 0, 0), (2, 2, 0), (0, 2, 0), (0, 0, 2), (2, 0, 2), (2, 2, 2), (0, 2, 2),
157
+ (1, 0, 0), (2, 1, 0), (1, 2, 0), (0, 1, 0), (1, 0, 2), (2, 1, 2), (1, 2, 2), (0, 1, 2),
158
+ (0, 0, 1), (2, 0, 1), (2, 2, 1), (0, 2, 1)]
159
+
160
+
161
+ def calculix(strip: Strip, load: str, through: int = 4, along: float = 0.5) -> dict:
162
+ """CalculiX (ccx on PATH) on the exact strip, quadratic hexes."""
163
+ from ..crosscheck import _read_frd
164
+
165
+ exe = shutil.which("ccx")
166
+ if exe is None:
167
+ raise RuntimeError("CalculiX (ccx) is not on PATH (Debian: apt install calculix-ccx)")
168
+ ns = int(round((strip.L + strip.c) / along))
169
+ nt, nw = through, 3
170
+ gs = np.linspace(-strip.c, strip.L, 2 * ns + 1)
171
+ gy = np.linspace(-strip.t / 2, strip.t / 2, 2 * nt + 1)
172
+ gz = np.linspace(0.0, W, 2 * nw + 1)
173
+ shape = (len(gs), len(gy), len(gz))
174
+ elems, ezone = [], []
175
+ for i in range(ns):
176
+ for j in range(nt):
177
+ for k in range(nw):
178
+ elems.append([np.ravel_multi_index((2 * i + a, 2 * j + b, 2 * k + c), shape) for a, b, c in _HEX20])
179
+ ezone.append(0.5 * (gs[2 * i] + gs[2 * i + 2]) > strip.L - ZONE)
180
+ elems = np.array(elems)
181
+ used, conn = np.unique(elems, return_inverse=True)
182
+ conn = conn.reshape(elems.shape)
183
+ ii, jj, kk = np.unravel_index(used, shape)
184
+ s, y, z = gs[ii], gy[jj], gz[kk]
185
+ xyz = strip.world(s, y, z)
186
+ ezone = np.array(ezone)
187
+ bf = F / (ZONE * strip.t * W) * strip.direction(load)
188
+ s_f = strip.L - ZONE / 2
189
+ with tempfile.TemporaryDirectory() as tmp:
190
+ with open(os.path.join(tmp, "job.inp"), "w") as fh:
191
+ fh.write("*NODE,NSET=NALL\n")
192
+ np.savetxt(fh, np.column_stack([np.arange(1, len(xyz) + 1), xyz]), fmt="%d,%.12g,%.12g,%.12g")
193
+ fh.write("*ELEMENT,TYPE=C3D20,ELSET=PART\n")
194
+ rows = np.column_stack([np.arange(1, len(conn) + 1), conn + 1])
195
+ for r in rows: # 21 integers: CalculiX takes at most 16 per line
196
+ fh.write(",".join(map(str, r[:16])) + ",\n" + ",".join(map(str, r[16:])) + "\n")
197
+ fh.write("*ELSET,ELSET=EZONE\n")
198
+ np.savetxt(fh, np.nonzero(ezone)[0] + 1, fmt="%d")
199
+ fh.write("*NSET,NSET=CLAMP\n")
200
+ np.savetxt(fh, np.nonzero(s <= 1e-9)[0] + 1, fmt="%d")
201
+ fh.write(f"*MATERIAL,NAME=M\n*ELASTIC\n{E},{NU}\n*SOLID SECTION,ELSET=PART,MATERIAL=M\n")
202
+ fh.write("*BOUNDARY\nCLAMP,1,3\n*STEP\n*STATIC\n*DLOAD\n")
203
+ for c, lab in enumerate(("BX", "BY", "BZ")):
204
+ if abs(bf[c]) > 1e-15:
205
+ fh.write(f"EZONE,{lab},{bf[c]:.12g}\n")
206
+ fh.write("*NODE FILE\nU\n*EL FILE\nS\n*END STEP\n")
207
+ run = subprocess.run([exe, "job"], cwd=tmp, capture_output=True, text=True, timeout=1800,
208
+ env={**os.environ, "OMP_NUM_THREADS": "1"})
209
+ if run.returncode != 0 or not os.path.exists(os.path.join(tmp, "job.frd")):
210
+ raise RuntimeError(f"CalculiX failed: {(run.stdout + run.stderr)[-2000:]}")
211
+ u, vm, _sig = _read_frd(os.path.join(tmp, "job.frd"), len(xyz))[0]
212
+ lo, hi = strip.window
213
+ win = (s >= lo) & (s <= hi)
214
+ # load-point displacement: the body force's work over the end zone,
215
+ # sampled at the zone's nodes (same weights as the load)
216
+ zn = np.unique(conn[ezone].ravel())
217
+ d = float(np.mean(u[zn] @ strip.direction(load)))
218
+ return {"peak": float(np.max(vm[win] / strip.beam(load, s[win], s_f))),
219
+ "displacement": d, "elements": int(len(conn))}
220
+
221
+
222
+ def run(thick=(1, 2, 3, 5), angles=(0, 30, 45), loads=("bending", "tension"), elements=("cut", "im", "plain")):
223
+ rows = []
224
+ for load in loads:
225
+ for t in thick:
226
+ for th in angles:
227
+ strip = Strip(t, th)
228
+ ref = calculix(strip, load)
229
+ row = {"load": load, "t": t, "theta": th, "calculix": ref}
230
+ for el in elements:
231
+ v = cut(strip, load) if el == "cut" else voxel(strip, load, el)
232
+ v["ratio"] = v["peak"] / ref["peak"]
233
+ v["stiffness"] = ref["displacement"] / v["displacement"]
234
+ row[el] = v
235
+ rows.append(row)
236
+ print(f"{load:8s} t={t} theta={th:2d} calculix {ref['peak']:.3f} " + " ".join(
237
+ f"{el} {row[el]['ratio']:.3f} (stiff {row[el]['stiffness']:.3f})" for el in elements), flush=True)
238
+ return rows
239
+
240
+
241
+ def table(rows, elements=("cut", "im", "plain")) -> str:
242
+ head = "| load | cells thick | angle | " + " | ".join(
243
+ f"{el} peak / CalculiX | {el} stiffness / CalculiX" for el in elements) + " |"
244
+ out = [head, "|" + "---|" * (3 + 2 * len(elements))]
245
+ for r in rows:
246
+ out.append(f"| {r['load']} | {r['t']} | {r['theta']} | " + " | ".join(
247
+ f"{r[el]['ratio']:.2f} | {r[el]['stiffness']:.2f}" for el in elements) + " |")
248
+ return "\n".join(out)
249
+
250
+
251
+ def main(argv=None):
252
+ ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
253
+ ap.add_argument("--quick", action="store_true", help="1 and 3 cells, 0 and 45 degrees, bending only")
254
+ ap.add_argument("--out", help="write the rows as JSON here")
255
+ a = ap.parse_args(argv)
256
+ kw = dict(thick=(1, 3), angles=(0, 45), loads=("bending",)) if a.quick else {}
257
+ rows = run(**kw)
258
+ print()
259
+ print(table(rows))
260
+ if a.out:
261
+ with open(a.out, "w") as fh:
262
+ json.dump(rows, fh, indent=1)
263
+
264
+
265
+ if __name__ == "__main__":
266
+ main()
@@ -0,0 +1,380 @@
1
+ {
2
+ "name": "ge-bracket",
3
+ "description": "GE jet engine bracket challenge (GrabCAD 2013), approximate: the challenge's four load cases, Ti-6Al-4V, a 0.75 in pin through a two-lug clevis and four 0.375 in bolts, minimum wall 0.050 in, pass = peak von Mises under yield (131 ksi) in every case. The design envelope is a box about the original's overall size (178 x 107 x 63 mm), roomier than the original L-shaped envelope, so masses compare only roughly with the entries. The original machined bracket weighed 4 lb (1.81 kg); the winning entry removed about 80% of it (Carter et al. 2014).",
4
+ "designSpace": [
5
+ {
6
+ "type": "box",
7
+ "min": [
8
+ 0,
9
+ 0,
10
+ 0
11
+ ],
12
+ "max": [
13
+ 178.0,
14
+ 107.0,
15
+ 63.0
16
+ ]
17
+ }
18
+ ],
19
+ "preserve": [
20
+ {
21
+ "id": "lug-a",
22
+ "shape": {
23
+ "type": "cylinder",
24
+ "center": [
25
+ 74.7,
26
+ 53.5,
27
+ 50.0
28
+ ],
29
+ "axis": "x",
30
+ "length": 10.0,
31
+ "r": 14.525
32
+ }
33
+ },
34
+ {
35
+ "id": "lug-b",
36
+ "shape": {
37
+ "type": "cylinder",
38
+ "center": [
39
+ 103.3,
40
+ 53.5,
41
+ 50.0
42
+ ],
43
+ "axis": "x",
44
+ "length": 10.0,
45
+ "r": 14.525
46
+ }
47
+ },
48
+ {
49
+ "id": "pad-0",
50
+ "shape": {
51
+ "type": "cylinder",
52
+ "center": [
53
+ 14,
54
+ 14,
55
+ 4.0
56
+ ],
57
+ "axis": "z",
58
+ "length": 8.0,
59
+ "r": 9.085
60
+ }
61
+ },
62
+ {
63
+ "id": "pad-1",
64
+ "shape": {
65
+ "type": "cylinder",
66
+ "center": [
67
+ 164.0,
68
+ 14,
69
+ 4.0
70
+ ],
71
+ "axis": "z",
72
+ "length": 8.0,
73
+ "r": 9.085
74
+ }
75
+ },
76
+ {
77
+ "id": "pad-2",
78
+ "shape": {
79
+ "type": "cylinder",
80
+ "center": [
81
+ 14,
82
+ 93.0,
83
+ 4.0
84
+ ],
85
+ "axis": "z",
86
+ "length": 8.0,
87
+ "r": 9.085
88
+ }
89
+ },
90
+ {
91
+ "id": "pad-3",
92
+ "shape": {
93
+ "type": "cylinder",
94
+ "center": [
95
+ 164.0,
96
+ 93.0,
97
+ 4.0
98
+ ],
99
+ "axis": "z",
100
+ "length": 8.0,
101
+ "r": 9.085
102
+ }
103
+ }
104
+ ],
105
+ "keepOut": [
106
+ {
107
+ "id": "pin-a",
108
+ "shape": {
109
+ "type": "cylinder",
110
+ "center": [
111
+ 74.7,
112
+ 53.5,
113
+ 50.0
114
+ ],
115
+ "axis": "x",
116
+ "length": 10.2,
117
+ "r": 9.525
118
+ }
119
+ },
120
+ {
121
+ "id": "pin-b",
122
+ "shape": {
123
+ "type": "cylinder",
124
+ "center": [
125
+ 103.3,
126
+ 53.5,
127
+ 50.0
128
+ ],
129
+ "axis": "x",
130
+ "length": 10.2,
131
+ "r": 9.525
132
+ }
133
+ },
134
+ {
135
+ "id": "clevis-gap",
136
+ "shape": {
137
+ "type": "box",
138
+ "min": [
139
+ 79.7,
140
+ 33.5,
141
+ 30.0
142
+ ],
143
+ "max": [
144
+ 98.3,
145
+ 73.5,
146
+ 64.0
147
+ ]
148
+ },
149
+ "bore": false
150
+ },
151
+ {
152
+ "id": "bolt-0",
153
+ "shape": {
154
+ "type": "cylinder",
155
+ "center": [
156
+ 14,
157
+ 14,
158
+ 4.0
159
+ ],
160
+ "axis": "z",
161
+ "length": 8.2,
162
+ "r": 4.7625
163
+ }
164
+ },
165
+ {
166
+ "id": "bolt-1",
167
+ "shape": {
168
+ "type": "cylinder",
169
+ "center": [
170
+ 164.0,
171
+ 14,
172
+ 4.0
173
+ ],
174
+ "axis": "z",
175
+ "length": 8.2,
176
+ "r": 4.7625
177
+ }
178
+ },
179
+ {
180
+ "id": "bolt-2",
181
+ "shape": {
182
+ "type": "cylinder",
183
+ "center": [
184
+ 14,
185
+ 93.0,
186
+ 4.0
187
+ ],
188
+ "axis": "z",
189
+ "length": 8.2,
190
+ "r": 4.7625
191
+ }
192
+ },
193
+ {
194
+ "id": "bolt-3",
195
+ "shape": {
196
+ "type": "cylinder",
197
+ "center": [
198
+ 164.0,
199
+ 93.0,
200
+ 4.0
201
+ ],
202
+ "axis": "z",
203
+ "length": 8.2,
204
+ "r": 4.7625
205
+ }
206
+ },
207
+ {
208
+ "id": "nut-0",
209
+ "shape": {
210
+ "type": "cylinder",
211
+ "center": [
212
+ 14,
213
+ 14,
214
+ 36.0
215
+ ],
216
+ "axis": "z",
217
+ "length": 56.0,
218
+ "r": 8.085
219
+ },
220
+ "bore": false
221
+ },
222
+ {
223
+ "id": "nut-1",
224
+ "shape": {
225
+ "type": "cylinder",
226
+ "center": [
227
+ 164.0,
228
+ 14,
229
+ 36.0
230
+ ],
231
+ "axis": "z",
232
+ "length": 56.0,
233
+ "r": 8.085
234
+ },
235
+ "bore": false
236
+ },
237
+ {
238
+ "id": "nut-2",
239
+ "shape": {
240
+ "type": "cylinder",
241
+ "center": [
242
+ 14,
243
+ 93.0,
244
+ 36.0
245
+ ],
246
+ "axis": "z",
247
+ "length": 56.0,
248
+ "r": 8.085
249
+ },
250
+ "bore": false
251
+ },
252
+ {
253
+ "id": "nut-3",
254
+ "shape": {
255
+ "type": "cylinder",
256
+ "center": [
257
+ 164.0,
258
+ 93.0,
259
+ 36.0
260
+ ],
261
+ "axis": "z",
262
+ "length": 56.0,
263
+ "r": 8.085
264
+ },
265
+ "bore": false
266
+ }
267
+ ],
268
+ "supports": [
269
+ {
270
+ "region": "bolt-0"
271
+ },
272
+ {
273
+ "region": "bolt-1"
274
+ },
275
+ {
276
+ "region": "bolt-2"
277
+ },
278
+ {
279
+ "region": "bolt-3"
280
+ }
281
+ ],
282
+ "loadCases": [
283
+ {
284
+ "name": "vertical",
285
+ "loads": [
286
+ {
287
+ "region": "pin-a",
288
+ "force": [
289
+ 0,
290
+ 0,
291
+ 17793.0
292
+ ]
293
+ },
294
+ {
295
+ "region": "pin-b",
296
+ "force": [
297
+ 0,
298
+ 0,
299
+ 17793.0
300
+ ]
301
+ }
302
+ ]
303
+ },
304
+ {
305
+ "name": "horizontal",
306
+ "loads": [
307
+ {
308
+ "region": "pin-a",
309
+ "force": [
310
+ 0,
311
+ 18905.0,
312
+ 0
313
+ ]
314
+ },
315
+ {
316
+ "region": "pin-b",
317
+ "force": [
318
+ 0,
319
+ 18905.0,
320
+ 0
321
+ ]
322
+ }
323
+ ]
324
+ },
325
+ {
326
+ "name": "angled-42",
327
+ "loads": [
328
+ {
329
+ "region": "pin-a",
330
+ "force": [
331
+ 0,
332
+ 14138.1,
333
+ 15701.9
334
+ ]
335
+ },
336
+ {
337
+ "region": "pin-b",
338
+ "force": [
339
+ 0,
340
+ 14138.1,
341
+ 15701.9
342
+ ]
343
+ }
344
+ ]
345
+ },
346
+ {
347
+ "name": "torsion",
348
+ "loads": [
349
+ {
350
+ "region": "pin-a",
351
+ "force": [
352
+ 0,
353
+ 0,
354
+ 19770
355
+ ]
356
+ },
357
+ {
358
+ "region": "pin-b",
359
+ "force": [
360
+ 0,
361
+ 0,
362
+ -19770
363
+ ]
364
+ }
365
+ ]
366
+ }
367
+ ],
368
+ "material": "ti64",
369
+ "goal": {
370
+ "safetyFactor": 1.0
371
+ },
372
+ "resolution": {
373
+ "maxElements": 80000
374
+ },
375
+ "expect": {
376
+ "bodies": 1,
377
+ "minSafetyFactor": 1.0,
378
+ "maxMass": 362
379
+ }
380
+ }