cadjoint 0.1.0__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 (250) hide show
  1. cadjoint/__init__.py +36 -0
  2. cadjoint/backends/__init__.py +15 -0
  3. cadjoint/backends/wgsl/__init__.py +29 -0
  4. cadjoint/backends/wgsl/_culling.py +326 -0
  5. cadjoint/backends/wgsl/_wgsl_emitter.py +964 -0
  6. cadjoint/backends/wgsl/codegen.py +626 -0
  7. cadjoint/backends/wgsl/direct.py +877 -0
  8. cadjoint/cache.py +229 -0
  9. cadjoint/constraints/__init__.py +104 -0
  10. cadjoint/constraints/null_space.py +143 -0
  11. cadjoint/constraints/residual.py +103 -0
  12. cadjoint/constraints/solve.py +437 -0
  13. cadjoint/constraints/types/__init__.py +0 -0
  14. cadjoint/constraints/types/_sketch.py +57 -0
  15. cadjoint/constraints/types/angle.py +84 -0
  16. cadjoint/constraints/types/base.py +80 -0
  17. cadjoint/constraints/types/coincident.py +63 -0
  18. cadjoint/constraints/types/distance.py +83 -0
  19. cadjoint/constraints/types/equal_length.py +74 -0
  20. cadjoint/constraints/types/fixed.py +62 -0
  21. cadjoint/constraints/types/horizontal.py +62 -0
  22. cadjoint/constraints/types/parallel.py +69 -0
  23. cadjoint/constraints/types/parallel_edges.py +77 -0
  24. cadjoint/constraints/types/perpendicular.py +69 -0
  25. cadjoint/constraints/types/perpendicular_edges.py +78 -0
  26. cadjoint/constraints/types/point_on_line.py +69 -0
  27. cadjoint/constraints/types/vertical.py +62 -0
  28. cadjoint/construction/__init__.py +47 -0
  29. cadjoint/construction/extrude.py +87 -0
  30. cadjoint/construction/faces.py +812 -0
  31. cadjoint/construction/loft.py +66 -0
  32. cadjoint/construction/revolve.py +53 -0
  33. cadjoint/construction/sketch.py +761 -0
  34. cadjoint/construction/solid.py +306 -0
  35. cadjoint/enums.py +556 -0
  36. cadjoint/extraction.py +103 -0
  37. cadjoint/fem/__init__.py +107 -0
  38. cadjoint/fem/backends.py +519 -0
  39. cadjoint/fem/boundary.py +220 -0
  40. cadjoint/fem/calculix.py +1354 -0
  41. cadjoint/fem/cutfem.py +1141 -0
  42. cadjoint/fem/discretization.py +189 -0
  43. cadjoint/fem/elements.py +121 -0
  44. cadjoint/fem/gmsh.py +1156 -0
  45. cadjoint/fem/hexmesh.py +532 -0
  46. cadjoint/fem/jaxfem.py +918 -0
  47. cadjoint/fem/motion.py +201 -0
  48. cadjoint/fem/postprocess.py +204 -0
  49. cadjoint/fem/properties.py +364 -0
  50. cadjoint/fem/quality.py +131 -0
  51. cadjoint/fem/render_payload.py +156 -0
  52. cadjoint/fem/result.py +260 -0
  53. cadjoint/fem/rungs.py +365 -0
  54. cadjoint/fem/simmesh.py +483 -0
  55. cadjoint/fem/simulate.py +255 -0
  56. cadjoint/fem/study.py +782 -0
  57. cadjoint/fem/tesseracts/__init__.py +1 -0
  58. cadjoint/fem/tesseracts/chain.py +851 -0
  59. cadjoint/fem/tesseracts/elastic_calculix/__init__.py +1 -0
  60. cadjoint/fem/tesseracts/elastic_calculix/tesseract_api.py +172 -0
  61. cadjoint/fem/tesseracts/elastic_calculix/tesseract_config.yaml +33 -0
  62. cadjoint/fem/tesseracts/elastic_calculix/tesseract_environment.yaml +27 -0
  63. cadjoint/fem/tesseracts/elastic_jaxfem/__init__.py +1 -0
  64. cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_api.py +275 -0
  65. cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_config.yaml +25 -0
  66. cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_environment.yaml +40 -0
  67. cadjoint/fem/tesseracts/flow_brinkman/__init__.py +1 -0
  68. cadjoint/fem/tesseracts/flow_brinkman/tesseract_api.py +249 -0
  69. cadjoint/fem/tesseracts/flow_brinkman/tesseract_config.yaml +19 -0
  70. cadjoint/fem/tesseracts/flow_brinkman/tesseract_requirements.txt +12 -0
  71. cadjoint/fem/tesseracts/mesher/__init__.py +1 -0
  72. cadjoint/fem/tesseracts/mesher/tesseract_api.py +298 -0
  73. cadjoint/fem/tesseracts/mesher/tesseract_config.yaml +18 -0
  74. cadjoint/fem/tesseracts/mesher/tesseract_requirements.txt +14 -0
  75. cadjoint/fem/tesseracts/tet_gmsh/__init__.py +1 -0
  76. cadjoint/fem/tesseracts/tet_gmsh/tesseract_api.py +286 -0
  77. cadjoint/fem/tesseracts/tet_gmsh/tesseract_config.yaml +22 -0
  78. cadjoint/fem/tesseracts/tet_gmsh/tesseract_requirements.txt +19 -0
  79. cadjoint/fem/tesseracts/tetfill/__init__.py +1 -0
  80. cadjoint/fem/tesseracts/tetfill/tesseract_api.py +287 -0
  81. cadjoint/fem/tesseracts/tetfill/tesseract_config.yaml +20 -0
  82. cadjoint/fem/tesseracts/tetfill/tesseract_requirements.txt +14 -0
  83. cadjoint/fem/tesseracts/thermal_jaxfem/__init__.py +1 -0
  84. cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_api.py +234 -0
  85. cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_config.yaml +27 -0
  86. cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_environment.yaml +40 -0
  87. cadjoint/fem/tetmesh.py +752 -0
  88. cadjoint/flow/__init__.py +168 -0
  89. cadjoint/flow/domain.py +217 -0
  90. cadjoint/flow/energy.py +697 -0
  91. cadjoint/flow/lattice.py +114 -0
  92. cadjoint/flow/lbm.py +306 -0
  93. cadjoint/flow/objectives.py +93 -0
  94. cadjoint/flow/precision.py +80 -0
  95. cadjoint/flow/regions.py +90 -0
  96. cadjoint/flow/solver.py +275 -0
  97. cadjoint/flow/steady.py +297 -0
  98. cadjoint/flow/study.py +1024 -0
  99. cadjoint/fluent.py +72 -0
  100. cadjoint/functionalize.py +496 -0
  101. cadjoint/geometry/__init__.py +32 -0
  102. cadjoint/geometry/parameters.py +287 -0
  103. cadjoint/geometry/vectors.py +37 -0
  104. cadjoint/materials.py +309 -0
  105. cadjoint/meshing/__init__.py +136 -0
  106. cadjoint/meshing/_lattice.py +30 -0
  107. cadjoint/meshing/adaptive.py +234 -0
  108. cadjoint/meshing/bounds.py +136 -0
  109. cadjoint/meshing/diagnostics.py +345 -0
  110. cadjoint/meshing/dual_contouring.py +423 -0
  111. cadjoint/meshing/edge_detection.py +395 -0
  112. cadjoint/meshing/export.py +584 -0
  113. cadjoint/meshing/features.py +506 -0
  114. cadjoint/meshing/patch_fields.py +194 -0
  115. cadjoint/meshing/simplify.py +394 -0
  116. cadjoint/optimize.py +1114 -0
  117. cadjoint/parametrization.py +231 -0
  118. cadjoint/plugins/__init__.py +155 -0
  119. cadjoint/plugins/contracts.py +571 -0
  120. cadjoint/plugins/plugin.py +637 -0
  121. cadjoint/plugins/registry.py +509 -0
  122. cadjoint/plugins/spec.py +361 -0
  123. cadjoint/render/__init__.py +33 -0
  124. cadjoint/render/_plotting.py +20 -0
  125. cadjoint/render/material.py +373 -0
  126. cadjoint/render/overlay.py +149 -0
  127. cadjoint/render/raymarch/__init__.py +51 -0
  128. cadjoint/render/raymarch/_constants.py +13 -0
  129. cadjoint/render/raymarch/camera.py +52 -0
  130. cadjoint/render/raymarch/env.py +89 -0
  131. cadjoint/render/raymarch/render.py +518 -0
  132. cadjoint/render/raymarch/shade.py +208 -0
  133. cadjoint/render/raymarch/surface.py +14 -0
  134. cadjoint/render/raymarch/trace.py +156 -0
  135. cadjoint/render/scene.py +31 -0
  136. cadjoint/render/settings.py +99 -0
  137. cadjoint/sdf/__init__.py +99 -0
  138. cadjoint/sdf/_lowering.py +466 -0
  139. cadjoint/sdf/base.py +277 -0
  140. cadjoint/sdf/boolean/__init__.py +24 -0
  141. cadjoint/sdf/boolean/base.py +163 -0
  142. cadjoint/sdf/boolean/difference.py +104 -0
  143. cadjoint/sdf/boolean/intersection.py +96 -0
  144. cadjoint/sdf/boolean/smooth.py +76 -0
  145. cadjoint/sdf/boolean/union.py +92 -0
  146. cadjoint/sdf/boolean/xor.py +57 -0
  147. cadjoint/sdf/measure/__init__.py +6 -0
  148. cadjoint/sdf/measure/mass.py +97 -0
  149. cadjoint/sdf/measure/volume.py +46 -0
  150. cadjoint/sdf/primitives/__init__.py +27 -0
  151. cadjoint/sdf/primitives/base.py +23 -0
  152. cadjoint/sdf/primitives/box.py +81 -0
  153. cadjoint/sdf/primitives/capsule.py +59 -0
  154. cadjoint/sdf/primitives/cylinder.py +77 -0
  155. cadjoint/sdf/primitives/loft.py +358 -0
  156. cadjoint/sdf/primitives/plane.py +56 -0
  157. cadjoint/sdf/primitives/polygon.py +536 -0
  158. cadjoint/sdf/primitives/round_box.py +60 -0
  159. cadjoint/sdf/primitives/sphere.py +84 -0
  160. cadjoint/sdf/primitives/torus.py +57 -0
  161. cadjoint/sdf/transforms/__init__.py +6 -0
  162. cadjoint/sdf/transforms/_operation.py +72 -0
  163. cadjoint/sdf/transforms/affine/__init__.py +7 -0
  164. cadjoint/sdf/transforms/affine/rotate.py +97 -0
  165. cadjoint/sdf/transforms/affine/scale.py +88 -0
  166. cadjoint/sdf/transforms/affine/translate.py +68 -0
  167. cadjoint/sdf/transforms/base.py +25 -0
  168. cadjoint/sdf/transforms/deformations/__init__.py +5 -0
  169. cadjoint/sdf/transforms/deformations/twist.py +87 -0
  170. cadjoint/sdf/transforms/fields.py +239 -0
  171. cadjoint/sdf/transforms/patterns.py +473 -0
  172. cadjoint/studies/__init__.py +49 -0
  173. cadjoint/studies/_validate.py +35 -0
  174. cadjoint/studies/capture.py +71 -0
  175. cadjoint/studies/selection.py +517 -0
  176. cadjoint/tier.py +311 -0
  177. cadjoint/viewer/__init__.py +16 -0
  178. cadjoint/viewer/_camera_wgsl.py +86 -0
  179. cadjoint/viewer/_edge_overlay.py +870 -0
  180. cadjoint/viewer/_example_scene.py +337 -0
  181. cadjoint/viewer/_http.py +242 -0
  182. cadjoint/viewer/_intelligence.py +722 -0
  183. cadjoint/viewer/_jobs.py +705 -0
  184. cadjoint/viewer/_limits.py +27 -0
  185. cadjoint/viewer/_overlay_refresh.py +193 -0
  186. cadjoint/viewer/_patch_requests.py +914 -0
  187. cadjoint/viewer/_pathtracer.py +656 -0
  188. cadjoint/viewer/_scenes.py +346 -0
  189. cadjoint/viewer/_webgpu.py +939 -0
  190. cadjoint/viewer/_worker_client.py +607 -0
  191. cadjoint/viewer/patch/__init__.py +173 -0
  192. cadjoint/viewer/patch/constraints.py +295 -0
  193. cadjoint/viewer/patch/edits.py +228 -0
  194. cadjoint/viewer/patch/errors.py +16 -0
  195. cadjoint/viewer/patch/format.py +140 -0
  196. cadjoint/viewer/patch/geometry.py +518 -0
  197. cadjoint/viewer/patch/materials.py +561 -0
  198. cadjoint/viewer/patch/meshes.py +186 -0
  199. cadjoint/viewer/patch/optimizations.py +92 -0
  200. cadjoint/viewer/patch/parameters.py +62 -0
  201. cadjoint/viewer/patch/resolvers.py +278 -0
  202. cadjoint/viewer/patch/scene.py +127 -0
  203. cadjoint/viewer/patch/sketch.py +553 -0
  204. cadjoint/viewer/patch/studies.py +558 -0
  205. cadjoint/viewer/playground.py +586 -0
  206. cadjoint/viewer/schema/__init__.py +90 -0
  207. cadjoint/viewer/schema/emit.py +256 -0
  208. cadjoint/viewer/schema/payloads.d.ts +873 -0
  209. cadjoint/viewer/schema/payloads.py +589 -0
  210. cadjoint/viewer/schema/requests.py +482 -0
  211. cadjoint/viewer/source_map/__init__.py +138 -0
  212. cadjoint/viewer/source_map/calls.py +178 -0
  213. cadjoint/viewer/source_map/capture.py +64 -0
  214. cadjoint/viewer/source_map/constraints.py +179 -0
  215. cadjoint/viewer/source_map/declarations.py +349 -0
  216. cadjoint/viewer/source_map/elements.py +489 -0
  217. cadjoint/viewer/source_map/features.py +216 -0
  218. cadjoint/viewer/source_map/identity.py +541 -0
  219. cadjoint/viewer/source_map/materials.py +158 -0
  220. cadjoint/viewer/source_map/nodes.py +273 -0
  221. cadjoint/viewer/source_map/parameters.py +109 -0
  222. cadjoint/viewer/source_map/payload.py +575 -0
  223. cadjoint/viewer/static/assets/index-Wg5bw0AO.js +787 -0
  224. cadjoint/viewer/static/assets/index-mniljhS-.css +1 -0
  225. cadjoint/viewer/static/index.html +14 -0
  226. cadjoint/viewer/widget.py +384 -0
  227. cadjoint/viewer/worker/__init__.py +23 -0
  228. cadjoint/viewer/worker/__main__.py +7 -0
  229. cadjoint/viewer/worker/declarations.py +208 -0
  230. cadjoint/viewer/worker/export.py +364 -0
  231. cadjoint/viewer/worker/fem.py +172 -0
  232. cadjoint/viewer/worker/main.py +417 -0
  233. cadjoint/viewer/worker/optimize.py +157 -0
  234. cadjoint/viewer/worker/payloads.py +127 -0
  235. cadjoint/viewer/worker/protocol.py +20 -0
  236. cadjoint/viewer/worker/scene.py +91 -0
  237. cadjoint/zeroset/__init__.py +14 -0
  238. cadjoint/zeroset/bounds.py +373 -0
  239. cadjoint/zeroset/evaluate.py +343 -0
  240. cadjoint/zeroset/gpu.py +389 -0
  241. cadjoint/zeroset/project.py +468 -0
  242. cadjoint/zeroset/refresh.py +141 -0
  243. cadjoint/zeroset/table.py +553 -0
  244. cadjoint/zeroset/wgsl.py +378 -0
  245. cadjoint-0.1.0.dist-info/METADATA +300 -0
  246. cadjoint-0.1.0.dist-info/RECORD +250 -0
  247. cadjoint-0.1.0.dist-info/WHEEL +5 -0
  248. cadjoint-0.1.0.dist-info/entry_points.txt +2 -0
  249. cadjoint-0.1.0.dist-info/licenses/LICENSE +202 -0
  250. cadjoint-0.1.0.dist-info/top_level.txt +1 -0
