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/fieldtorch.py ADDED
@@ -0,0 +1,464 @@
1
+ """
2
+ Field graphs evaluated with torch: on a GPU (CUDA or Metal) when there is
3
+ one, the same code on the CPU otherwise.
4
+
5
+ Meshing a field graph is almost all field evaluation (marching cubes is a
6
+ few percent): every op of every node over millions of sample points, a
7
+ dozen elementwise passes each. That is what a GPU does best, and the
8
+ editor already shows it, raymarching the same graphs in a fragment shader.
9
+ This module runs FieldGraph's ops as torch tensor ops instead of numpy:
10
+ constant, coordinate, the analytic shapes (box, cylinder, tube, sphere and
11
+ their unions), remap (curves and tables), expr, the smooth booleans, shell,
12
+ offset, transform, TPMS lattices (cartesian, with the graded cell's warp
13
+ map sampled trilinearly on the device) and sampled fields on a regular
14
+ grid (a study's result, an imported grid, an image).
15
+
16
+ What it does not port runs as before: such a node (an exact distance to a
17
+ mesh, a distance to points, a cylindrical lattice, a cross-faded grading,
18
+ an offset or mesh shape) is evaluated by the numpy graph on the CPU, with
19
+ everything under it, and its values copied over. So every graph runs, and
20
+ the answer is the numpy one: in float64 to ~1e-7 (sampled fields are
21
+ stored as float32, which numpy interpolates in float32); in float32, the
22
+ default on a GPU, where float64 is slow or missing, to ~5e-4 mm on the
23
+ graded arm link (180 mm across, the lattice's phase is where the digits
24
+ go), a fraction of a percent of any pitch it is meshed at.
25
+
26
+ Selection: LOADPATH_FIELD_DEVICE=auto (default: CUDA, then Metal; numpy
27
+ when there is neither), cuda, mps, torch (torch on the CPU) or numpy;
28
+ LOADPATH_FIELD_DTYPE=float32|float64 overrides the precision (float64 on
29
+ the CPU, float32 on a GPU by default).
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import ast
34
+ import math
35
+ import os
36
+
37
+ import numpy as np
38
+
39
+ __all__ = ["field_device", "TorchField"]
40
+
41
+
42
+ def field_device(name: str | None = None):
43
+ """The torch device field graphs evaluate on, or None for numpy."""
44
+ name = (name if name is not None else os.environ.get("LOADPATH_FIELD_DEVICE", "auto")).strip().lower()
45
+ if name in ("", "cpu", "none", "off", "numpy"):
46
+ return None
47
+ try:
48
+ import torch
49
+ except ImportError:
50
+ if name == "auto":
51
+ return None
52
+ raise RuntimeError(f"LOADPATH_FIELD_DEVICE={name} needs torch (pip install 'loadpath[gpu]')")
53
+ if name == "auto":
54
+ if torch.cuda.is_available():
55
+ return torch.device("cuda")
56
+ if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available():
57
+ return torch.device("mps")
58
+ return None
59
+ if name == "torch":
60
+ return torch.device("cpu")
61
+ return torch.device(name)
62
+
63
+
64
+ class TorchField:
65
+ """`TorchField(graph, device)(P)`: graph.evaluate(P, name) with torch.
66
+ P and the result are numpy (float64); in between, tensors on `device`."""
67
+
68
+ def __init__(self, graph, device=None, dtype=None):
69
+ import torch
70
+
71
+ self.torch = torch
72
+ self.g = graph
73
+ self.device = torch.device(device) if device is not None else torch.device("cpu")
74
+ if dtype is None:
75
+ want = os.environ.get("LOADPATH_FIELD_DTYPE", "")
76
+ dtype = {"float32": torch.float32, "float64": torch.float64}.get(
77
+ want, torch.float64 if self.device.type == "cpu" else torch.float32)
78
+ self.dtype = dtype
79
+ self._exprs, self._grids = {}, {}
80
+ self.fallbacks = set() # nodes that ran on numpy, for the curious
81
+ # On a GPU the mesher calls serially in batches this big (enough
82
+ # to amortize kernel launches; a deep graph's ~40 intermediates
83
+ # still fit). On the CPU, None: the mesher's thread pool beats
84
+ # torch's own threads on one big batch (10 s vs 19 s, arm link).
85
+ b = int(os.environ.get("LOADPATH_FIELD_BATCH", 0))
86
+ self.batch = b or (None if self.device.type == "cpu" else 4_000_000)
87
+
88
+ # ---- entry ----
89
+
90
+ def __call__(self, P, name=None):
91
+ torch = self.torch
92
+ P = np.asarray(P, float).reshape(-1, 3)
93
+ Pt = torch.as_tensor(P, dtype=self.dtype, device=self.device)
94
+ memo = {"np": (P, {}), "P": Pt}
95
+ out = self._eval(name or self.g.output, Pt, memo)
96
+ if not torch.is_tensor(out):
97
+ return np.full(len(P), float(out))
98
+ return np.broadcast_to(out.to(torch.float64).cpu().numpy(), (len(P),)).astype(float)
99
+
100
+ # ---- helpers ----
101
+
102
+ def _t(self, x):
103
+ torch = self.torch
104
+ if torch.is_tensor(x):
105
+ return x
106
+ return torch.as_tensor(np.asarray(x, float), dtype=self.dtype, device=self.device)
107
+
108
+ def _val(self, r, P, memo):
109
+ if isinstance(r, float):
110
+ return r
111
+ return self._eval(r, P, memo)
112
+
113
+ def _eval(self, name, P, memo):
114
+ key = (name, id(P))
115
+ if key in memo:
116
+ return memo[key][1]
117
+ try:
118
+ out = self._compute(self.g.nodes[name], P, memo)
119
+ except _NumpyOnly:
120
+ out = self._numpy(name, P, memo)
121
+ memo[key] = (P, out)
122
+ return out
123
+
124
+ def _numpy(self, name, P, memo):
125
+ """The node (and what it uses) by the numpy graph, copied over."""
126
+ self.fallbacks.add(name)
127
+ if P is memo["P"]:
128
+ Pn, nmemo = memo["np"]
129
+ else: # a transformed frame: its own points
130
+ Pn, nmemo = P.to(self.torch.float64).cpu().numpy(), {}
131
+ out = self.g._eval(name, Pn, nmemo)
132
+ return out if isinstance(out, float) else self._t(np.asarray(out, float))
133
+
134
+ def _interp(self, x, xs, ys):
135
+ """np.interp on tensors: piecewise linear, clamped at the ends."""
136
+ torch = self.torch
137
+ xs, ys = self._t(xs).contiguous(), self._t(ys)
138
+ x = self._t(x)
139
+ i = torch.clamp(torch.searchsorted(xs, x.contiguous()), 1, len(xs) - 1)
140
+ x0, x1, y0, y1 = xs[i - 1], xs[i], ys[i - 1], ys[i]
141
+ t = torch.clamp((x - x0) / (x1 - x0), 0.0, 1.0)
142
+ return y0 + (y1 - y0) * t
143
+
144
+ def _trilinear(self, key, grid, idx):
145
+ """map_coordinates(grid, idx, order=1, mode='nearest') on the device:
146
+ `idx` (N, 3) fractional grid indices."""
147
+ torch = self.torch
148
+ if key not in self._grids:
149
+ G = self._t(np.asarray(grid, float))
150
+ shape = list(G.shape)
151
+ self._grids[key] = (G.reshape(-1), torch.as_tensor(shape, device=self.device),
152
+ (shape[1] * shape[2], shape[2]))
153
+ G, n, (sx, sy) = self._grids[key]
154
+ i = torch.minimum(torch.clamp(idx, min=0.0), (n - 1).to(idx.dtype))
155
+ i0 = torch.minimum(torch.floor(i).long(), torch.clamp(n - 2, min=0))
156
+ f = i - i0.to(i.dtype)
157
+ step = torch.minimum(i0 + 1, n - 1) - i0 # 0 on a one-sample axis
158
+ base = i0[:, 0] * sx + i0[:, 1] * sy + i0[:, 2]
159
+ dx, dy, dz = step[:, 0] * sx, step[:, 1] * sy, step[:, 2]
160
+ fx, fy, fz = f[:, 0], f[:, 1], f[:, 2]
161
+ c00 = torch.lerp(G[base], G[base + dz], fz)
162
+ c01 = torch.lerp(G[base + dy], G[base + dy + dz], fz)
163
+ c10 = torch.lerp(G[base + dx], G[base + dx + dz], fz)
164
+ c11 = torch.lerp(G[base + dx + dy], G[base + dx + dy + dz], fz)
165
+ return torch.lerp(torch.lerp(c00, c01, fy), torch.lerp(c10, c11, fy), fx)
166
+
167
+ def _smin(self, a, b, k):
168
+ torch = self.torch
169
+ a, b = self._t(a), self._t(b)
170
+ if isinstance(k, float) and k <= 1e-9:
171
+ return torch.minimum(a, b)
172
+ k = self._t(k)
173
+ hard = k <= 1e-9
174
+ kk = torch.where(hard, torch.ones_like(k), k)
175
+ h = torch.clamp(0.5 + 0.5 * (b - a) / kk, 0.0, 1.0)
176
+ s = b * (1 - h) + a * h - kk * h * (1 - h)
177
+ return torch.where(hard, torch.minimum(a, b), s)
178
+
179
+ # ---- ops ----
180
+
181
+ def _compute(self, nd, P, memo):
182
+ torch = self.torch
183
+ s, r, op = nd.spec, nd.refs, nd.op
184
+ v = lambda key, default=None: self._val(r[key], P, memo) if key in r else default # noqa: E731
185
+ if op == "constant":
186
+ return float(s["value"])
187
+ if op == "coordinate":
188
+ ax = s.get("axis", "z")
189
+ if isinstance(ax, str):
190
+ return P[:, "xyz".index(ax)]
191
+ a = np.asarray(ax, float)
192
+ a = a / np.linalg.norm(a)
193
+ return (P - self._t(s.get("origin", (0, 0, 0)))) @ self._t(a)
194
+ if op == "shape":
195
+ from .shapes import shape_from_spec
196
+
197
+ if s["shape"].get("type") == "mesh":
198
+ raise _NumpyOnly
199
+ shp = self.g._resource(nd, lambda: shape_from_spec(s["shape"], self.g.base_dir))
200
+ return self._shape(shp, P)
201
+ if op in ("result", "grid", "image"):
202
+ smp = self.g._resource(nd, lambda: self.g._samples(nd))
203
+ if smp.tree is not None:
204
+ raise _NumpyOnly
205
+ idx = (P - self._t(smp.origin)) / self._t(smp.pitch)
206
+ return self._trilinear(("samples", nd.name, id(smp)), smp.grid, idx)
207
+ if op == "remap":
208
+ return self._remap(v("input"), s, nd.name)
209
+ if op == "expr":
210
+ fn = self._exprs.get(nd.name)
211
+ if fn is None:
212
+ from .fields import _compile_expr
213
+
214
+ fn = self._exprs[nd.name] = _compile_expr(s["expr"], self._lib())[1]
215
+ env = {"x": P[:, 0], "y": P[:, 1], "z": P[:, 2]}
216
+ for k, ref in r["vars"].items():
217
+ env[k] = self._val(ref, P, memo)
218
+ out = fn(env)
219
+ return out if torch.is_tensor(out) else float(out)
220
+ if op in ("union", "intersect", "subtract"):
221
+ vals = [self._val(x, P, memo) for x in r["inputs"]]
222
+ k = v("blend", 0.0)
223
+ out = self._t(vals[0])
224
+ for b in vals[1:]:
225
+ if op == "union":
226
+ out = self._smin(out, b, k)
227
+ elif op == "intersect":
228
+ out = -self._smin(-out, -self._t(b), k)
229
+ else:
230
+ out = -self._smin(-out, b, k)
231
+ return out
232
+ if op == "shell":
233
+ f, t = self._t(v("input")), v("thickness")
234
+ side = s.get("side", "inside")
235
+ if side == "inside":
236
+ return torch.maximum(f, -(f + t))
237
+ if side == "outside":
238
+ return torch.maximum(-f, f - t)
239
+ if side == "center":
240
+ return torch.abs(f) - 0.5 * t
241
+ raise _NumpyOnly
242
+ if op == "offset":
243
+ return v("input") - v("distance")
244
+ if op == "lattice":
245
+ return self._lattice(nd, P, memo)
246
+ if op == "transform":
247
+ return self._transform(nd, P, memo)
248
+ if op == "distance":
249
+ return self._distance(nd, P, memo)
250
+ raise _NumpyOnly # distance to points or a shape, mesh: exact geometry stays on the CPU
251
+
252
+ def _distance(self, nd, P, memo):
253
+ from .shapes import shape_from_spec
254
+
255
+ torch = self.torch
256
+ s, r = nd.spec, nd.refs
257
+ if "input" in r:
258
+ f = self._t(self._val(r["input"], P, memo))
259
+ return f if s.get("signed") else torch.abs(f)
260
+ if "segment" in s:
261
+ a, b = (np.asarray(x, float) for x in s["segment"])
262
+ ab = b - a
263
+ t = torch.clamp(((P - self._t(a)) @ self._t(ab)) / max(float(ab @ ab), 1e-300), 0.0, 1.0)
264
+ return torch.linalg.norm(P - (self._t(a) + t[:, None] * self._t(ab)), dim=1)
265
+ if "plane" in s:
266
+ n = np.asarray(s["plane"]["normal"], float)
267
+ return torch.abs((P - self._t(s["plane"]["point"])) @ self._t(n / np.linalg.norm(n)))
268
+ if "shape" in s:
269
+ shp = self.g._resource(nd, lambda: shape_from_spec(s["shape"], self.g.base_dir))
270
+ return torch.abs(self._shape(shp, P))
271
+ raise _NumpyOnly # points (a k-d tree)
272
+
273
+ def _shape(self, shp, P):
274
+ from .shapes import Box, Cylinder, Sphere, Union
275
+
276
+ torch = self.torch
277
+ if isinstance(shp, Box):
278
+ c, h = self._t(0.5 * (shp.lo + shp.hi)), self._t(0.5 * (shp.hi - shp.lo))
279
+ q = torch.abs(P - c) - h
280
+ return torch.linalg.norm(torch.clamp(q, min=0.0), dim=-1) + torch.clamp(q.max(dim=-1).values, max=0.0)
281
+ if isinstance(shp, Cylinder):
282
+ d = P - self._t(shp.p0)
283
+ ax = self._t(shp.axis)
284
+ t = d @ ax
285
+ radial = torch.linalg.norm(d - t[:, None] * ax, dim=-1)
286
+ half = 0.5 * shp.length
287
+ if shp.r_inner > 0:
288
+ rad_q = torch.abs(radial - 0.5 * (shp.r + shp.r_inner)) - 0.5 * (shp.r - shp.r_inner)
289
+ else:
290
+ rad_q = radial - shp.r
291
+ q = torch.stack([rad_q, torch.abs(t - half) - half], dim=-1)
292
+ return torch.linalg.norm(torch.clamp(q, min=0.0), dim=-1) + torch.clamp(q.max(dim=-1).values, max=0.0)
293
+ if isinstance(shp, Sphere):
294
+ return torch.linalg.norm(P - self._t(shp.center), dim=-1) - shp.r
295
+ if isinstance(shp, Union):
296
+ out = None
297
+ for x in shp.shapes:
298
+ d = self._shape(x, P)
299
+ out = d if out is None else torch.minimum(out, d)
300
+ return out
301
+ raise _NumpyOnly
302
+
303
+ def _remap(self, x, s, name):
304
+ torch = self.torch
305
+ x = self._t(x)
306
+ if len(s.get("from", ())) > 2 or len(s.get("to", ())) > 2:
307
+ return self._interp(x, np.asarray(s["from"], float), np.asarray(s["to"], float))
308
+ a, b = (float(u) for u in s.get("from", (0.0, 1.0)))
309
+ c, d = (float(u) for u in s.get("to", (0.0, 1.0)))
310
+ if a == b:
311
+ raise _NumpyOnly # the numpy path names the mistake
312
+ u = (x - a) / (b - a)
313
+ if s.get("clamp", True):
314
+ u = torch.clamp(u, 0.0, 1.0)
315
+ curve = s.get("curve", "linear")
316
+ if isinstance(curve, (list, tuple)):
317
+ pts = np.asarray(curve, float)
318
+ return c + (d - c) * self._interp(torch.clamp(u, float(pts[0, 0]), float(pts[-1, 0])), pts[:, 0],
319
+ pts[:, 1])
320
+ if curve == "smoothstep":
321
+ uc = torch.clamp(u, 0.0, 1.0)
322
+ u = uc * uc * (3 - 2 * uc)
323
+ elif curve == "smootherstep":
324
+ uc = torch.clamp(u, 0.0, 1.0)
325
+ u = uc ** 3 * (uc * (6 * uc - 15) + 10)
326
+ elif curve == "sqrt":
327
+ u = torch.sqrt(torch.clamp(u, min=0.0))
328
+ elif curve == "square":
329
+ u = u * u
330
+ elif curve != "linear":
331
+ raise _NumpyOnly
332
+ return c + (d - c) * u
333
+
334
+ def _tpms(self, kind, Q):
335
+ """lattice.tpms on tensors (period 1 in Q's units)."""
336
+ torch = self.torch
337
+ k = 2.0 * math.pi
338
+ x, y, z = (k * Q[:, i] for i in range(3))
339
+ sx, cx, sy, cy, sz, cz = torch.sin(x), torch.cos(x), torch.sin(y), torch.cos(y), torch.sin(z), torch.cos(z)
340
+ if kind == "gyroid":
341
+ f = sx * cy + sy * cz + sz * cx
342
+ g = (cx * cy - sz * sx, cy * cz - sx * sy, cz * cx - sy * sz)
343
+ elif kind == "diamond":
344
+ f = sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz
345
+ g = (cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz,
346
+ sx * cy * sz - sx * sy * cz + cx * cy * cz - cx * sy * sz,
347
+ sx * sy * cz - sx * cy * sz - cx * sy * sz + cx * cy * cz)
348
+ else: # schwarz-p
349
+ f = cx + cy + cz
350
+ g = (-sx, -sy, -sz)
351
+ gn = torch.sqrt(g[0] * g[0] + g[1] * g[1] + g[2] * g[2]) * k
352
+ return f / torch.clamp(gn, min=0.3 * k)
353
+
354
+ def _lattice(self, nd, P, memo):
355
+ from .fields import _TPMS_KIND
356
+
357
+ torch = self.torch
358
+ s, r = nd.spec, nd.refs
359
+ kind = _TPMS_KIND.get(s.get("type", "gyroid"))
360
+ if kind is None or s.get("frame") or "aspect" in s or "orient" in s:
361
+ raise _NumpyOnly
362
+ cell = self._val(r["cell"], P, memo) if "cell" in r else 8.0
363
+ Q = P - self._t(s["origin"]) if "origin" in s else P
364
+ if isinstance(r.get("cell"), str):
365
+ if s.get("grading", "warp") != "warp":
366
+ raise _NumpyOnly
367
+ warp = self.g._resource(nd, lambda: self.g._warp(nd))
368
+ cell = self._t(cell)
369
+ idx = (Q - self._t(warp.lo)) / warp.h
370
+ n = self._t(np.asarray(warp.dims, float) - 1)
371
+ cl = torch.minimum(torch.clamp(idx, min=0.0), n)
372
+ U = torch.stack([self._trilinear(("warp", nd.name, id(warp), i), u, cl) for i, u in enumerate(warp.u)],
373
+ dim=1)
374
+ U = U + (idx - cl) * warp.h / cell[:, None]
375
+ d = self._tpms(kind, U) * cell
376
+ else:
377
+ c = float(cell) if not torch.is_tensor(cell) else None
378
+ if c is None:
379
+ raise _NumpyOnly
380
+ d = self._tpms(kind, Q / c) * c
381
+ if "offset" in r:
382
+ d = d - self._val(r["offset"], P, memo)
383
+ if "thickness" in r:
384
+ t = self._val(r["thickness"], P, memo)
385
+ elif "density" in r:
386
+ from .lattice import _density_table
387
+
388
+ rho = torch.clamp(self._t(self._val(r["density"], P, memo)), 0.05, 0.95)
389
+ tt, rr = _density_table(kind)
390
+ t = self._interp(rho, rr, tt) * self._t(cell)
391
+ else:
392
+ t = 1.0
393
+ form = s.get("form", "sheet")
394
+ if form == "sheet":
395
+ return torch.abs(d) - 0.5 * t
396
+ if form == "network":
397
+ return d - 0.5 * t
398
+ raise _NumpyOnly
399
+
400
+ def _transform(self, nd, P, memo):
401
+ s = nd.spec
402
+ Q = P
403
+ if "translate" in s:
404
+ Q = Q - self._t(s["translate"])
405
+ if "rotate" in s:
406
+ rot = s["rotate"]
407
+ ax = rot.get("axis", "z")
408
+ a = np.asarray({"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}[ax] if isinstance(ax, str) else ax,
409
+ float)
410
+ a = a / np.linalg.norm(a)
411
+ c = self._t(rot.get("center", (0, 0, 0)))
412
+ th = -math.radians(float(rot["degrees"]))
413
+ K = np.array([[0, -a[2], a[1]], [a[2], 0, -a[0]], [-a[1], a[0], 0]])
414
+ R = np.eye(3) + math.sin(th) * K + (1 - math.cos(th)) * K @ K
415
+ Q = (Q - c) @ self._t(R.T) + c
416
+ scale = float(s.get("scale", 1.0))
417
+ if scale != 1.0:
418
+ Q = Q / scale
419
+ return self._val(nd.refs["input"], Q, memo) * scale
420
+
421
+ def _lib(self):
422
+ """The expr functions on tensors (fields._compile_expr's `lib`)."""
423
+ torch = self.torch
424
+ T = self._t
425
+
426
+ def red(fn, args):
427
+ out = T(args[0])
428
+ for a in args[1:]:
429
+ out = fn(out, T(a))
430
+ return out
431
+
432
+ def smoothstep(e0, e1, x):
433
+ e0, e1, x = T(e0), T(e1), T(x)
434
+ d = torch.where(e1 == e0, torch.full_like(e1, 1e-12), e1 - e0)
435
+ u = torch.clamp((x - e0) / d, 0.0, 1.0)
436
+ return u * u * (3.0 - 2.0 * u)
437
+
438
+ tiny = 1e-300 if self.dtype == torch.float64 else 1e-30
439
+ funcs = {
440
+ "abs": lambda a: torch.abs(T(a)), "sqrt": lambda a: torch.sqrt(torch.clamp(T(a), min=0.0)),
441
+ "exp": lambda a: torch.exp(T(a)), "log": lambda a: torch.log(torch.clamp(T(a), min=tiny)),
442
+ "sin": lambda a: torch.sin(T(a)), "cos": lambda a: torch.cos(T(a)), "tan": lambda a: torch.tan(T(a)),
443
+ "floor": lambda a: torch.floor(T(a)), "ceil": lambda a: torch.ceil(T(a)),
444
+ "sign": lambda a: torch.sign(T(a)),
445
+ "min": lambda *a: red(torch.minimum, a), "max": lambda *a: red(torch.maximum, a),
446
+ "clamp": lambda x, lo, hi: torch.minimum(torch.maximum(T(x), T(lo)), T(hi)),
447
+ "lerp": lambda a, b, t: a + (b - a) * t,
448
+ "smoothstep": smoothstep,
449
+ "step": lambda edge, x: (T(x) >= T(edge)).to(self.dtype),
450
+ "where": lambda c, a, b: torch.where(T(c) > 0, T(a), T(b)),
451
+ }
452
+ binops = {ast.Add: lambda a, b: a + b, ast.Sub: lambda a, b: a - b, ast.Mult: lambda a, b: a * b,
453
+ ast.Div: lambda a, b: a / b, ast.Pow: lambda a, b: T(a) ** b if torch.is_tensor(a) or
454
+ torch.is_tensor(b) else a ** b,
455
+ ast.Mod: lambda a, b: torch.remainder(T(a), T(b))}
456
+ cmpops = {ast.Lt: lambda a, b: T(a) < T(b), ast.LtE: lambda a, b: T(a) <= T(b),
457
+ ast.Gt: lambda a, b: T(a) > T(b), ast.GtE: lambda a, b: T(a) >= T(b)}
458
+ return {"funcs": funcs, "binops": binops, "cmpops": cmpops,
459
+ "where": lambda c, a, b: torch.where(T(c) > 0, T(a), T(b)),
460
+ "float": lambda a: a.to(self.dtype)}
461
+
462
+
463
+ class _NumpyOnly(Exception):
464
+ """This node runs on the numpy graph (see the module docstring)."""