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.
- cadjoint/__init__.py +36 -0
- cadjoint/backends/__init__.py +15 -0
- cadjoint/backends/wgsl/__init__.py +29 -0
- cadjoint/backends/wgsl/_culling.py +326 -0
- cadjoint/backends/wgsl/_wgsl_emitter.py +964 -0
- cadjoint/backends/wgsl/codegen.py +626 -0
- cadjoint/backends/wgsl/direct.py +877 -0
- cadjoint/cache.py +229 -0
- cadjoint/constraints/__init__.py +104 -0
- cadjoint/constraints/null_space.py +143 -0
- cadjoint/constraints/residual.py +103 -0
- cadjoint/constraints/solve.py +437 -0
- cadjoint/constraints/types/__init__.py +0 -0
- cadjoint/constraints/types/_sketch.py +57 -0
- cadjoint/constraints/types/angle.py +84 -0
- cadjoint/constraints/types/base.py +80 -0
- cadjoint/constraints/types/coincident.py +63 -0
- cadjoint/constraints/types/distance.py +83 -0
- cadjoint/constraints/types/equal_length.py +74 -0
- cadjoint/constraints/types/fixed.py +62 -0
- cadjoint/constraints/types/horizontal.py +62 -0
- cadjoint/constraints/types/parallel.py +69 -0
- cadjoint/constraints/types/parallel_edges.py +77 -0
- cadjoint/constraints/types/perpendicular.py +69 -0
- cadjoint/constraints/types/perpendicular_edges.py +78 -0
- cadjoint/constraints/types/point_on_line.py +69 -0
- cadjoint/constraints/types/vertical.py +62 -0
- cadjoint/construction/__init__.py +47 -0
- cadjoint/construction/extrude.py +87 -0
- cadjoint/construction/faces.py +812 -0
- cadjoint/construction/loft.py +66 -0
- cadjoint/construction/revolve.py +53 -0
- cadjoint/construction/sketch.py +761 -0
- cadjoint/construction/solid.py +306 -0
- cadjoint/enums.py +556 -0
- cadjoint/extraction.py +103 -0
- cadjoint/fem/__init__.py +107 -0
- cadjoint/fem/backends.py +519 -0
- cadjoint/fem/boundary.py +220 -0
- cadjoint/fem/calculix.py +1354 -0
- cadjoint/fem/cutfem.py +1141 -0
- cadjoint/fem/discretization.py +189 -0
- cadjoint/fem/elements.py +121 -0
- cadjoint/fem/gmsh.py +1156 -0
- cadjoint/fem/hexmesh.py +532 -0
- cadjoint/fem/jaxfem.py +918 -0
- cadjoint/fem/motion.py +201 -0
- cadjoint/fem/postprocess.py +204 -0
- cadjoint/fem/properties.py +364 -0
- cadjoint/fem/quality.py +131 -0
- cadjoint/fem/render_payload.py +156 -0
- cadjoint/fem/result.py +260 -0
- cadjoint/fem/rungs.py +365 -0
- cadjoint/fem/simmesh.py +483 -0
- cadjoint/fem/simulate.py +255 -0
- cadjoint/fem/study.py +782 -0
- cadjoint/fem/tesseracts/__init__.py +1 -0
- cadjoint/fem/tesseracts/chain.py +851 -0
- cadjoint/fem/tesseracts/elastic_calculix/__init__.py +1 -0
- cadjoint/fem/tesseracts/elastic_calculix/tesseract_api.py +172 -0
- cadjoint/fem/tesseracts/elastic_calculix/tesseract_config.yaml +33 -0
- cadjoint/fem/tesseracts/elastic_calculix/tesseract_environment.yaml +27 -0
- cadjoint/fem/tesseracts/elastic_jaxfem/__init__.py +1 -0
- cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_api.py +275 -0
- cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_config.yaml +25 -0
- cadjoint/fem/tesseracts/elastic_jaxfem/tesseract_environment.yaml +40 -0
- cadjoint/fem/tesseracts/flow_brinkman/__init__.py +1 -0
- cadjoint/fem/tesseracts/flow_brinkman/tesseract_api.py +249 -0
- cadjoint/fem/tesseracts/flow_brinkman/tesseract_config.yaml +19 -0
- cadjoint/fem/tesseracts/flow_brinkman/tesseract_requirements.txt +12 -0
- cadjoint/fem/tesseracts/mesher/__init__.py +1 -0
- cadjoint/fem/tesseracts/mesher/tesseract_api.py +298 -0
- cadjoint/fem/tesseracts/mesher/tesseract_config.yaml +18 -0
- cadjoint/fem/tesseracts/mesher/tesseract_requirements.txt +14 -0
- cadjoint/fem/tesseracts/tet_gmsh/__init__.py +1 -0
- cadjoint/fem/tesseracts/tet_gmsh/tesseract_api.py +286 -0
- cadjoint/fem/tesseracts/tet_gmsh/tesseract_config.yaml +22 -0
- cadjoint/fem/tesseracts/tet_gmsh/tesseract_requirements.txt +19 -0
- cadjoint/fem/tesseracts/tetfill/__init__.py +1 -0
- cadjoint/fem/tesseracts/tetfill/tesseract_api.py +287 -0
- cadjoint/fem/tesseracts/tetfill/tesseract_config.yaml +20 -0
- cadjoint/fem/tesseracts/tetfill/tesseract_requirements.txt +14 -0
- cadjoint/fem/tesseracts/thermal_jaxfem/__init__.py +1 -0
- cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_api.py +234 -0
- cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_config.yaml +27 -0
- cadjoint/fem/tesseracts/thermal_jaxfem/tesseract_environment.yaml +40 -0
- cadjoint/fem/tetmesh.py +752 -0
- cadjoint/flow/__init__.py +168 -0
- cadjoint/flow/domain.py +217 -0
- cadjoint/flow/energy.py +697 -0
- cadjoint/flow/lattice.py +114 -0
- cadjoint/flow/lbm.py +306 -0
- cadjoint/flow/objectives.py +93 -0
- cadjoint/flow/precision.py +80 -0
- cadjoint/flow/regions.py +90 -0
- cadjoint/flow/solver.py +275 -0
- cadjoint/flow/steady.py +297 -0
- cadjoint/flow/study.py +1024 -0
- cadjoint/fluent.py +72 -0
- cadjoint/functionalize.py +496 -0
- cadjoint/geometry/__init__.py +32 -0
- cadjoint/geometry/parameters.py +287 -0
- cadjoint/geometry/vectors.py +37 -0
- cadjoint/materials.py +309 -0
- cadjoint/meshing/__init__.py +136 -0
- cadjoint/meshing/_lattice.py +30 -0
- cadjoint/meshing/adaptive.py +234 -0
- cadjoint/meshing/bounds.py +136 -0
- cadjoint/meshing/diagnostics.py +345 -0
- cadjoint/meshing/dual_contouring.py +423 -0
- cadjoint/meshing/edge_detection.py +395 -0
- cadjoint/meshing/export.py +584 -0
- cadjoint/meshing/features.py +506 -0
- cadjoint/meshing/patch_fields.py +194 -0
- cadjoint/meshing/simplify.py +394 -0
- cadjoint/optimize.py +1114 -0
- cadjoint/parametrization.py +231 -0
- cadjoint/plugins/__init__.py +155 -0
- cadjoint/plugins/contracts.py +571 -0
- cadjoint/plugins/plugin.py +637 -0
- cadjoint/plugins/registry.py +509 -0
- cadjoint/plugins/spec.py +361 -0
- cadjoint/render/__init__.py +33 -0
- cadjoint/render/_plotting.py +20 -0
- cadjoint/render/material.py +373 -0
- cadjoint/render/overlay.py +149 -0
- cadjoint/render/raymarch/__init__.py +51 -0
- cadjoint/render/raymarch/_constants.py +13 -0
- cadjoint/render/raymarch/camera.py +52 -0
- cadjoint/render/raymarch/env.py +89 -0
- cadjoint/render/raymarch/render.py +518 -0
- cadjoint/render/raymarch/shade.py +208 -0
- cadjoint/render/raymarch/surface.py +14 -0
- cadjoint/render/raymarch/trace.py +156 -0
- cadjoint/render/scene.py +31 -0
- cadjoint/render/settings.py +99 -0
- cadjoint/sdf/__init__.py +99 -0
- cadjoint/sdf/_lowering.py +466 -0
- cadjoint/sdf/base.py +277 -0
- cadjoint/sdf/boolean/__init__.py +24 -0
- cadjoint/sdf/boolean/base.py +163 -0
- cadjoint/sdf/boolean/difference.py +104 -0
- cadjoint/sdf/boolean/intersection.py +96 -0
- cadjoint/sdf/boolean/smooth.py +76 -0
- cadjoint/sdf/boolean/union.py +92 -0
- cadjoint/sdf/boolean/xor.py +57 -0
- cadjoint/sdf/measure/__init__.py +6 -0
- cadjoint/sdf/measure/mass.py +97 -0
- cadjoint/sdf/measure/volume.py +46 -0
- cadjoint/sdf/primitives/__init__.py +27 -0
- cadjoint/sdf/primitives/base.py +23 -0
- cadjoint/sdf/primitives/box.py +81 -0
- cadjoint/sdf/primitives/capsule.py +59 -0
- cadjoint/sdf/primitives/cylinder.py +77 -0
- cadjoint/sdf/primitives/loft.py +358 -0
- cadjoint/sdf/primitives/plane.py +56 -0
- cadjoint/sdf/primitives/polygon.py +536 -0
- cadjoint/sdf/primitives/round_box.py +60 -0
- cadjoint/sdf/primitives/sphere.py +84 -0
- cadjoint/sdf/primitives/torus.py +57 -0
- cadjoint/sdf/transforms/__init__.py +6 -0
- cadjoint/sdf/transforms/_operation.py +72 -0
- cadjoint/sdf/transforms/affine/__init__.py +7 -0
- cadjoint/sdf/transforms/affine/rotate.py +97 -0
- cadjoint/sdf/transforms/affine/scale.py +88 -0
- cadjoint/sdf/transforms/affine/translate.py +68 -0
- cadjoint/sdf/transforms/base.py +25 -0
- cadjoint/sdf/transforms/deformations/__init__.py +5 -0
- cadjoint/sdf/transforms/deformations/twist.py +87 -0
- cadjoint/sdf/transforms/fields.py +239 -0
- cadjoint/sdf/transforms/patterns.py +473 -0
- cadjoint/studies/__init__.py +49 -0
- cadjoint/studies/_validate.py +35 -0
- cadjoint/studies/capture.py +71 -0
- cadjoint/studies/selection.py +517 -0
- cadjoint/tier.py +311 -0
- cadjoint/viewer/__init__.py +16 -0
- cadjoint/viewer/_camera_wgsl.py +86 -0
- cadjoint/viewer/_edge_overlay.py +870 -0
- cadjoint/viewer/_example_scene.py +337 -0
- cadjoint/viewer/_http.py +242 -0
- cadjoint/viewer/_intelligence.py +722 -0
- cadjoint/viewer/_jobs.py +705 -0
- cadjoint/viewer/_limits.py +27 -0
- cadjoint/viewer/_overlay_refresh.py +193 -0
- cadjoint/viewer/_patch_requests.py +914 -0
- cadjoint/viewer/_pathtracer.py +656 -0
- cadjoint/viewer/_scenes.py +346 -0
- cadjoint/viewer/_webgpu.py +939 -0
- cadjoint/viewer/_worker_client.py +607 -0
- cadjoint/viewer/patch/__init__.py +173 -0
- cadjoint/viewer/patch/constraints.py +295 -0
- cadjoint/viewer/patch/edits.py +228 -0
- cadjoint/viewer/patch/errors.py +16 -0
- cadjoint/viewer/patch/format.py +140 -0
- cadjoint/viewer/patch/geometry.py +518 -0
- cadjoint/viewer/patch/materials.py +561 -0
- cadjoint/viewer/patch/meshes.py +186 -0
- cadjoint/viewer/patch/optimizations.py +92 -0
- cadjoint/viewer/patch/parameters.py +62 -0
- cadjoint/viewer/patch/resolvers.py +278 -0
- cadjoint/viewer/patch/scene.py +127 -0
- cadjoint/viewer/patch/sketch.py +553 -0
- cadjoint/viewer/patch/studies.py +558 -0
- cadjoint/viewer/playground.py +586 -0
- cadjoint/viewer/schema/__init__.py +90 -0
- cadjoint/viewer/schema/emit.py +256 -0
- cadjoint/viewer/schema/payloads.d.ts +873 -0
- cadjoint/viewer/schema/payloads.py +589 -0
- cadjoint/viewer/schema/requests.py +482 -0
- cadjoint/viewer/source_map/__init__.py +138 -0
- cadjoint/viewer/source_map/calls.py +178 -0
- cadjoint/viewer/source_map/capture.py +64 -0
- cadjoint/viewer/source_map/constraints.py +179 -0
- cadjoint/viewer/source_map/declarations.py +349 -0
- cadjoint/viewer/source_map/elements.py +489 -0
- cadjoint/viewer/source_map/features.py +216 -0
- cadjoint/viewer/source_map/identity.py +541 -0
- cadjoint/viewer/source_map/materials.py +158 -0
- cadjoint/viewer/source_map/nodes.py +273 -0
- cadjoint/viewer/source_map/parameters.py +109 -0
- cadjoint/viewer/source_map/payload.py +575 -0
- cadjoint/viewer/static/assets/index-Wg5bw0AO.js +787 -0
- cadjoint/viewer/static/assets/index-mniljhS-.css +1 -0
- cadjoint/viewer/static/index.html +14 -0
- cadjoint/viewer/widget.py +384 -0
- cadjoint/viewer/worker/__init__.py +23 -0
- cadjoint/viewer/worker/__main__.py +7 -0
- cadjoint/viewer/worker/declarations.py +208 -0
- cadjoint/viewer/worker/export.py +364 -0
- cadjoint/viewer/worker/fem.py +172 -0
- cadjoint/viewer/worker/main.py +417 -0
- cadjoint/viewer/worker/optimize.py +157 -0
- cadjoint/viewer/worker/payloads.py +127 -0
- cadjoint/viewer/worker/protocol.py +20 -0
- cadjoint/viewer/worker/scene.py +91 -0
- cadjoint/zeroset/__init__.py +14 -0
- cadjoint/zeroset/bounds.py +373 -0
- cadjoint/zeroset/evaluate.py +343 -0
- cadjoint/zeroset/gpu.py +389 -0
- cadjoint/zeroset/project.py +468 -0
- cadjoint/zeroset/refresh.py +141 -0
- cadjoint/zeroset/table.py +553 -0
- cadjoint/zeroset/wgsl.py +378 -0
- cadjoint-0.1.0.dist-info/METADATA +300 -0
- cadjoint-0.1.0.dist-info/RECORD +250 -0
- cadjoint-0.1.0.dist-info/WHEEL +5 -0
- cadjoint-0.1.0.dist-info/entry_points.txt +2 -0
- cadjoint-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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)
|