cadjoint/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """cadjoint: Differentiable CAD with SDFs and CSG."""
2
+
3
+ from cadjoint.constraints.solve import solve_constraints
4
+ from cadjoint.extraction import apply_parameters, extract_parameters
5
+ from cadjoint.functionalize import functionalize, functionalize_scene
6
+ from cadjoint.parametrization import (
7
+ compute_param_scales,
8
+ from_normalized,
9
+ normalize,
10
+ to_constrained,
11
+ to_normalized,
12
+ to_unconstrained,
13
+ unnormalize,
14
+ )
15
+ from cadjoint.render.material import Material
16
+ from cadjoint.sdf import SDF, boolean, primitives, transforms
17
+
18
+ __all__ = [
19
+ "SDF",
20
+ "Material",
21
+ "primitives",
22
+ "boolean",
23
+ "transforms",
24
+ "extract_parameters",
25
+ "apply_parameters",
26
+ "functionalize",
27
+ "functionalize_scene",
28
+ "solve_constraints",
29
+ "to_unconstrained",
30
+ "to_constrained",
31
+ "compute_param_scales",
32
+ "normalize",
33
+ "unnormalize",
34
+ "to_normalized",
35
+ "from_normalized",
36
+ ]
@@ -0,0 +1,15 @@
1
+ """Shader code generation: compile JAX SDF functions to WGSL."""
2
+
3
+ from .wgsl import (
4
+ MATERIAL_BASE_ENTRY_POINT,
5
+ MATERIAL_OPTICS_ENTRY_POINT,
6
+ compile_scene_to_wgsl,
7
+ compile_sdf_to_wgsl,
8
+ )
9
+
10
+ __all__ = [
11
+ "MATERIAL_BASE_ENTRY_POINT",
12
+ "MATERIAL_OPTICS_ENTRY_POINT",
13
+ "compile_scene_to_wgsl",
14
+ "compile_sdf_to_wgsl",
15
+ ]
@@ -0,0 +1,29 @@
1
+ from .codegen import (
2
+ CULL_DISABLED_MARGIN,
3
+ DEFAULT_PARAMETER_BINDING,
4
+ DEFAULT_PARAMETER_GROUP,
5
+ MATERIAL_BASE_ENTRY_POINT,
6
+ MATERIAL_OPTICS_ENTRY_POINT,
7
+ PARAMETER_SLOT_BYTES,
8
+ RESERVED_PARAMETER_SLOTS,
9
+ ShaderParameter,
10
+ ShaderProgram,
11
+ compile_scene_to_wgsl,
12
+ compile_scene_with_uniforms,
13
+ compile_sdf_to_wgsl,
14
+ )
15
+
16
+ __all__ = [
17
+ "CULL_DISABLED_MARGIN",
18
+ "DEFAULT_PARAMETER_BINDING",
19
+ "DEFAULT_PARAMETER_GROUP",
20
+ "MATERIAL_BASE_ENTRY_POINT",
21
+ "MATERIAL_OPTICS_ENTRY_POINT",
22
+ "PARAMETER_SLOT_BYTES",
23
+ "RESERVED_PARAMETER_SLOTS",
24
+ "ShaderParameter",
25
+ "ShaderProgram",
26
+ "compile_scene_to_wgsl",
27
+ "compile_scene_with_uniforms",
28
+ "compile_sdf_to_wgsl",
29
+ ]
@@ -0,0 +1,326 @@
1
+ """The scene's distance field with every far-away node skipped.
2
+
3
+ :func:`cadjoint.functionalize.functionalize_scene` lowers the tree faithfully:
4
+ a union evaluates every operand at every query point, and a sphere trace then
5
+ pays for all forty leaves of a part at every step of every ray. Almost all of
6
+ that is wasted — at any point most leaves are far away and lose the ``min``
7
+ by a wide margin — but a ``min`` over a value is not a skipped value. This
8
+ module traces the same tree with one addition: before a boolean evaluates an
9
+ operand, it compares the distance to the operand's bounding box (see
10
+ :mod:`cadjoint.sdf._lowering`) against what it already holds, and when the
11
+ operand cannot change the result it is not evaluated at all. ``lax.cond``
12
+ lowers to a ``stablehlo.case``, which the WGSL emitter turns into a real
13
+ ``if``, so the skipped branch costs nothing on the GPU.
14
+
15
+ The result is not an approximation. Each skip is taken only when the exact
16
+ value is provably what the running result already is:
17
+
18
+ * **Smooth union.** ``smin(m, d) = min(m, d) - h²/(4K)`` with
19
+ ``h = max(K - |m - d|, 0)`` and ``K`` the scaled band. When ``d >= m + K``
20
+ the band term is exactly zero and ``smin(m, d) = m``. The box gives
21
+ ``d >= box(p)``, so ``box(p) >= m + K`` suffices.
22
+ * **Smooth difference.** ``smax(m, -d) = -smin(-m, d)``, which is ``m`` when
23
+ ``d >= K - m``; again ``box(p) >= K - m`` suffices. Outside the body
24
+ ``m > 0`` and the condition is nearly always met, so every hole of a part
25
+ is skipped for every exterior ray step that is not inside the hole's box.
26
+ * **Pattern instance.** A pattern is a plain ``min``; ``min(m, d) = m`` when
27
+ ``d >= m``.
28
+
29
+ Each test also requires ``box(p) > 0`` — the point is outside the box — which
30
+ is where the lower bound holds, and adds :data:`CULL_MARGIN` so that float
31
+ rounding in the box distance can never flip a test the exact arithmetic
32
+ would not. ``tests/backends/test_wgsl_culling.py`` checks the two forms
33
+ agree to the last bit on random points in every shipped scene.
34
+
35
+ Intersections and XORs are not culled: a lower bound on an operand cannot
36
+ show that the *maximum* is unchanged. Their operands are still culled
37
+ inside, where they are unions.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import math
43
+ from typing import Callable
44
+
45
+ import jax
46
+ import jax.numpy as jnp
47
+
48
+ from cadjoint.functionalize import _collect, _outlined, _repeated_nodes, _static_params
49
+ from cadjoint.sdf._lowering import (
50
+ Bounds,
51
+ box_distance,
52
+ is_scalar_lowering,
53
+ node_bounds,
54
+ rotation_rows,
55
+ )
56
+ from cadjoint.sdf._lowering import (
57
+ _apply_rows as apply_rows,
58
+ )
59
+
60
+ #: World-unit slack on every skip test. A bound violated by rounding is on
61
+ #: the order of 1e-7 of the scene's extent; this is three orders above it.
62
+ CULL_MARGIN = 1e-4
63
+
64
+
65
+ def _skip(condition, held, evaluate: Callable):
66
+ """``held`` if ``condition``, else ``evaluate(held)`` — as a real branch."""
67
+ return jax.lax.cond(condition, lambda m: m, evaluate, held)
68
+
69
+
70
+ def culled_scene_sdf(geometry, *, margin: float = CULL_MARGIN) -> Callable:
71
+ """Compile a scene's distance field with bounding-box culling.
72
+
73
+ Mirrors :func:`cadjoint.functionalize.functionalize_scene` node for node
74
+ — the same DFS numbering, so path-keyed fixed parameters line up; the
75
+ same sharing and outlining — and returns only the distance half of it.
76
+ Every node type is evaluated through its own static ``sdf`` exactly as
77
+ there; unions, differences and the two patterns are the exception,
78
+ re-spelled with a ``lax.cond`` around each operand.
79
+
80
+ Args:
81
+ geometry: Root SDF node.
82
+ margin: Slack added to every skip test, in world units. A *traced*
83
+ value here is what lets the viewer switch culling off without a
84
+ recompile: every test is ``box_distance(p, bounds) >= threshold +
85
+ margin``, so an infinite margin makes all of them false and the
86
+ module computes the flat field (see
87
+ :data:`cadjoint.backends.wgsl.CULL_DISABLED_MARGIN`).
88
+
89
+ Returns:
90
+ ``(free_params, fixed_params, margin=None) -> (point -> distance)``.
91
+ The optional third argument replaces the build-time margin, which
92
+ is how a traced one gets in: the skip tests read it late, at trace
93
+ time, so binding it here reaches every one of them.
94
+ """
95
+ # One cell, read by every skip test at trace time, so `bound` can replace
96
+ # the margin after the tree is built.
97
+ nonlocal_margin = [margin]
98
+
99
+ from cadjoint.render.material import Material
100
+ from cadjoint.sdf.boolean.base import BooleanOp
101
+ from cadjoint.sdf.boolean.difference import Difference
102
+ from cadjoint.sdf.boolean.smooth import smooth_max, smooth_min
103
+ from cadjoint.sdf.boolean.union import Union
104
+ from cadjoint.sdf.primitives.base import Primitive
105
+ from cadjoint.sdf.transforms.patterns import (
106
+ LinearPattern,
107
+ PolarPattern,
108
+ _kept_instances,
109
+ _rotate_about,
110
+ )
111
+
112
+ node_counter = {"count": 0}
113
+ repeated = _repeated_nodes(geometry)
114
+ memo: dict[int, tuple] = {}
115
+
116
+ def skip(obj) -> None:
117
+ node_counter["count"] += 1
118
+ for child in obj.children():
119
+ skip(child)
120
+
121
+ def build(obj):
122
+ """DFS builder — returns ``(sdf_eval | None, bounds_fn | None)``.
123
+
124
+ ``sdf_eval(p, free, fixed) -> distance``;
125
+ ``bounds_fn(free, fixed) -> Bounds | None`` in the node's own frame.
126
+ """
127
+ if id(obj) in memo:
128
+ skip(obj)
129
+ return memo[id(obj)]
130
+
131
+ node_id = f"{obj.__class__.__name__.lower()}_{node_counter['count']}"
132
+ node_counter["count"] += 1
133
+ ps = obj.params
134
+ static = _static_params(obj)
135
+
136
+ if not hasattr(obj.__class__, "sdf") or isinstance(obj, Material):
137
+ for child in obj.children():
138
+ build(child)
139
+ memo[id(obj)] = (None, None)
140
+ return memo[id(obj)]
141
+
142
+ pure_sdf = obj.__class__.sdf
143
+ child_results = [build(child) for child in obj.children()]
144
+ sdf_children = [(s, b) for s, b in child_results if s is not None]
145
+ outline = id(obj) in repeated
146
+
147
+ def collect(free, fixed, _nid=node_id, _ps=ps, _static=static):
148
+ return _collect(_nid, _ps, free, fixed, _static)
149
+
150
+ def bounds_fn(free, fixed, _obj=obj, _ch=sdf_children):
151
+ values = collect(free, fixed)
152
+ return node_bounds(_obj, values, [b(free, fixed) if b else None for _, b in _ch])
153
+
154
+ # ── primitive ─────────────────────────────────────────────────────
155
+ if isinstance(obj, Primitive):
156
+
157
+ def sdf_eval(p, free, fixed, _fn=pure_sdf):
158
+ return _fn(p, **collect(free, fixed))
159
+
160
+ memo[id(obj)] = (_outlined(sdf_eval, outline), bounds_fn)
161
+ return memo[id(obj)]
162
+
163
+ # ── union / difference: one branch per operand ────────────────────
164
+ if isinstance(obj, (Union, Difference)):
165
+ is_union = isinstance(obj, Union)
166
+
167
+ def sdf_eval(p, free, fixed, _ch=sdf_children, _union=is_union):
168
+ values = collect(free, fixed)
169
+ smoothness = values["smoothness"]
170
+ band = jnp.maximum(smoothness * 4.0, 1e-10)
171
+ result = _ch[0][0](p, free, fixed)
172
+ for child_sdf, child_bounds in _ch[1:]:
173
+ bounds = child_bounds(free, fixed) if child_bounds else None
174
+
175
+ if _union:
176
+
177
+ def combine(m, _s=child_sdf):
178
+ return smooth_min(m, _s(p, free, fixed), smoothness)
179
+
180
+ threshold = jnp.maximum(result + band, 0.0) + nonlocal_margin[0]
181
+ else:
182
+
183
+ def combine(m, _s=child_sdf):
184
+ d = _s(p, free, fixed)
185
+ return jnp.where(
186
+ smoothness > 0,
187
+ smooth_max(m, -d, smoothness),
188
+ jnp.maximum(m, -d),
189
+ )
190
+
191
+ threshold = jnp.maximum(band - result, 0.0) + nonlocal_margin[0]
192
+
193
+ if bounds is None:
194
+ result = combine(result)
195
+ else:
196
+ result = _skip(box_distance(p, bounds) >= threshold, result, combine)
197
+ return result
198
+
199
+ memo[id(obj)] = (_outlined(sdf_eval, outline), bounds_fn)
200
+ return memo[id(obj)]
201
+
202
+ # ── other booleans: verbatim ──────────────────────────────────────
203
+ if isinstance(obj, BooleanOp):
204
+
205
+ def sdf_eval(p, free, fixed, _fn=pure_sdf, _ch=sdf_children):
206
+ evals = tuple(lambda p_, s=s: s(p_, free, fixed) for s, _ in _ch)
207
+ return _fn(evals, p, **collect(free, fixed))
208
+
209
+ memo[id(obj)] = (_outlined(sdf_eval, outline), bounds_fn)
210
+ return memo[id(obj)]
211
+
212
+ assert len(sdf_children) == 1, f"Transform {obj.__class__.__name__} must have 1 SDF child"
213
+ child_sdf, child_bounds = sdf_children[0]
214
+
215
+ # ── patterns: one branch per instance (the shader's unrolled form) ─
216
+ if isinstance(obj, (PolarPattern, LinearPattern)):
217
+ is_polar = isinstance(obj, PolarPattern)
218
+
219
+ def sdf_eval(p, free, fixed, _fn=pure_sdf, _polar=is_polar):
220
+ values = collect(free, fixed)
221
+ child = lambda q: child_sdf(q, free, fixed) # noqa: E731
222
+ count = int(values["count"])
223
+ bounds = child_bounds(free, fixed) if child_bounds else None
224
+ # A pattern may leave instances out — the row of holes a part
225
+ # interrupts where something else passes through. The flat
226
+ # field emits `kept`, so this has to emit exactly `kept` too;
227
+ # unrolling `range(1, count)` instead puts back the geometry
228
+ # the scene asked to omit.
229
+ kept = _kept_instances(count, values["skip_mask"])
230
+ if not is_scalar_lowering() or len(kept) == 1 or bounds is None:
231
+ return _fn(child, p, **values)
232
+ result = child(p)
233
+ if _polar:
234
+ origin, direction = values["origin"], values["direction"]
235
+ for i in kept[1:]:
236
+ theta = 2.0 * math.pi * i / count
237
+ rows = rotation_rows(direction, theta)
238
+ instance = Bounds(
239
+ center=origin + apply_rows(rows, bounds.center - origin),
240
+ half=apply_rows([jnp.abs(row) for row in rows], bounds.half),
241
+ scale=bounds.scale,
242
+ )
243
+
244
+ def combine(m, _theta=theta):
245
+ return jnp.minimum(
246
+ m, child(_rotate_about(p, origin, direction, -_theta))
247
+ )
248
+
249
+ threshold = jnp.maximum(result, 0.0) + nonlocal_margin[0]
250
+ result = _skip(box_distance(p, instance) >= threshold, result, combine)
251
+ return result
252
+ direction, spacing = values["direction"], values["spacing"]
253
+ axis = direction / jnp.linalg.norm(direction)
254
+ for i in kept[1:]:
255
+ shift = axis * (spacing * i)
256
+ instance = Bounds(
257
+ center=bounds.center + shift, half=bounds.half, scale=bounds.scale
258
+ )
259
+
260
+ def combine(m, _shift=shift):
261
+ return jnp.minimum(m, child(p - _shift))
262
+
263
+ threshold = jnp.maximum(result, 0.0) + nonlocal_margin[0]
264
+ result = _skip(box_distance(p, instance) >= threshold, result, combine)
265
+ return result
266
+
267
+ memo[id(obj)] = (_outlined(sdf_eval, outline), bounds_fn)
268
+ return memo[id(obj)]
269
+
270
+ # ── transforms and field operations: verbatim ─────────────────────
271
+ def sdf_eval(p, free, fixed, _fn=pure_sdf):
272
+ return _fn(lambda q: child_sdf(q, free, fixed), p, **collect(free, fixed))
273
+
274
+ memo[id(obj)] = (_outlined(sdf_eval, outline), bounds_fn)
275
+ return memo[id(obj)]
276
+
277
+ inner_sdf, _ = build(geometry)
278
+ if inner_sdf is None:
279
+ raise ValueError("The scene root is not an SDF node")
280
+
281
+ def bound(free, fixed, margin=None):
282
+ # Every `threshold = ... + margin` above is evaluated when the tree is
283
+ # traced, not when it is built, and Python closures read their
284
+ # enclosing scope late — so rebinding the name here reaches all of
285
+ # them without threading a fourth argument through the outlining.
286
+ if margin is not None:
287
+ nonlocal_margin[0] = margin
288
+ return lambda p: inner_sdf(p, free, fixed)
289
+
290
+ return bound
291
+
292
+
293
+ def scene_bounds(geometry, free: dict, fixed: dict) -> Bounds | None:
294
+ """The whole scene's bounds in world space, for reporting and tests."""
295
+ from cadjoint.render.material import Material
296
+
297
+ node_counter = {"count": 0}
298
+ memo: dict[int, Bounds | None] = {}
299
+
300
+ def skip(obj) -> None:
301
+ node_counter["count"] += 1
302
+ for child in obj.children():
303
+ skip(child)
304
+
305
+ def walk(obj):
306
+ if id(obj) in memo:
307
+ skip(obj)
308
+ return memo[id(obj)]
309
+ node_id = f"{obj.__class__.__name__.lower()}_{node_counter['count']}"
310
+ node_counter["count"] += 1
311
+ if not hasattr(obj.__class__, "sdf") or isinstance(obj, Material):
312
+ for child in obj.children():
313
+ walk(child)
314
+ memo[id(obj)] = None
315
+ return None
316
+ children = []
317
+ for child in obj.children():
318
+ bounds = walk(child)
319
+ # Material children advance the counter too, exactly as build() does.
320
+ if hasattr(child.__class__, "sdf"):
321
+ children.append(bounds)
322
+ values = _collect(node_id, obj.params, free, fixed, _static_params(obj))
323
+ memo[id(obj)] = node_bounds(obj, values, children)
324
+ return memo[id(obj)]
325
+
326
+ return walk(geometry)