loadpath 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
loadpath/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)."""
|