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/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