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/fieldjit.py
ADDED
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Field graphs compiled to one numba kernel: every node of the graph, point by
|
|
3
|
+
point, in a single pass.
|
|
4
|
+
|
|
5
|
+
The numpy graph (FieldGraph._compute) runs each node as a handful of
|
|
6
|
+
whole-array passes, so a deep graph (the graded arm link: ~25 nodes, a
|
|
7
|
+
dozen elementwise ops each) streams hundreds of temporaries of a million
|
|
8
|
+
doubles through memory per chunk, and peval's four threads mostly wait on
|
|
9
|
+
memory bandwidth (they scaled ~2.4x on 4 cores). Here the graph becomes
|
|
10
|
+
Python source for one loop over the points, each node a few scalar lines,
|
|
11
|
+
compiled by numba (nogil, so peval's threads still split the points). The
|
|
12
|
+
source depends only on the graph's structure; every number (box corners,
|
|
13
|
+
remap ranges, blend radii) is read from a parameter array, so a tune that
|
|
14
|
+
moves parameters reuses the compiled kernel, and kernels are cached on disk
|
|
15
|
+
across processes (~/.cache/loadpath/fieldjit, LOADPATH_FIELDJIT_CACHE).
|
|
16
|
+
|
|
17
|
+
Compiled: constant, coordinate, the analytic shapes (box, cylinder, tube,
|
|
18
|
+
sphere, their unions, intersections, differences and shells), sampled
|
|
19
|
+
fields on a regular grid (a study's result, grid, image), remap (curves,
|
|
20
|
+
drawn curves, tables), expr, the smooth booleans, shell, offset, distance
|
|
21
|
+
(to a field, segment, plane or shape), transform, and TPMS lattices in the
|
|
22
|
+
cartesian frame (constant cell or the graded cell's warp, orient). Anything
|
|
23
|
+
else (an exact mesh distance, distance to points, a strut, conformal or
|
|
24
|
+
zoned lattice, a cross-faded grading, density-driven walls) stays numpy:
|
|
25
|
+
the node and what is under it are evaluated by the numpy graph first and
|
|
26
|
+
read by the kernel as an input array.
|
|
27
|
+
|
|
28
|
+
Same answer as numpy: every op is written in numpy's order of operations
|
|
29
|
+
(no fast-math, no fused multiply-add), the trilinear lookups as
|
|
30
|
+
scipy.ndimage.map_coordinates(order=1) sums them, so the values match bit
|
|
31
|
+
for bit (the arm link meshes to the same vertices, in double precision)
|
|
32
|
+
except where numpy hands a 3-vector dot product to BLAS (a cylinder on a
|
|
33
|
+
slanted axis, an orient or rotate, a coordinate along a vector) or uses its
|
|
34
|
+
own SIMD pow / exp / log (smootherstep, expr): there to ~1e-15 relative.
|
|
35
|
+
In single-precision mode (lattice.single_precision, which the mesher's grid
|
|
36
|
+
sampling and first relax rounds use) the lattice trig is libm's float32
|
|
37
|
+
instead of numpy's SIMD float32, which agree to ~1 ulp of float32 (~2e-6
|
|
38
|
+
mm on a 7 mm cell, the noise that mode accepts anyway): the meshes come out
|
|
39
|
+
with the same triangles, vertices apart by up to ~1e-3 mm where the relax
|
|
40
|
+
carries that noise along.
|
|
41
|
+
|
|
42
|
+
LOADPATH_FIELDJIT=0 (or numpy) turns it off; without numba it is off.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import ast
|
|
47
|
+
import hashlib
|
|
48
|
+
import importlib.util
|
|
49
|
+
import math
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
import threading
|
|
53
|
+
|
|
54
|
+
import numpy as np
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
import numba as nb
|
|
58
|
+
|
|
59
|
+
from . import meshops as _meshops # noqa: F401 - caps numba's threads (LOADPATH_THREADS)
|
|
60
|
+
except ImportError: # pragma: no cover
|
|
61
|
+
nb = None
|
|
62
|
+
|
|
63
|
+
__all__ = ["available", "compile_graph", "CompiledField"]
|
|
64
|
+
|
|
65
|
+
_KINDS = {"gyroid": 0, "diamond": 1, "schwarz-p": 2}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def available():
|
|
69
|
+
if nb is None:
|
|
70
|
+
return False
|
|
71
|
+
return os.environ.get("LOADPATH_FIELDJIT", "1").strip().lower() not in ("0", "numpy", "off", "false", "no")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---- scalar helpers, numpy's semantics -----------------------------------------------
|
|
75
|
+
|
|
76
|
+
if nb is not None:
|
|
77
|
+
_jit = nb.njit(cache=True, nogil=True, error_model="numpy")
|
|
78
|
+
|
|
79
|
+
@_jit
|
|
80
|
+
def _mx(a, b):
|
|
81
|
+
# np.maximum: NaN wins
|
|
82
|
+
if a != a:
|
|
83
|
+
return a
|
|
84
|
+
return a if a >= b else b
|
|
85
|
+
|
|
86
|
+
@_jit
|
|
87
|
+
def _mn(a, b):
|
|
88
|
+
if a != a:
|
|
89
|
+
return a
|
|
90
|
+
return a if a <= b else b
|
|
91
|
+
|
|
92
|
+
@_jit
|
|
93
|
+
def _clip(x, lo, hi):
|
|
94
|
+
return _mn(_mx(x, lo), hi)
|
|
95
|
+
|
|
96
|
+
@_jit
|
|
97
|
+
def _smin(a, b, k):
|
|
98
|
+
# fields._smin, one radius per point (0 = hard)
|
|
99
|
+
if k <= 1e-9:
|
|
100
|
+
return _mn(a, b)
|
|
101
|
+
h = _clip(0.5 + 0.5 * (b - a) / k, 0.0, 1.0)
|
|
102
|
+
return b * (1 - h) + a * h - k * h * (1 - h)
|
|
103
|
+
|
|
104
|
+
@_jit
|
|
105
|
+
def _sign(a):
|
|
106
|
+
if a > 0:
|
|
107
|
+
return 1.0
|
|
108
|
+
if a < 0:
|
|
109
|
+
return -1.0
|
|
110
|
+
return a # 0, -0 and NaN as np.sign
|
|
111
|
+
|
|
112
|
+
@_jit
|
|
113
|
+
def _interp(x, xs, ys, i0, n):
|
|
114
|
+
# np.interp(x, xs[i0:i0+n], ys[i0:i0+n]) (numpy's compiled_base.c)
|
|
115
|
+
if x != x:
|
|
116
|
+
return x
|
|
117
|
+
if x < xs[i0]:
|
|
118
|
+
return ys[i0]
|
|
119
|
+
last = i0 + n - 1
|
|
120
|
+
if x > xs[last]:
|
|
121
|
+
return ys[last]
|
|
122
|
+
if x == xs[last]:
|
|
123
|
+
return ys[last]
|
|
124
|
+
lo = i0
|
|
125
|
+
hi = last
|
|
126
|
+
while hi - lo > 1:
|
|
127
|
+
mid = (lo + hi) // 2
|
|
128
|
+
if xs[mid] <= x:
|
|
129
|
+
lo = mid
|
|
130
|
+
else:
|
|
131
|
+
hi = mid
|
|
132
|
+
j = lo
|
|
133
|
+
if x == xs[j]:
|
|
134
|
+
return ys[j]
|
|
135
|
+
slope = (ys[j + 1] - ys[j]) / (xs[j + 1] - xs[j])
|
|
136
|
+
r = slope * (x - xs[j]) + ys[j]
|
|
137
|
+
if r != r:
|
|
138
|
+
r = slope * (x - xs[j + 1]) + ys[j + 1]
|
|
139
|
+
if r != r and ys[j] == ys[j + 1]:
|
|
140
|
+
r = ys[j]
|
|
141
|
+
return r
|
|
142
|
+
|
|
143
|
+
@_jit
|
|
144
|
+
def _tri(G, x, y, z):
|
|
145
|
+
# map_coordinates(G, idx, order=1, mode="nearest") in float64: the
|
|
146
|
+
# eight corners summed a-outer, c-inner, each value * wa * wb * wc
|
|
147
|
+
n0, n1, n2 = G.shape
|
|
148
|
+
fx = math.floor(x)
|
|
149
|
+
fy = math.floor(y)
|
|
150
|
+
fz = math.floor(z)
|
|
151
|
+
tx = x - fx
|
|
152
|
+
ty = y - fy
|
|
153
|
+
tz = z - fz
|
|
154
|
+
i = int(fx)
|
|
155
|
+
j = int(fy)
|
|
156
|
+
k = int(fz)
|
|
157
|
+
t = 0.0
|
|
158
|
+
for a in range(2):
|
|
159
|
+
ia = min(max(i + a, 0), n0 - 1)
|
|
160
|
+
wa = 1.0 - tx if a == 0 else tx
|
|
161
|
+
for b in range(2):
|
|
162
|
+
jb = min(max(j + b, 0), n1 - 1)
|
|
163
|
+
wb = 1.0 - ty if b == 0 else ty
|
|
164
|
+
for c in range(2):
|
|
165
|
+
kc = min(max(k + c, 0), n2 - 1)
|
|
166
|
+
wc = 1.0 - tz if c == 0 else tz
|
|
167
|
+
t += np.float64(G[ia, jb, kc]) * wa * wb * wc
|
|
168
|
+
return t
|
|
169
|
+
|
|
170
|
+
@_jit
|
|
171
|
+
def _tri32(G, x, y, z):
|
|
172
|
+
# a float32 grid: map_coordinates hands back float32
|
|
173
|
+
return np.float64(np.float32(_tri(G, x, y, z)))
|
|
174
|
+
|
|
175
|
+
@_jit
|
|
176
|
+
def _lerp_index(x, n):
|
|
177
|
+
i0 = int(np.floor(x))
|
|
178
|
+
if i0 > n - 2:
|
|
179
|
+
i0 = n - 2 if n > 1 else 0
|
|
180
|
+
w = x - i0
|
|
181
|
+
i1 = i0 + 1 if n > 1 else 0
|
|
182
|
+
return i0, i1, w
|
|
183
|
+
|
|
184
|
+
@_jit
|
|
185
|
+
def _warp1(u, a0, a1, wa, b0, b1, wb, c0, c1, wc):
|
|
186
|
+
v00 = u[a0, b0, c0] * (1 - wc) + u[a0, b0, c1] * wc
|
|
187
|
+
v01 = u[a0, b1, c0] * (1 - wc) + u[a0, b1, c1] * wc
|
|
188
|
+
v10 = u[a1, b0, c0] * (1 - wc) + u[a1, b0, c1] * wc
|
|
189
|
+
v11 = u[a1, b1, c0] * (1 - wc) + u[a1, b1, c1] * wc
|
|
190
|
+
v0 = v00 * (1 - wb) + v01 * wb
|
|
191
|
+
v1 = v10 * (1 - wb) + v11 * wb
|
|
192
|
+
return v0 * (1 - wa) + v1 * wa
|
|
193
|
+
|
|
194
|
+
@_jit
|
|
195
|
+
def _warp(x, y, z, lo0, lo1, lo2, h, u0, u1, u2, c):
|
|
196
|
+
# fieldops._warp for one point: the graded cell's lattice coordinates
|
|
197
|
+
n0, n1, n2 = u0.shape
|
|
198
|
+
ix = (x - lo0) / h
|
|
199
|
+
iy = (y - lo1) / h
|
|
200
|
+
iz = (z - lo2) / h
|
|
201
|
+
cx = min(max(ix, 0.0), n0 - 1.0)
|
|
202
|
+
cy = min(max(iy, 0.0), n1 - 1.0)
|
|
203
|
+
cz = min(max(iz, 0.0), n2 - 1.0)
|
|
204
|
+
a0, a1, wa = _lerp_index(cx, n0)
|
|
205
|
+
b0, b1, wb = _lerp_index(cy, n1)
|
|
206
|
+
c0, c1, wc = _lerp_index(cz, n2)
|
|
207
|
+
U0 = _warp1(u0, a0, a1, wa, b0, b1, wb, c0, c1, wc) + (ix - cx) * h / c
|
|
208
|
+
U1 = _warp1(u1, a0, a1, wa, b0, b1, wb, c0, c1, wc) + (iy - cy) * h / c
|
|
209
|
+
U2 = _warp1(u2, a0, a1, wa, b0, b1, wb, c0, c1, wc) + (iz - cz) * h / c
|
|
210
|
+
return U0, U1, U2
|
|
211
|
+
|
|
212
|
+
@_jit
|
|
213
|
+
def _tpms64(kind, px, py, pz, k):
|
|
214
|
+
# fieldops._tpms for one point (= lattice.tpms in double)
|
|
215
|
+
x = k * px
|
|
216
|
+
y = k * py
|
|
217
|
+
z = k * pz
|
|
218
|
+
if kind == 2:
|
|
219
|
+
f = np.cos(x) + np.cos(y) + np.cos(z)
|
|
220
|
+
g0 = -np.sin(x) * k
|
|
221
|
+
g1 = -np.sin(y) * k
|
|
222
|
+
g2 = -np.sin(z) * k
|
|
223
|
+
gn = np.sqrt(g0 * g0 + g1 * g1 + g2 * g2)
|
|
224
|
+
return f / max(gn, 0.3 * k)
|
|
225
|
+
sx, cx, sy, cy, sz, cz = np.sin(x), np.cos(x), np.sin(y), np.cos(y), np.sin(z), np.cos(z)
|
|
226
|
+
if kind == 0:
|
|
227
|
+
f = sx * cy + sy * cz + sz * cx
|
|
228
|
+
g0 = cx * cy - sz * sx
|
|
229
|
+
g1 = cy * cz - sx * sy
|
|
230
|
+
g2 = cz * cx - sy * sz
|
|
231
|
+
else:
|
|
232
|
+
f = sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz
|
|
233
|
+
g0 = cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz
|
|
234
|
+
g1 = sx * cy * sz - sx * sy * cz + cx * cy * cz - cx * sy * sz
|
|
235
|
+
g2 = sx * sy * cz - sx * cy * sz - cx * sy * sz + cx * cy * cz
|
|
236
|
+
gn = np.sqrt(g0 * g0 + g1 * g1 + g2 * g2) * k
|
|
237
|
+
return f / max(gn, 0.3 * k)
|
|
238
|
+
|
|
239
|
+
@_jit
|
|
240
|
+
def _tpms32(kind, px, py, pz, cell):
|
|
241
|
+
# lattice.tpms under single_precision: phase reduced in double, trig in float32
|
|
242
|
+
two_pi = np.float32(2.0 * np.pi)
|
|
243
|
+
inv = 1.0 / cell
|
|
244
|
+
u = px * inv
|
|
245
|
+
u -= np.floor(u)
|
|
246
|
+
x = np.float32(u) * two_pi
|
|
247
|
+
u = py * inv
|
|
248
|
+
u -= np.floor(u)
|
|
249
|
+
y = np.float32(u) * two_pi
|
|
250
|
+
u = pz * inv
|
|
251
|
+
u -= np.floor(u)
|
|
252
|
+
z = np.float32(u) * two_pi
|
|
253
|
+
k = np.float32(2.0 * np.pi / cell)
|
|
254
|
+
sx, cx, sy, cy, sz, cz = np.sin(x), np.cos(x), np.sin(y), np.cos(y), np.sin(z), np.cos(z)
|
|
255
|
+
if kind == 0:
|
|
256
|
+
f = sx * cy + sy * cz + sz * cx
|
|
257
|
+
g0 = cx * cy - sz * sx
|
|
258
|
+
g1 = cy * cz - sx * sy
|
|
259
|
+
g2 = cz * cx - sy * sz
|
|
260
|
+
else:
|
|
261
|
+
f = sx * sy * sz + sx * cy * cz + cx * sy * cz + cx * cy * sz
|
|
262
|
+
g0 = cx * sy * sz + cx * cy * cz - sx * sy * cz - sx * cy * sz
|
|
263
|
+
g1 = sx * cy * sz - sx * sy * cz + cx * cy * cz - cx * sy * sz
|
|
264
|
+
g2 = sx * sy * cz - sx * cy * sz - cx * sy * sz + cx * cy * cz
|
|
265
|
+
gn = np.sqrt(g0 * g0 + g1 * g1 + g2 * g2) * k
|
|
266
|
+
lim = np.float32(0.3) * k
|
|
267
|
+
return np.float64(f / (gn if gn >= lim else lim))
|
|
268
|
+
|
|
269
|
+
@_jit
|
|
270
|
+
def _tpms(kind, single, px, py, pz, cell):
|
|
271
|
+
"""lattice.tpms(kind, p, cell) for one point, as the numpy graph
|
|
272
|
+
takes it (single precision inside lattice.single_precision)."""
|
|
273
|
+
if single and kind != 2:
|
|
274
|
+
return _tpms32(kind, px, py, pz, cell)
|
|
275
|
+
return _tpms64(kind, px, py, pz, 2.0 * np.pi / cell)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---- code generation ---------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
class _Opaque(Exception):
|
|
281
|
+
"""The node runs on the numpy graph."""
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_EXPR_FUNCS = {"abs", "sqrt", "exp", "log", "sin", "cos", "tan", "floor", "ceil", "sign", "min", "max",
|
|
285
|
+
"clamp", "lerp", "smoothstep", "step", "where"}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class _Gen:
|
|
289
|
+
def __init__(self, g):
|
|
290
|
+
self.g = g
|
|
291
|
+
self.C = [] # parameters
|
|
292
|
+
self.tables = [] # remap tables / drawn curves: concatenated
|
|
293
|
+
self.lines = []
|
|
294
|
+
self.vars = {} # (node, frame) -> variable
|
|
295
|
+
self.opaque = [] # root-frame nodes the numpy graph evaluates
|
|
296
|
+
self.grids = [] # (array, dtype tag)
|
|
297
|
+
self.grid_ids = {}
|
|
298
|
+
self.frames = [("px", "py", "pz")]
|
|
299
|
+
self.n = 0
|
|
300
|
+
self._ok = {}
|
|
301
|
+
|
|
302
|
+
# -- small builders --
|
|
303
|
+
|
|
304
|
+
def c(self, v):
|
|
305
|
+
self.C.append(float(v))
|
|
306
|
+
return f"C[{len(self.C) - 1}]"
|
|
307
|
+
|
|
308
|
+
def t(self, expr):
|
|
309
|
+
name = f"t{self.n}"
|
|
310
|
+
self.n += 1
|
|
311
|
+
self.lines.append(f"{name} = {expr}")
|
|
312
|
+
return name
|
|
313
|
+
|
|
314
|
+
def grid(self, key, arr):
|
|
315
|
+
if key not in self.grid_ids:
|
|
316
|
+
a = np.ascontiguousarray(arr)
|
|
317
|
+
if a.dtype not in (np.float32, np.float64):
|
|
318
|
+
a = a.astype(np.float64)
|
|
319
|
+
self.grid_ids[key] = len(self.grids)
|
|
320
|
+
self.grids.append(a)
|
|
321
|
+
return f"G{self.grid_ids[key]}"
|
|
322
|
+
|
|
323
|
+
def table(self, xs, ys):
|
|
324
|
+
i0 = len(self.tables)
|
|
325
|
+
self.tables += [(float(a), float(b)) for a, b in zip(xs, ys)]
|
|
326
|
+
return self.c(i0), len(xs)
|
|
327
|
+
|
|
328
|
+
# -- which nodes compile --
|
|
329
|
+
|
|
330
|
+
def supported(self, name, frame=0):
|
|
331
|
+
"""True when the node itself compiles (its inputs may still be opaque,
|
|
332
|
+
in the root frame); a transform also needs everything under it."""
|
|
333
|
+
key = (name, frame)
|
|
334
|
+
if key in self._ok:
|
|
335
|
+
return self._ok[key]
|
|
336
|
+
self._ok[key] = False # cycles are refused by the graph anyway
|
|
337
|
+
nd = self.g.nodes[name]
|
|
338
|
+
ok = self._local_ok(nd)
|
|
339
|
+
if ok and nd.op == "transform":
|
|
340
|
+
ok = self._all_ok(nd.refs["input"])
|
|
341
|
+
self._ok[key] = ok
|
|
342
|
+
return ok
|
|
343
|
+
|
|
344
|
+
def _all_ok(self, ref, seen=None):
|
|
345
|
+
if not isinstance(ref, str):
|
|
346
|
+
return True
|
|
347
|
+
seen = seen if seen is not None else set()
|
|
348
|
+
if ref in seen:
|
|
349
|
+
return True
|
|
350
|
+
seen.add(ref)
|
|
351
|
+
nd = self.g.nodes[ref]
|
|
352
|
+
if not self._local_ok(nd):
|
|
353
|
+
return False
|
|
354
|
+
return all(self._all_ok(d, seen) for d in self.g.deps[ref])
|
|
355
|
+
|
|
356
|
+
def _local_ok(self, nd):
|
|
357
|
+
from .fields import _TPMS_KIND
|
|
358
|
+
|
|
359
|
+
s, r, op = nd.spec, nd.refs, nd.op
|
|
360
|
+
try:
|
|
361
|
+
if op in ("constant", "remap", "union", "intersect", "subtract", "offset", "transform"):
|
|
362
|
+
if op == "remap":
|
|
363
|
+
self._remap_kind(s)
|
|
364
|
+
return True
|
|
365
|
+
if op == "coordinate":
|
|
366
|
+
return True
|
|
367
|
+
if op == "shell":
|
|
368
|
+
return s.get("side", "inside") in ("inside", "outside", "center")
|
|
369
|
+
if op == "expr":
|
|
370
|
+
self._expr_check(s["expr"])
|
|
371
|
+
return True
|
|
372
|
+
if op == "shape":
|
|
373
|
+
if s["shape"].get("type") == "mesh":
|
|
374
|
+
return False
|
|
375
|
+
return self._shape_ok(self._shape(nd))
|
|
376
|
+
if op == "distance":
|
|
377
|
+
if "input" in r or "segment" in s or "plane" in s:
|
|
378
|
+
return True
|
|
379
|
+
if "shape" in s:
|
|
380
|
+
return self._shape_ok(self._shape(nd))
|
|
381
|
+
return False
|
|
382
|
+
if op in ("result", "grid", "image"):
|
|
383
|
+
smp = self.g._resource(nd, lambda: self.g._samples(nd))
|
|
384
|
+
return getattr(smp, "tree", 1) is None and np.ndim(getattr(smp, "grid", None)) == 3
|
|
385
|
+
if op == "lattice":
|
|
386
|
+
from .struts import is_strut
|
|
387
|
+
|
|
388
|
+
kind = s.get("type", "gyroid")
|
|
389
|
+
if _TPMS_KIND.get(kind) is None or is_strut(kind):
|
|
390
|
+
return False
|
|
391
|
+
if isinstance(s.get("orient"), dict) and "zones" in s["orient"]:
|
|
392
|
+
return False
|
|
393
|
+
if s.get("frame") or "aspect" in s or "density" in r:
|
|
394
|
+
return False
|
|
395
|
+
if s.get("form", "sheet") not in ("sheet", "network", "_plus", "_minus"):
|
|
396
|
+
return False
|
|
397
|
+
graded = isinstance(r.get("cell"), str)
|
|
398
|
+
if graded and s.get("grading", "warp") != "warp":
|
|
399
|
+
return False
|
|
400
|
+
from .fields import lattice_orientation
|
|
401
|
+
|
|
402
|
+
lattice_orientation(nd) # raises on what numpy refuses
|
|
403
|
+
return True
|
|
404
|
+
except Exception:
|
|
405
|
+
return False
|
|
406
|
+
return False
|
|
407
|
+
|
|
408
|
+
def _shape(self, nd):
|
|
409
|
+
from .shapes import shape_from_spec
|
|
410
|
+
|
|
411
|
+
return self.g._resource(nd, lambda: shape_from_spec(nd.spec["shape"], self.g.base_dir))
|
|
412
|
+
|
|
413
|
+
def _shape_ok(self, shp):
|
|
414
|
+
from .shapes import Boolean, Box, Cylinder, MeshShape, Shell, Sphere, Union
|
|
415
|
+
|
|
416
|
+
if isinstance(shp, (Box, Cylinder, Sphere)):
|
|
417
|
+
return True
|
|
418
|
+
if type(shp) is MeshShape:
|
|
419
|
+
return True
|
|
420
|
+
if isinstance(shp, Union):
|
|
421
|
+
return all(self._shape_ok(x) for x in shp.shapes)
|
|
422
|
+
if isinstance(shp, Boolean):
|
|
423
|
+
return all(self._shape_ok(x) for x in shp.parts)
|
|
424
|
+
if type(shp) is Shell:
|
|
425
|
+
return self._shape_ok(shp.outer) and self._shape_ok(shp.inner) and (
|
|
426
|
+
shp.clip is None or self._shape_ok(shp.clip))
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
@staticmethod
|
|
430
|
+
def _remap_kind(s):
|
|
431
|
+
if len(s.get("from", ())) > 2 or len(s.get("to", ())) > 2:
|
|
432
|
+
xs, ys = np.asarray(s["from"], float), np.asarray(s["to"], float)
|
|
433
|
+
if len(xs) != len(ys) or np.any(np.diff(xs) <= 0):
|
|
434
|
+
raise ValueError
|
|
435
|
+
return "table"
|
|
436
|
+
a, b = (float(u) for u in s.get("from", (0.0, 1.0)))
|
|
437
|
+
if a == b:
|
|
438
|
+
raise ValueError
|
|
439
|
+
curve = s.get("curve", "linear")
|
|
440
|
+
if isinstance(curve, (list, tuple)):
|
|
441
|
+
pts = np.asarray(curve, float)
|
|
442
|
+
if pts.ndim != 2 or pts.shape[1] != 2 or len(pts) < 2 or np.any(np.diff(pts[:, 0]) <= 0):
|
|
443
|
+
raise ValueError
|
|
444
|
+
return "points"
|
|
445
|
+
if curve not in ("linear", "smoothstep", "smootherstep", "sqrt", "square"):
|
|
446
|
+
raise ValueError
|
|
447
|
+
return curve
|
|
448
|
+
|
|
449
|
+
# -- emission --
|
|
450
|
+
|
|
451
|
+
def val(self, ref, frame):
|
|
452
|
+
if isinstance(ref, float):
|
|
453
|
+
return self.c(ref)
|
|
454
|
+
return self.node(ref, frame)
|
|
455
|
+
|
|
456
|
+
def node(self, name, frame=0):
|
|
457
|
+
key = (name, frame)
|
|
458
|
+
if key in self.vars:
|
|
459
|
+
return self.vars[key]
|
|
460
|
+
if not self.supported(name, frame):
|
|
461
|
+
if frame != 0:
|
|
462
|
+
raise AssertionError("an opaque node inside a transform") # _all_ok keeps this out
|
|
463
|
+
j = len(self.opaque)
|
|
464
|
+
self.opaque.append(name)
|
|
465
|
+
v = self.t(f"X[{j}, i]")
|
|
466
|
+
else:
|
|
467
|
+
v = self._emit(self.g.nodes[name], frame)
|
|
468
|
+
self.vars[key] = v
|
|
469
|
+
return v
|
|
470
|
+
|
|
471
|
+
def _emit(self, nd, f):
|
|
472
|
+
s, r, op = nd.spec, nd.refs, nd.op
|
|
473
|
+
x, y, z = self.frames[f]
|
|
474
|
+
v = lambda key: self.val(r[key], f) # noqa: E731
|
|
475
|
+
if op == "constant":
|
|
476
|
+
return self.c(s["value"])
|
|
477
|
+
if op == "coordinate":
|
|
478
|
+
ax = s.get("axis", "z")
|
|
479
|
+
if isinstance(ax, str):
|
|
480
|
+
return (x, y, z)["xyz".index(ax)]
|
|
481
|
+
a = np.asarray(ax, float)
|
|
482
|
+
a = a / np.linalg.norm(a)
|
|
483
|
+
o = np.asarray(s.get("origin", (0, 0, 0)), float)
|
|
484
|
+
return self.t(f"({x} - {self.c(o[0])}) * {self.c(a[0])} + ({y} - {self.c(o[1])}) * {self.c(a[1])}"
|
|
485
|
+
f" + ({z} - {self.c(o[2])}) * {self.c(a[2])}")
|
|
486
|
+
if op == "shape":
|
|
487
|
+
return self._sdf(self._shape(nd), f)
|
|
488
|
+
if op in ("result", "grid", "image"):
|
|
489
|
+
smp = self.g._resource(nd, lambda: self.g._samples(nd))
|
|
490
|
+
G = self.grid(("samples", nd.name), smp.grid)
|
|
491
|
+
o, p = smp.origin, smp.pitch
|
|
492
|
+
fn = "_tri32" if smp.grid.dtype == np.float32 else "_tri"
|
|
493
|
+
return self.t(f"{fn}({G}, ({x} - {self.c(o[0])}) / {self.c(p[0])}, ({y} - {self.c(o[1])}) / "
|
|
494
|
+
f"{self.c(p[1])}, ({z} - {self.c(o[2])}) / {self.c(p[2])})")
|
|
495
|
+
if op == "remap":
|
|
496
|
+
return self._remap(v("input"), s)
|
|
497
|
+
if op == "expr":
|
|
498
|
+
env = {"x": x, "y": y, "z": z}
|
|
499
|
+
for k, ref in r["vars"].items():
|
|
500
|
+
env[k] = self.val(ref, f)
|
|
501
|
+
return self.t(self._expr(s["expr"], env))
|
|
502
|
+
if op in ("union", "intersect", "subtract"):
|
|
503
|
+
vals = [self.val(a, f) for a in r["inputs"]]
|
|
504
|
+
k = v("blend") if "blend" in r else "0.0"
|
|
505
|
+
out = vals[0]
|
|
506
|
+
for b in vals[1:]:
|
|
507
|
+
if op == "union":
|
|
508
|
+
out = self.t(f"_smin({out}, {b}, {k})")
|
|
509
|
+
elif op == "intersect":
|
|
510
|
+
out = self.t(f"-_smin(-{out}, -{b}, {k})")
|
|
511
|
+
else:
|
|
512
|
+
out = self.t(f"-_smin(-{out}, {b}, {k})")
|
|
513
|
+
return out
|
|
514
|
+
if op == "shell":
|
|
515
|
+
a, t = v("input"), v("thickness")
|
|
516
|
+
side = s.get("side", "inside")
|
|
517
|
+
if side == "inside":
|
|
518
|
+
return self.t(f"_mx({a}, -({a} + {t}))")
|
|
519
|
+
if side == "outside":
|
|
520
|
+
return self.t(f"_mx(-{a}, {a} - {t})")
|
|
521
|
+
return self.t(f"abs({a}) - 0.5 * {t}")
|
|
522
|
+
if op == "offset":
|
|
523
|
+
return self.t(f"{v('input')} - {v('distance')}")
|
|
524
|
+
if op == "distance":
|
|
525
|
+
return self._distance(nd, f)
|
|
526
|
+
if op == "lattice":
|
|
527
|
+
return self._lattice(nd, f)
|
|
528
|
+
if op == "transform":
|
|
529
|
+
return self._transform(nd, f)
|
|
530
|
+
raise AssertionError(op)
|
|
531
|
+
|
|
532
|
+
def _remap(self, xv, s):
|
|
533
|
+
kind = self._remap_kind(s)
|
|
534
|
+
if kind == "table":
|
|
535
|
+
i0, n = self.table(s["from"], s["to"])
|
|
536
|
+
return self.t(f"_interp({xv}, TX, TY, int({i0}), {n})")
|
|
537
|
+
a, b = (float(u) for u in s.get("from", (0.0, 1.0)))
|
|
538
|
+
c, d = (float(u) for u in s.get("to", (0.0, 1.0)))
|
|
539
|
+
u = self.t(f"({xv} - {self.c(a)}) / {self.c(b - a)}")
|
|
540
|
+
if s.get("clamp", True):
|
|
541
|
+
u = self.t(f"_clip({u}, 0.0, 1.0)")
|
|
542
|
+
cc, dc = self.c(c), self.c(d - c)
|
|
543
|
+
if kind == "points":
|
|
544
|
+
pts = np.asarray(s["curve"], float)
|
|
545
|
+
i0, n = self.table(pts[:, 0], pts[:, 1])
|
|
546
|
+
w = self.t(f"_interp(_clip({u}, {self.c(pts[0, 0])}, {self.c(pts[-1, 0])}), TX, TY, int({i0}), {n})")
|
|
547
|
+
return self.t(f"{cc} + {dc} * {w}")
|
|
548
|
+
if kind == "smoothstep":
|
|
549
|
+
uc = self.t(f"_clip({u}, 0.0, 1.0)")
|
|
550
|
+
u = self.t(f"{uc} * {uc} * (3 - 2 * {uc})")
|
|
551
|
+
elif kind == "smootherstep":
|
|
552
|
+
uc = self.t(f"_clip({u}, 0.0, 1.0)")
|
|
553
|
+
u = self.t(f"{uc} ** 3.0 * ({uc} * (6 * {uc} - 15) + 10)")
|
|
554
|
+
elif kind == "sqrt":
|
|
555
|
+
u = self.t(f"np.sqrt(_mx({u}, 0.0))")
|
|
556
|
+
elif kind == "square":
|
|
557
|
+
u = self.t(f"{u} * {u}")
|
|
558
|
+
return self.t(f"{cc} + {dc} * {u}")
|
|
559
|
+
|
|
560
|
+
def _sdf(self, shp, f):
|
|
561
|
+
from .shapes import Boolean, Box, Cylinder, MeshShape, Shell, Sphere, Union
|
|
562
|
+
|
|
563
|
+
x, y, z = self.frames[f]
|
|
564
|
+
if isinstance(shp, Box):
|
|
565
|
+
c, h = 0.5 * (shp.lo + shp.hi), 0.5 * (shp.hi - shp.lo)
|
|
566
|
+
q = [self.t(f"abs({p} - {self.c(c[i])}) - {self.c(h[i])}") for i, p in enumerate((x, y, z))]
|
|
567
|
+
a, b, d = (self.t(f"_mx({qi}, 0.0)") for qi in q)
|
|
568
|
+
return self.t(f"np.sqrt({a} * {a} + {b} * {b} + {d} * {d}) + _mn(_mx(_mx({q[0]}, {q[1]}), {q[2]}), 0.0)")
|
|
569
|
+
if isinstance(shp, Sphere):
|
|
570
|
+
ce = shp.center
|
|
571
|
+
d = [self.t(f"{p} - {self.c(ce[i])}") for i, p in enumerate((x, y, z))]
|
|
572
|
+
return self.t(f"np.sqrt({d[0]} * {d[0]} + {d[1]} * {d[1]} + {d[2]} * {d[2]}) - {self.c(shp.r)}")
|
|
573
|
+
if isinstance(shp, Cylinder):
|
|
574
|
+
P = (x, y, z)
|
|
575
|
+
ax = shp.axis
|
|
576
|
+
k = int(np.argmax(np.abs(ax)))
|
|
577
|
+
if abs(ax[k]) == 1.0:
|
|
578
|
+
i, j = [w for w in range(3) if w != k]
|
|
579
|
+
t = self.t(f"({P[k]} - {self.c(shp.p0[k])}) * {self.c(ax[k])}")
|
|
580
|
+
u = self.t(f"{P[i]} - {self.c(shp.p0[i])}")
|
|
581
|
+
w = self.t(f"{P[j]} - {self.c(shp.p0[j])}")
|
|
582
|
+
radial = self.t(f"np.sqrt({u} * {u} + {w} * {w})")
|
|
583
|
+
else:
|
|
584
|
+
d = [self.t(f"{P[a]} - {self.c(shp.p0[a])}") for a in range(3)]
|
|
585
|
+
t = self.t(f"{d[0]} * {self.c(ax[0])} + {d[1]} * {self.c(ax[1])} + {d[2]} * {self.c(ax[2])}")
|
|
586
|
+
radial = self.t(f"np.sqrt(_mx({d[0]} * {d[0]} + {d[1]} * {d[1]} + {d[2]} * {d[2]} - {t} * {t}, 0.0))")
|
|
587
|
+
half = 0.5 * shp.length
|
|
588
|
+
if shp.r_inner > 0:
|
|
589
|
+
mid = 0.5 * (shp.r + shp.r_inner)
|
|
590
|
+
rq = self.t(f"abs({radial} - {self.c(mid)}) - {self.c(0.5 * (shp.r - shp.r_inner))}")
|
|
591
|
+
else:
|
|
592
|
+
rq = self.t(f"{radial} - {self.c(shp.r)}")
|
|
593
|
+
hc = self.c(half)
|
|
594
|
+
aq = self.t(f"abs({t} - {hc}) - {hc}")
|
|
595
|
+
a, b = self.t(f"_mx({rq}, 0.0)"), self.t(f"_mx({aq}, 0.0)")
|
|
596
|
+
return self.t(f"np.sqrt({a} * {a} + {b} * {b}) + _mn(_mx({rq}, {aq}), 0.0)")
|
|
597
|
+
if isinstance(shp, Union):
|
|
598
|
+
out = self._sdf(shp.shapes[0], f)
|
|
599
|
+
for sub in shp.shapes[1:]:
|
|
600
|
+
out = self.t(f"_mn({out}, {self._sdf(sub, f)})")
|
|
601
|
+
return out
|
|
602
|
+
if isinstance(shp, Boolean):
|
|
603
|
+
out = self._sdf(shp.parts[0], f)
|
|
604
|
+
for sub in shp.parts[1:]:
|
|
605
|
+
b = self._sdf(sub, f)
|
|
606
|
+
out = self.t(f"_mx({out}, {b})" if shp.kind == "intersect" else f"_mx({out}, -{b})")
|
|
607
|
+
return out
|
|
608
|
+
if isinstance(shp, Shell):
|
|
609
|
+
d = self.t(f"_mx({self._sdf(shp.outer, f)}, -{self._sdf(shp.inner, f)})")
|
|
610
|
+
return d if shp.clip is None else self.t(f"_mx({d}, {self._sdf(shp.clip, f)})")
|
|
611
|
+
if type(shp) is MeshShape:
|
|
612
|
+
# its voxel distance grid, trilinear; past the grid the box distance
|
|
613
|
+
if shp._grid is None:
|
|
614
|
+
shp._build_grid()
|
|
615
|
+
dist, origin, pitch = shp._grid
|
|
616
|
+
G = self.grid(("meshshape", id(shp)), np.asarray(dist, np.float64))
|
|
617
|
+
n = dist.shape
|
|
618
|
+
co = [self.t(f"({p} - {self.c(origin[i])}) / {self.c(pitch)} - 0.5") for i, p in enumerate((x, y, z))]
|
|
619
|
+
inside = " and ".join(f"({co[i]} >= 0 and {co[i]} <= {self.c(n[i] - 1)})" for i in range(3))
|
|
620
|
+
lo, hi = shp.bounds()
|
|
621
|
+
q = [self.t(f"_mx(_mx({self.c(lo[i])} - {p}, {p} - {self.c(hi[i])}), 0.0)") for i, p in enumerate((x, y, z))]
|
|
622
|
+
far = f"_mx(np.sqrt({q[0]} * {q[0]} + {q[1]} * {q[1]} + {q[2]} * {q[2]}), {self.c(4 * pitch)})"
|
|
623
|
+
return self.t(f"_tri({G}, {co[0]}, {co[1]}, {co[2]}) if {inside} else {far}")
|
|
624
|
+
raise AssertionError(type(shp))
|
|
625
|
+
|
|
626
|
+
def _distance(self, nd, f):
|
|
627
|
+
s, r = nd.spec, nd.refs
|
|
628
|
+
x, y, z = self.frames[f]
|
|
629
|
+
if "input" in r:
|
|
630
|
+
a = self.val(r["input"], f)
|
|
631
|
+
return a if s.get("signed") else self.t(f"abs({a})")
|
|
632
|
+
if "segment" in s:
|
|
633
|
+
a, b = (np.asarray(q, float) for q in s["segment"])
|
|
634
|
+
ab = b - a
|
|
635
|
+
d = [self.t(f"{p} - {self.c(a[i])}") for i, p in enumerate((x, y, z))]
|
|
636
|
+
den = self.c(max(float(ab @ ab), 1e-300))
|
|
637
|
+
t = self.t(f"_clip(({d[0]} * {self.c(ab[0])} + {d[1]} * {self.c(ab[1])} + {d[2]} * {self.c(ab[2])})"
|
|
638
|
+
f" / {den}, 0.0, 1.0)")
|
|
639
|
+
e = [self.t(f"{p} - ({self.c(a[i])} + {t} * {self.c(ab[i])})") for i, p in enumerate((x, y, z))]
|
|
640
|
+
return self.t(f"np.sqrt({e[0]} * {e[0]} + {e[1]} * {e[1]} + {e[2]} * {e[2]})")
|
|
641
|
+
if "plane" in s:
|
|
642
|
+
n = np.asarray(s["plane"]["normal"], float)
|
|
643
|
+
n = n / np.linalg.norm(n)
|
|
644
|
+
o = np.asarray(s["plane"]["point"], float)
|
|
645
|
+
return self.t(f"abs(({x} - {self.c(o[0])}) * {self.c(n[0])} + ({y} - {self.c(o[1])}) * {self.c(n[1])}"
|
|
646
|
+
f" + ({z} - {self.c(o[2])}) * {self.c(n[2])})")
|
|
647
|
+
return self.t(f"abs({self._sdf(self._shape(nd), f)})")
|
|
648
|
+
|
|
649
|
+
def _lattice(self, nd, f):
|
|
650
|
+
from .fields import _TPMS_KIND, lattice_orientation
|
|
651
|
+
|
|
652
|
+
s, r = nd.spec, nd.refs
|
|
653
|
+
x, y, z = self.frames[f]
|
|
654
|
+
kind = _KINDS[_TPMS_KIND[s.get("type", "gyroid")]]
|
|
655
|
+
cell = self.val(r["cell"], f) if "cell" in r else self.c(8.0)
|
|
656
|
+
q = [x, y, z]
|
|
657
|
+
if "origin" in s:
|
|
658
|
+
o = np.asarray(s["origin"], float)
|
|
659
|
+
q = [self.t(f"{p} - {self.c(o[i])}") for i, p in enumerate(q)]
|
|
660
|
+
R = lattice_orientation(nd)
|
|
661
|
+
if R is not None:
|
|
662
|
+
q = [self.t(" + ".join(f"{q[a]} * {self.c(R[a, b])}" for a in range(3))) for b in range(3)]
|
|
663
|
+
if isinstance(r.get("cell"), str):
|
|
664
|
+
warp = self.g._resource(nd, lambda: self.g._warp(nd))
|
|
665
|
+
us = warp._grids()
|
|
666
|
+
G = [self.grid(("warp", nd.name, i), us[i]) for i in range(3)]
|
|
667
|
+
u = [f"w{self.n}_{i}" for i in range(3)]
|
|
668
|
+
self.n += 1
|
|
669
|
+
self.lines.append(f"{u[0]}, {u[1]}, {u[2]} = _warp({q[0]}, {q[1]}, {q[2]}, {self.c(warp.lo[0])}, "
|
|
670
|
+
f"{self.c(warp.lo[1])}, {self.c(warp.lo[2])}, {self.c(warp.h)}, {G[0]}, {G[1]}, {G[2]}, "
|
|
671
|
+
f"{cell})")
|
|
672
|
+
d = self.t(f"_tpms({kind}, single, {u[0]}, {u[1]}, {u[2]}, 1.0) * {cell}")
|
|
673
|
+
else:
|
|
674
|
+
d = self.t(f"_tpms({kind}, single, {q[0]}, {q[1]}, {q[2]}, {cell})")
|
|
675
|
+
if "offset" in r:
|
|
676
|
+
d = self.t(f"{d} - {self.val(r['offset'], f)}")
|
|
677
|
+
t = self.val(r["thickness"], f) if "thickness" in r else "1.0"
|
|
678
|
+
form = s.get("form", "sheet")
|
|
679
|
+
if form == "sheet":
|
|
680
|
+
return self.t(f"abs({d}) - 0.5 * {t}")
|
|
681
|
+
if form in ("network", "_plus"):
|
|
682
|
+
return self.t(f"{d} - 0.5 * {t}")
|
|
683
|
+
return self.t(f"-{d} - 0.5 * {t}")
|
|
684
|
+
|
|
685
|
+
def _transform(self, nd, f):
|
|
686
|
+
s = nd.spec
|
|
687
|
+
q = list(self.frames[f])
|
|
688
|
+
if "translate" in s:
|
|
689
|
+
tr = np.asarray(s["translate"], float)
|
|
690
|
+
q = [self.t(f"{p} - {self.c(tr[i])}") for i, p in enumerate(q)]
|
|
691
|
+
if "rotate" in s:
|
|
692
|
+
rot = s["rotate"]
|
|
693
|
+
ax = rot.get("axis", "z")
|
|
694
|
+
a = np.asarray({"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}[ax] if isinstance(ax, str) else ax,
|
|
695
|
+
float)
|
|
696
|
+
a = a / np.linalg.norm(a)
|
|
697
|
+
c = np.asarray(rot.get("center", (0, 0, 0)), float)
|
|
698
|
+
th = -math.radians(float(rot["degrees"]))
|
|
699
|
+
K = np.array([[0, -a[2], a[1]], [a[2], 0, -a[0]], [-a[1], a[0], 0]])
|
|
700
|
+
R = np.eye(3) + math.sin(th) * K + (1 - math.cos(th)) * K @ K
|
|
701
|
+
d = [self.t(f"{p} - {self.c(c[i])}") for i, p in enumerate(q)]
|
|
702
|
+
q = [self.t(" + ".join(f"{d[a]} * {self.c(R[b, a])}" for a in range(3)) + f" + {self.c(c[b])}")
|
|
703
|
+
for b in range(3)]
|
|
704
|
+
scale = float(s.get("scale", 1.0))
|
|
705
|
+
if scale != 1.0:
|
|
706
|
+
sc = self.c(scale)
|
|
707
|
+
q = [self.t(f"{p} / {sc}") for p in q]
|
|
708
|
+
self.frames.append(tuple(q))
|
|
709
|
+
inner = self.node(nd.refs["input"], len(self.frames) - 1)
|
|
710
|
+
return self.t(f"{inner} * {self.c(scale)}")
|
|
711
|
+
|
|
712
|
+
# -- expr --
|
|
713
|
+
|
|
714
|
+
def _expr_check(self, text):
|
|
715
|
+
self._expr(text, None)
|
|
716
|
+
|
|
717
|
+
def _expr(self, text, env):
|
|
718
|
+
tree = ast.parse(text, mode="eval")
|
|
719
|
+
ops = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/", ast.Pow: "**", ast.Mod: "%"}
|
|
720
|
+
cmps = {ast.Lt: "<", ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">="}
|
|
721
|
+
|
|
722
|
+
def b(n):
|
|
723
|
+
if isinstance(n, ast.Expression):
|
|
724
|
+
return b(n.body)
|
|
725
|
+
if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)) and not isinstance(n.value, bool):
|
|
726
|
+
return repr(float(n.value))
|
|
727
|
+
if isinstance(n, ast.Name):
|
|
728
|
+
if n.id == "pi":
|
|
729
|
+
return repr(math.pi)
|
|
730
|
+
if n.id == "e":
|
|
731
|
+
return repr(math.e)
|
|
732
|
+
if env is None:
|
|
733
|
+
return "0.0"
|
|
734
|
+
return env[n.id]
|
|
735
|
+
if isinstance(n, ast.BinOp) and type(n.op) in ops:
|
|
736
|
+
return f"({b(n.left)} {ops[type(n.op)]} {b(n.right)})"
|
|
737
|
+
if isinstance(n, ast.UnaryOp) and isinstance(n.op, (ast.USub, ast.UAdd)):
|
|
738
|
+
return f"(-{b(n.operand)})" if isinstance(n.op, ast.USub) else b(n.operand)
|
|
739
|
+
if isinstance(n, ast.Compare) and len(n.ops) == 1 and type(n.ops[0]) in cmps:
|
|
740
|
+
return f"(1.0 if {b(n.left)} {cmps[type(n.ops[0])]} {b(n.comparators[0])} else 0.0)"
|
|
741
|
+
if isinstance(n, ast.IfExp):
|
|
742
|
+
return f"({b(n.body)} if {b(n.test)} > 0 else {b(n.orelse)})"
|
|
743
|
+
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id in _EXPR_FUNCS \
|
|
744
|
+
and not n.keywords:
|
|
745
|
+
a = [b(q) for q in n.args]
|
|
746
|
+
fn = n.func.id
|
|
747
|
+
one = {"abs": "abs({})", "sqrt": "np.sqrt(_mx({}, 0.0))", "exp": "np.exp({})",
|
|
748
|
+
"log": "np.log(_mx({}, 1e-300))", "sin": "np.sin({})", "cos": "np.cos({})",
|
|
749
|
+
"tan": "np.tan({})", "floor": "np.floor({})", "ceil": "np.ceil({})", "sign": "_sign({})"}
|
|
750
|
+
if fn in one:
|
|
751
|
+
if len(a) != 1:
|
|
752
|
+
raise ValueError
|
|
753
|
+
return one[fn].format(a[0])
|
|
754
|
+
if fn in ("min", "max"):
|
|
755
|
+
if not a:
|
|
756
|
+
raise ValueError
|
|
757
|
+
out = a[0]
|
|
758
|
+
for q in a[1:]:
|
|
759
|
+
out = f"{'_mn' if fn == 'min' else '_mx'}({out}, {q})"
|
|
760
|
+
return out
|
|
761
|
+
if fn == "clamp" and len(a) == 3:
|
|
762
|
+
return f"_clip({a[0]}, {a[1]}, {a[2]})"
|
|
763
|
+
if fn == "lerp" and len(a) == 3:
|
|
764
|
+
return f"({a[0]} + ({a[1]} - {a[0]}) * {a[2]})"
|
|
765
|
+
if fn == "smoothstep" and len(a) == 3:
|
|
766
|
+
u = f"_clip(({a[2]} - {a[0]}) / (1e-12 if {a[1]} == {a[0]} else {a[1]} - {a[0]}), 0.0, 1.0)"
|
|
767
|
+
return f"_sstep({u})"
|
|
768
|
+
if fn == "step" and len(a) == 2:
|
|
769
|
+
return f"(1.0 if {a[1]} >= {a[0]} else 0.0)"
|
|
770
|
+
if fn == "where" and len(a) == 3:
|
|
771
|
+
return f"({a[1]} if {a[0]} > 0 else {a[2]})"
|
|
772
|
+
raise ValueError(f"expr {text!r} does not compile")
|
|
773
|
+
|
|
774
|
+
return b(tree)
|
|
775
|
+
|
|
776
|
+
# -- the kernel --
|
|
777
|
+
|
|
778
|
+
def source(self, out):
|
|
779
|
+
"""The kernel: one output (out[i]) or a list of rows (out[j, i])."""
|
|
780
|
+
if isinstance(out, list):
|
|
781
|
+
store = "\n".join(f" out[{j}, i] = {o}" for j, o in enumerate(out)) + "\n"
|
|
782
|
+
else:
|
|
783
|
+
store = f" out[i] = {out}\n"
|
|
784
|
+
args = ", ".join(["P", "C", "TX", "TY", "X", "single", "out"] + [f"G{i}" for i in range(len(self.grids))])
|
|
785
|
+
body = "\n".join(" " + ln for ln in self.lines)
|
|
786
|
+
return (
|
|
787
|
+
"import math\nimport numpy as np\nimport numba as nb\n"
|
|
788
|
+
"from loadpath.fieldjit import _mx, _mn, _clip, _smin, _sign, _interp, _tri, _tri32, _warp, _tpms, _sstep\n\n"
|
|
789
|
+
"@nb.njit(cache=True, nogil=True, error_model='numpy')\n"
|
|
790
|
+
f"def kernel({args}):\n"
|
|
791
|
+
" for i in range(P.shape[0]):\n"
|
|
792
|
+
" px = P[i, 0]\n py = P[i, 1]\n pz = P[i, 2]\n"
|
|
793
|
+
f"{body}\n"
|
|
794
|
+
f"{store}")
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
if nb is not None:
|
|
798
|
+
@_jit
|
|
799
|
+
def _sstep(u):
|
|
800
|
+
return u * u * (3.0 - 2.0 * u)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
# ---- loading kernels ----------------------------------------------------------------
|
|
804
|
+
|
|
805
|
+
_KERNELS = {}
|
|
806
|
+
_LOCK = threading.Lock()
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def _cache_dir():
|
|
810
|
+
d = os.environ.get("LOADPATH_FIELDJIT_CACHE") or os.path.join(os.path.expanduser("~"), ".cache", "loadpath",
|
|
811
|
+
"fieldjit")
|
|
812
|
+
try:
|
|
813
|
+
os.makedirs(d, exist_ok=True)
|
|
814
|
+
probe = os.path.join(d, ".w")
|
|
815
|
+
with open(probe, "w"):
|
|
816
|
+
pass
|
|
817
|
+
return d
|
|
818
|
+
except OSError:
|
|
819
|
+
return None
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def _kernel(src):
|
|
823
|
+
"""The compiled kernel for this source (in-process, then on disk)."""
|
|
824
|
+
key = hashlib.sha1(src.encode()).hexdigest()[:20]
|
|
825
|
+
with _LOCK:
|
|
826
|
+
if key in _KERNELS:
|
|
827
|
+
return _KERNELS[key]
|
|
828
|
+
d = _cache_dir()
|
|
829
|
+
name = f"loadpath_fieldjit_{key}"
|
|
830
|
+
if d is not None:
|
|
831
|
+
path = os.path.join(d, name + ".py")
|
|
832
|
+
if not os.path.exists(path):
|
|
833
|
+
tmp = path + f".{os.getpid()}.tmp"
|
|
834
|
+
with open(tmp, "w") as fh:
|
|
835
|
+
fh.write(src)
|
|
836
|
+
os.replace(tmp, path)
|
|
837
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
838
|
+
mod = importlib.util.module_from_spec(spec)
|
|
839
|
+
sys.modules[name] = mod
|
|
840
|
+
spec.loader.exec_module(mod)
|
|
841
|
+
k = mod.kernel
|
|
842
|
+
else: # nowhere to cache: compile in memory
|
|
843
|
+
ns = {}
|
|
844
|
+
exec(compile(src.replace("cache=True", "cache=False"), name, "exec"), ns)
|
|
845
|
+
k = ns["kernel"]
|
|
846
|
+
_KERNELS[key] = k
|
|
847
|
+
return k
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
class CompiledField:
|
|
851
|
+
"""graph.evaluate(P, name) as one compiled pass (plus the numpy graph for
|
|
852
|
+
the nodes that do not compile, see `opaque`)."""
|
|
853
|
+
|
|
854
|
+
def __init__(self, g, name):
|
|
855
|
+
gen = _Gen(g)
|
|
856
|
+
out = gen.node(name)
|
|
857
|
+
self.g = g
|
|
858
|
+
self.name = name
|
|
859
|
+
self.opaque = gen.opaque
|
|
860
|
+
self.C = np.asarray(gen.C, np.float64)
|
|
861
|
+
tb = np.asarray(gen.tables, np.float64).reshape(-1, 2)
|
|
862
|
+
self.TX, self.TY = np.ascontiguousarray(tb[:, 0]), np.ascontiguousarray(tb[:, 1])
|
|
863
|
+
self.grids = gen.grids
|
|
864
|
+
self.source = gen.source(out)
|
|
865
|
+
self.kernel = _kernel(self.source)
|
|
866
|
+
self.nodes = len(gen.vars)
|
|
867
|
+
|
|
868
|
+
def __call__(self, P):
|
|
869
|
+
from .lattice import _SINGLE
|
|
870
|
+
|
|
871
|
+
P = np.ascontiguousarray(np.asarray(P, np.float64).reshape(-1, 3))
|
|
872
|
+
n = len(P)
|
|
873
|
+
X = np.empty((len(self.opaque), n))
|
|
874
|
+
memo = {}
|
|
875
|
+
for j, name in enumerate(self.opaque):
|
|
876
|
+
X[j] = np.broadcast_to(np.asarray(self.g._eval(name, P, memo), float), (n,))
|
|
877
|
+
out = np.empty(n)
|
|
878
|
+
if n:
|
|
879
|
+
self.kernel(P, self.C, self.TX, self.TY, X, bool(_SINGLE[0]), out, *self.grids)
|
|
880
|
+
return out
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
class CompiledRows:
|
|
884
|
+
"""Several values of a graph at once, one compiled pass sharing every
|
|
885
|
+
node they have in common: rows [(ref, plus, plus_sign, sign)], row j =
|
|
886
|
+
sign * (ref + plus * plus_sign) (plus a node name, a number or None):
|
|
887
|
+
a crease tree's leaves (crease.CreaseTree.from_graph)."""
|
|
888
|
+
|
|
889
|
+
def __init__(self, g, rows):
|
|
890
|
+
gen = _Gen(g)
|
|
891
|
+
outs = []
|
|
892
|
+
for ref, plus, ps, sign in rows:
|
|
893
|
+
v = gen.val(ref, 0)
|
|
894
|
+
if plus is not None:
|
|
895
|
+
v = gen.t(f"{v} + {gen.val(plus, 0)} * {gen.c(ps)}")
|
|
896
|
+
outs.append(gen.t(f"{gen.c(sign)} * {v}"))
|
|
897
|
+
self.g = g
|
|
898
|
+
self.opaque = gen.opaque
|
|
899
|
+
self.rows = len(rows)
|
|
900
|
+
self.C = np.asarray(gen.C, np.float64)
|
|
901
|
+
tb = np.asarray(gen.tables, np.float64).reshape(-1, 2)
|
|
902
|
+
self.TX, self.TY = np.ascontiguousarray(tb[:, 0]), np.ascontiguousarray(tb[:, 1])
|
|
903
|
+
self.grids = gen.grids
|
|
904
|
+
self.kernel = _kernel(gen.source(outs))
|
|
905
|
+
|
|
906
|
+
def __call__(self, P):
|
|
907
|
+
from .lattice import _SINGLE
|
|
908
|
+
|
|
909
|
+
P = np.ascontiguousarray(np.asarray(P, np.float64).reshape(-1, 3))
|
|
910
|
+
n = len(P)
|
|
911
|
+
X = np.empty((len(self.opaque), n))
|
|
912
|
+
memo = {}
|
|
913
|
+
for j, name in enumerate(self.opaque):
|
|
914
|
+
X[j] = np.broadcast_to(np.asarray(self.g._eval(name, P, memo), float), (n,))
|
|
915
|
+
out = np.empty((self.rows, n))
|
|
916
|
+
if n:
|
|
917
|
+
self.kernel(P, self.C, self.TX, self.TY, X, bool(_SINGLE[0]), out, *self.grids)
|
|
918
|
+
return out
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def compile_rows(g, rows):
|
|
922
|
+
"""CompiledRows, or None (fieldjit off). Rows whose node does not compile
|
|
923
|
+
are read from the numpy graph, as any opaque node."""
|
|
924
|
+
if not available():
|
|
925
|
+
return None
|
|
926
|
+
try:
|
|
927
|
+
return CompiledRows(g, rows)
|
|
928
|
+
except Exception as exc: # pragma: no cover
|
|
929
|
+
g._jit_errors = getattr(g, "_jit_errors", []) + [f"rows: {type(exc).__name__}: {exc}"]
|
|
930
|
+
return None
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
class CompiledShapes:
|
|
934
|
+
"""min over (shape, sign) of sign * shape.sdf(P), compiled: the region a
|
|
935
|
+
sharp plan's extended field steers around (sharp._Extended.g)."""
|
|
936
|
+
|
|
937
|
+
def __init__(self, regions):
|
|
938
|
+
gen = _Gen(None)
|
|
939
|
+
out = None
|
|
940
|
+
for shp, sg in regions:
|
|
941
|
+
v = gen._sdf(shp, 0)
|
|
942
|
+
v = v if sg > 0 else gen.t(f"-{v}")
|
|
943
|
+
out = v if out is None else gen.t(f"_mn({out}, {v})")
|
|
944
|
+
self.C = np.asarray(gen.C, np.float64)
|
|
945
|
+
self.grids = gen.grids
|
|
946
|
+
self.kernel = _kernel(gen.source(out))
|
|
947
|
+
self._t = np.empty(0)
|
|
948
|
+
|
|
949
|
+
def __call__(self, P):
|
|
950
|
+
P = np.ascontiguousarray(np.asarray(P, np.float64).reshape(-1, 3))
|
|
951
|
+
out = np.empty(len(P))
|
|
952
|
+
if len(P):
|
|
953
|
+
self.kernel(P, self.C, self._t, self._t, np.empty((0, len(P))), False, out, *self.grids)
|
|
954
|
+
return out
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def compile_shapes(regions):
|
|
958
|
+
"""CompiledShapes for [(shape, +1 | -1)], or None when a shape does not compile."""
|
|
959
|
+
if not available() or not regions:
|
|
960
|
+
return None
|
|
961
|
+
gen = _Gen(None)
|
|
962
|
+
if not all(gen._shape_ok(shp) for shp, _ in regions):
|
|
963
|
+
return None
|
|
964
|
+
try:
|
|
965
|
+
return CompiledShapes(regions)
|
|
966
|
+
except Exception: # pragma: no cover
|
|
967
|
+
return None
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def compile_graph(g, name):
|
|
971
|
+
"""CompiledField for graph g's node `name`, or None when it would only
|
|
972
|
+
wrap the numpy graph (the node itself does not compile) or compiling
|
|
973
|
+
fails (the reason is kept in g._jit_errors)."""
|
|
974
|
+
gen = _Gen(g)
|
|
975
|
+
if not gen.supported(name):
|
|
976
|
+
return None
|
|
977
|
+
try:
|
|
978
|
+
return CompiledField(g, name)
|
|
979
|
+
except Exception as exc: # pragma: no cover - a bug here must not stop a mesh
|
|
980
|
+
g._jit_errors = getattr(g, "_jit_errors", []) + [f"{name}: {type(exc).__name__}: {exc}"]
|
|
981
|
+
return None
|