DeepSDFStruct 1.7.2__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.
- DeepSDFStruct/SDF.py +1734 -0
- DeepSDFStruct/__init__.py +66 -0
- DeepSDFStruct/deep_sdf/__init__.py +69 -0
- DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
- DeepSDFStruct/deep_sdf/data.py +258 -0
- DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
- DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
- DeepSDFStruct/deep_sdf/models.py +197 -0
- DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
- DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
- DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
- DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
- DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
- DeepSDFStruct/deep_sdf/plotting.py +143 -0
- DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
- DeepSDFStruct/deep_sdf/training.py +840 -0
- DeepSDFStruct/deep_sdf/workspace.py +363 -0
- DeepSDFStruct/design_of_experiments.py +235 -0
- DeepSDFStruct/flexicubes/__init__.py +15 -0
- DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
- DeepSDFStruct/flexicubes/tables.py +2338 -0
- DeepSDFStruct/flexisquares/__init__.py +15 -0
- DeepSDFStruct/flexisquares/flexisquares.py +967 -0
- DeepSDFStruct/flexisquares/tables.py +63 -0
- DeepSDFStruct/lattice_structure.py +254 -0
- DeepSDFStruct/local_shapes.py +226 -0
- DeepSDFStruct/mesh.py +1184 -0
- DeepSDFStruct/optimization.py +357 -0
- DeepSDFStruct/parametrization.py +215 -0
- DeepSDFStruct/pretrained_models.py +128 -0
- DeepSDFStruct/sampling.py +670 -0
- DeepSDFStruct/sdf_operations.py +990 -0
- DeepSDFStruct/sdf_primitives.py +1416 -0
- DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
- DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
- DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
- DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
- DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
- DeepSDFStruct/torch_spline.py +446 -0
- DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
- DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/specs.json +46 -0
- DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
- DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
- DeepSDFStruct/utils.py +96 -0
- DeepSDFStruct/visualization/latent_viewer.py +211 -0
- DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
- benchmarks/generate_sdf_showcase.py +622 -0
- benchmarks/sdf_from_mesh_benchmark.py +52 -0
- benchmarks/sdf_from_mesh_comparison.md +33 -0
- benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
- deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
- deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
- deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
- deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
- deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
- deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
- deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
- deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
- docs/Makefile +20 -0
- docs/api_reference.rst +6 -0
- docs/conf.py +60 -0
- docs/index.rst +136 -0
- docs/make.bat +35 -0
- docs/qr_code_github.png +0 -0
- docs/qr_code_paper.png +0 -0
- docs/readme_images/example_output_01.png +0 -0
- docs/readme_images/example_output_02.png +0 -0
- docs/readme_images/example_output_03.png +0 -0
- docs/readme_images/example_output_04.png +0 -0
- tests/data/chairs/1005.obj +282 -0
- tests/data/chairs/1024.obj +222 -0
- tests/data/chairs/README.md +1 -0
- tests/data/circular_balls_test_case.stl +0 -0
- tests/data/cone.stl +1794 -0
- tests/data/example_disconnectd_mesh.inp +1784 -0
- tests/data/example_line_mesh.vtk +0 -0
- tests/data/stanford_bunny.stl +0 -0
- tests/data/sweep_test_case.stl +0 -0
- tests/test_DOE.py +49 -0
- tests/test_elongate_sdf.py +195 -0
- tests/test_flexisquares.py +187 -0
- tests/test_generate_dataset.py +76 -0
- tests/test_lattice_evaluation.py +137 -0
- tests/test_mesh_export.py +137 -0
- tests/test_mesh_functions.py +49 -0
- tests/test_networks.py +71 -0
- tests/test_pretrained_models.py +40 -0
- tests/test_reconstruction.py +101 -0
- tests/test_sdf_from_mesh.py +52 -0
- tests/test_sdf_functions.py +117 -0
- tests/test_sdf_primitives.py +668 -0
- tests/test_sdf_trimesh_comparison.py +482 -0
- tests/test_splinepy_unitcells.py +64 -0
- tests/test_structural_optimization.py +178 -0
- tests/test_torch_spline.py +216 -0
- tests/test_train_model.py +110 -0
- tests/test_training_data_ids.py +41 -0
- tests/tmp_outputs/temp_file.txt +0 -0
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDF Operations and Transformations
|
|
3
|
+
==================================
|
|
4
|
+
|
|
5
|
+
This module provides various operations that can be applied to SDFs,
|
|
6
|
+
including space warping transformations like elongation, twisting,
|
|
7
|
+
bending, dilation/erosion, and shell creation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import numpy as np
|
|
12
|
+
from DeepSDFStruct.SDF import SDFBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ElongateSDF(SDFBase):
|
|
16
|
+
"""Elongate an SDF by adding material along the coordinate axes.
|
|
17
|
+
|
|
18
|
+
Extends the geometry by adding material in the positive and negative
|
|
19
|
+
directions along each axis. Useful for creating stretched or extended
|
|
20
|
+
versions of existing shapes.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
sdf : SDFBase
|
|
25
|
+
The base SDF to elongate.
|
|
26
|
+
size : array-like of shape (3,) or float
|
|
27
|
+
Elongation amount along each axis (x, y, z). If a single float,
|
|
28
|
+
applies uniform elongation in all directions.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
>>> # Elongate a sphere by 0.5 in all directions
|
|
33
|
+
>>> sphere = SphereSDF(center=[0, 0, 0], radius=0.5)
|
|
34
|
+
>>> ElongateSDF(sphere, size=0.5)
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Elongate more in x-direction than y and z
|
|
37
|
+
>>> ElongateSDF(sphere, size=[1.0, 0.2, 0.2])
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, sdf: SDFBase, size):
|
|
41
|
+
"""Initialize ElongateSDF.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
sdf : SDFBase
|
|
46
|
+
The base SDF to elongate.
|
|
47
|
+
size : array-like of shape (3,) or float
|
|
48
|
+
Elongation amount along each axis.
|
|
49
|
+
"""
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.sdf = sdf
|
|
52
|
+
self.size = torch.nn.Parameter(torch.as_tensor(size, dtype=torch.float32))
|
|
53
|
+
|
|
54
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
55
|
+
size = self.size.to(device=queries.device, dtype=queries.dtype)
|
|
56
|
+
q = torch.abs(queries) - size
|
|
57
|
+
w = torch.minimum(
|
|
58
|
+
torch.maximum(torch.maximum(q[:, 0], q[:, 1]), q[:, 2]), torch.tensor(0.0)
|
|
59
|
+
).unsqueeze(1)
|
|
60
|
+
sdf_q = self.sdf._compute(torch.clamp(q, min=0.0))
|
|
61
|
+
return torch.maximum(sdf_q, w)
|
|
62
|
+
|
|
63
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
64
|
+
b = self.sdf._get_domain_bounds()
|
|
65
|
+
return torch.stack([b[0] - self.size, b[1] + self.size])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TwistSDF(SDFBase):
|
|
69
|
+
"""Twist an SDF around the Z-axis by an angle proportional to height.
|
|
70
|
+
|
|
71
|
+
Applies a rotational deformation where the rotation angle increases
|
|
72
|
+
linearly with the z-coordinate. Creates a spiral or twisted effect.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
sdf : SDFBase
|
|
77
|
+
The SDF to twist.
|
|
78
|
+
k : float
|
|
79
|
+
Twist rate (radians per unit length). Positive values create
|
|
80
|
+
counter-clockwise twist when looking down the Z-axis.
|
|
81
|
+
|
|
82
|
+
Examples
|
|
83
|
+
--------
|
|
84
|
+
>>> # Twist a box by 0.5 radians per unit height
|
|
85
|
+
>>> box = BoxSDF(center=[0, 0, 0], extents=[1, 1, 2])
|
|
86
|
+
>>> TwistSDF(box, k=0.5)
|
|
87
|
+
>>>
|
|
88
|
+
>>> # Stronger twist (full rotation over height 2π)
|
|
89
|
+
>>> TwistSDF(box, k=1.0)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, sdf: SDFBase, k):
|
|
93
|
+
"""Initialize TwistSDF.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
sdf : SDFBase
|
|
98
|
+
The SDF to twist.
|
|
99
|
+
k : float
|
|
100
|
+
Twist rate in radians per unit length.
|
|
101
|
+
"""
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.sdf = sdf
|
|
104
|
+
self.k = torch.nn.Parameter(torch.as_tensor(k, dtype=torch.float32))
|
|
105
|
+
|
|
106
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
107
|
+
k = self.k.to(device=queries.device, dtype=queries.dtype)
|
|
108
|
+
x, y, z = queries[:, 0], queries[:, 1], queries[:, 2]
|
|
109
|
+
c = torch.cos(k * z)
|
|
110
|
+
s = torch.sin(k * z)
|
|
111
|
+
x2 = c * x - s * y
|
|
112
|
+
y2 = s * x + c * y
|
|
113
|
+
rotated = torch.stack([x2, y2, z], dim=1)
|
|
114
|
+
return self.sdf._compute(rotated)
|
|
115
|
+
|
|
116
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
117
|
+
return self.sdf._get_domain_bounds()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BendLinearSDF(SDFBase):
|
|
121
|
+
"""Bend SDF linearly between two control points."""
|
|
122
|
+
|
|
123
|
+
def __init__(self, sdf: SDFBase, p0, p1, v):
|
|
124
|
+
super().__init__()
|
|
125
|
+
self.sdf = sdf
|
|
126
|
+
self.p0 = torch.nn.Parameter(torch.as_tensor(p0, dtype=torch.float32))
|
|
127
|
+
self.p1 = torch.nn.Parameter(torch.as_tensor(p1, dtype=torch.float32))
|
|
128
|
+
self.v = torch.nn.Parameter(torch.as_tensor(v, dtype=torch.float32))
|
|
129
|
+
|
|
130
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
131
|
+
p0 = self.p0.to(device=queries.device, dtype=queries.dtype)
|
|
132
|
+
p1 = self.p1.to(device=queries.device, dtype=queries.dtype)
|
|
133
|
+
v = self.v.to(device=queries.device, dtype=queries.dtype)
|
|
134
|
+
|
|
135
|
+
ab = p1 - p0
|
|
136
|
+
t = torch.clamp(torch.sum((queries - p0) * ab, dim=1) / torch.sum(ab**2), 0, 1)
|
|
137
|
+
t = t.reshape(-1, 1)
|
|
138
|
+
return self.sdf._compute(queries + t * v)
|
|
139
|
+
|
|
140
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
141
|
+
b = self.sdf._get_domain_bounds()
|
|
142
|
+
return b
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class BendRadialSDF(SDFBase):
|
|
146
|
+
"""Bend SDF radially."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, sdf: SDFBase, r0, r1, dz):
|
|
149
|
+
super().__init__()
|
|
150
|
+
self.sdf = sdf
|
|
151
|
+
self.r0 = torch.nn.Parameter(torch.as_tensor(r0, dtype=torch.float32))
|
|
152
|
+
self.r1 = torch.nn.Parameter(torch.as_tensor(r1, dtype=torch.float32))
|
|
153
|
+
self.dz = torch.nn.Parameter(torch.as_tensor(dz, dtype=torch.float32))
|
|
154
|
+
|
|
155
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
156
|
+
r0 = self.r0.to(device=queries.device, dtype=queries.dtype)
|
|
157
|
+
r1 = self.r1.to(device=queries.device, dtype=queries.dtype)
|
|
158
|
+
dz = self.dz.to(device=queries.device, dtype=queries.dtype)
|
|
159
|
+
|
|
160
|
+
x = queries[:, 0]
|
|
161
|
+
y = queries[:, 1]
|
|
162
|
+
r = torch.sqrt(x**2 + y**2)
|
|
163
|
+
t = torch.clamp((r - r0) / (r1 - r0), 0, 1)
|
|
164
|
+
z = queries[:, 2] - dz * t
|
|
165
|
+
|
|
166
|
+
p = torch.stack([queries[:, 0], queries[:, 1], z], dim=1)
|
|
167
|
+
return self.sdf._compute(p)
|
|
168
|
+
|
|
169
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
170
|
+
return self.sdf._get_domain_bounds()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class DilateSDF(SDFBase):
|
|
174
|
+
"""Expand an SDF uniformly by adding material to the surface.
|
|
175
|
+
|
|
176
|
+
Increases the size of the geometry by moving the surface outward
|
|
177
|
+
by a specified distance. Equivalent to making the object larger
|
|
178
|
+
while maintaining its shape.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
sdf : SDFBase
|
|
183
|
+
The SDF to expand.
|
|
184
|
+
r : float
|
|
185
|
+
Expansion distance. Positive values add material (make larger),
|
|
186
|
+
negative values remove material (make smaller).
|
|
187
|
+
|
|
188
|
+
Examples
|
|
189
|
+
--------
|
|
190
|
+
>>> # Expand a sphere by 0.1 units
|
|
191
|
+
>>> sphere = SphereSDF(center=[0, 0, 0], radius=0.5)
|
|
192
|
+
>>> DilateSDF(sphere, r=0.1) # radius effectively becomes 0.6
|
|
193
|
+
>>>
|
|
194
|
+
>>> # Shrink by using negative dilation
|
|
195
|
+
>>> DilateSDF(sphere, r=-0.1) # radius effectively becomes 0.4
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, sdf: SDFBase, r):
|
|
199
|
+
"""Initialize DilateSDF.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
sdf : SDFBase
|
|
204
|
+
The SDF to expand.
|
|
205
|
+
r : float
|
|
206
|
+
Expansion distance (positive=larger, negative=smaller).
|
|
207
|
+
"""
|
|
208
|
+
super().__init__()
|
|
209
|
+
self.sdf = sdf
|
|
210
|
+
self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
|
|
211
|
+
|
|
212
|
+
def _compute(self, queries) -> torch.Tensor:
|
|
213
|
+
r = self.r.to(device=queries.device, dtype=queries.dtype)
|
|
214
|
+
return self.sdf._compute(queries) - r
|
|
215
|
+
|
|
216
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
217
|
+
b = self.sdf._get_domain_bounds()
|
|
218
|
+
# Expand bounds by r
|
|
219
|
+
return torch.stack([b[0] - self.r, b[1] + self.r])
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ErodeSDF(SDFBase):
|
|
223
|
+
"""Contract an SDF uniformly by removing material from the surface.
|
|
224
|
+
|
|
225
|
+
Decreases the size of the geometry by moving the surface inward
|
|
226
|
+
by a specified distance. Equivalent to making the object smaller
|
|
227
|
+
while maintaining its shape.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
sdf : SDFBase
|
|
232
|
+
The SDF to contract.
|
|
233
|
+
r : float
|
|
234
|
+
Contraction distance. Must be positive. For expansion, use
|
|
235
|
+
DilateSDF instead.
|
|
236
|
+
|
|
237
|
+
Examples
|
|
238
|
+
--------
|
|
239
|
+
>>> # Shrink a sphere by 0.1 units
|
|
240
|
+
>>> sphere = SphereSDF(center=[0, 0, 0], radius=0.5)
|
|
241
|
+
>>> ErodeSDF(sphere, r=0.1) # radius effectively becomes 0.4
|
|
242
|
+
>>>
|
|
243
|
+
>>> # Create a thin shell by combining with original
|
|
244
|
+
>>> ErodeSDF(sphere, r=0.05)
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
def __init__(self, sdf: SDFBase, r):
|
|
248
|
+
"""Initialize ErodeSDF.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
sdf : SDFBase
|
|
253
|
+
The SDF to contract.
|
|
254
|
+
r : float
|
|
255
|
+
Contraction distance (must be positive).
|
|
256
|
+
"""
|
|
257
|
+
super().__init__()
|
|
258
|
+
self.sdf = sdf
|
|
259
|
+
self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
|
|
260
|
+
|
|
261
|
+
def _compute(self, queries) -> torch.Tensor:
|
|
262
|
+
r = self.r.to(device=queries.device, dtype=queries.dtype)
|
|
263
|
+
return self.sdf._compute(queries) + r
|
|
264
|
+
|
|
265
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
266
|
+
return self.sdf._get_domain_bounds()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ShellSDF(SDFBase):
|
|
270
|
+
"""Create a hollow shell by keeping only a thin layer around the surface.
|
|
271
|
+
|
|
272
|
+
Converts a solid object into a hollow shell by taking the absolute
|
|
273
|
+
value of the SDF and offsetting by half the thickness. The result
|
|
274
|
+
is a shell centered on the original surface.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
sdf : SDFBase
|
|
279
|
+
The SDF to convert to a shell.
|
|
280
|
+
thickness : float
|
|
281
|
+
Total thickness of the shell wall. Must be positive.
|
|
282
|
+
|
|
283
|
+
Examples
|
|
284
|
+
--------
|
|
285
|
+
>>> # Create a hollow sphere with 0.05 wall thickness
|
|
286
|
+
>>> sphere = SphereSDF(center=[0, 0, 0], radius=0.5)
|
|
287
|
+
>>> ShellSDF(sphere, thickness=0.05)
|
|
288
|
+
>>>
|
|
289
|
+
>>> # Create a thin-walled box
|
|
290
|
+
>>> box = BoxSDF(center=[0, 0, 0], extents=[1, 1, 1])
|
|
291
|
+
>>> ShellSDF(box, thickness=0.02)
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(self, sdf: SDFBase, thickness):
|
|
295
|
+
"""Initialize ShellSDF.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
sdf : SDFBase
|
|
300
|
+
The SDF to convert to a shell.
|
|
301
|
+
thickness : float
|
|
302
|
+
Total thickness of the shell wall.
|
|
303
|
+
"""
|
|
304
|
+
super().__init__()
|
|
305
|
+
self.sdf = sdf
|
|
306
|
+
self.thickness = torch.nn.Parameter(
|
|
307
|
+
torch.as_tensor(thickness, dtype=torch.float32)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _compute(self, queries) -> torch.Tensor:
|
|
311
|
+
t = self.thickness.to(device=queries.device, dtype=queries.dtype)
|
|
312
|
+
return torch.abs(self.sdf._compute(queries)) - t / 2
|
|
313
|
+
|
|
314
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
315
|
+
return self.sdf._get_domain_bounds()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class RepeatSDF(SDFBase):
|
|
319
|
+
"""Infinite or finite grid repetition of an SDF."""
|
|
320
|
+
|
|
321
|
+
def __init__(self, sdf: SDFBase, spacing, count=None):
|
|
322
|
+
super().__init__()
|
|
323
|
+
self.sdf = sdf
|
|
324
|
+
self.spacing = torch.nn.Parameter(torch.as_tensor(spacing, dtype=torch.float32))
|
|
325
|
+
self.count = count # None for infinite, odd number for finite
|
|
326
|
+
|
|
327
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
328
|
+
spacing = self.spacing.to(device=queries.device, dtype=queries.dtype)
|
|
329
|
+
|
|
330
|
+
# Fold all points symmetrically into [-spacing/2, spacing/2]
|
|
331
|
+
q = queries - spacing * torch.round(queries / spacing)
|
|
332
|
+
|
|
333
|
+
if self.count is not None:
|
|
334
|
+
# Clamp the grid index so only `count` copies are active
|
|
335
|
+
half = (self.count - 1) / 2.0
|
|
336
|
+
n = torch.round(queries / spacing)
|
|
337
|
+
n = torch.clamp(n, -half, half)
|
|
338
|
+
q = queries - spacing * n
|
|
339
|
+
|
|
340
|
+
return self.sdf._compute(q)
|
|
341
|
+
|
|
342
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
343
|
+
if self.count is None:
|
|
344
|
+
# Return single-period bounds for visualization
|
|
345
|
+
return self.sdf._get_domain_bounds()
|
|
346
|
+
else:
|
|
347
|
+
b = self.sdf._get_domain_bounds()
|
|
348
|
+
span = (self.count - 1) * self.spacing
|
|
349
|
+
return torch.stack([b[0] - span / 2, b[1] + span / 2])
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class MirrorSDF(SDFBase):
|
|
353
|
+
"""Reflect an SDF across a plane to create symmetric geometry.
|
|
354
|
+
|
|
355
|
+
Creates a mirror copy of an SDF across a specified plane, effectively
|
|
356
|
+
doubling the geometry. The result is the union of the original and
|
|
357
|
+
reflected SDF, creating a symmetric object.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
sdf : SDFBase
|
|
362
|
+
The SDF to mirror. This will be reflected across the plane.
|
|
363
|
+
plane_point : array-like of shape (3,)
|
|
364
|
+
A point that lies on the mirror plane. This point, together with
|
|
365
|
+
plane_normal, defines the mirror plane.
|
|
366
|
+
plane_normal : array-like of shape (3,)
|
|
367
|
+
Normal vector of the mirror plane. The plane is perpendicular to
|
|
368
|
+
this vector. Will be normalized internally.
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
--------
|
|
372
|
+
>>> # Mirror a sphere across the YZ plane (x=0)
|
|
373
|
+
>>> sphere = SphereSDF(center=[0.5, 0, 0], radius=0.3)
|
|
374
|
+
>>> MirrorSDF(sphere, plane_point=[0, 0, 0], plane_normal=[1, 0, 0])
|
|
375
|
+
>>>
|
|
376
|
+
>>> # Mirror across the XY plane (z=0)
|
|
377
|
+
>>> MirrorSDF(sphere, plane_point=[0, 0, 0], plane_normal=[0, 0, 1])
|
|
378
|
+
>>>
|
|
379
|
+
>>> # Mirror across a diagonal plane
|
|
380
|
+
>>> MirrorSDF(sphere, plane_point=[0, 0, 0], plane_normal=[1, 1, 0])
|
|
381
|
+
|
|
382
|
+
Notes
|
|
383
|
+
-----
|
|
384
|
+
- The result is the union (minimum) of original and mirrored SDF
|
|
385
|
+
- Use plane_point and plane_normal to define any mirror plane
|
|
386
|
+
- Common mirror planes:
|
|
387
|
+
- YZ plane: point=[0,0,0], normal=[1,0,0]
|
|
388
|
+
- XZ plane: point=[0,0,0], normal=[0,1,0]
|
|
389
|
+
- XY plane: point=[0,0,0], normal=[0,0,1]
|
|
390
|
+
- The mirrored geometry is exactly symmetric - no thickness or gap
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, sdf: SDFBase, plane_point, plane_normal):
|
|
394
|
+
"""Initialize MirrorSDF.
|
|
395
|
+
|
|
396
|
+
Parameters
|
|
397
|
+
----------
|
|
398
|
+
sdf : SDFBase
|
|
399
|
+
The SDF to mirror.
|
|
400
|
+
plane_point : array-like of shape (3,)
|
|
401
|
+
A point on the mirror plane.
|
|
402
|
+
plane_normal : array-like of shape (3,)
|
|
403
|
+
Normal vector of the mirror plane.
|
|
404
|
+
"""
|
|
405
|
+
super().__init__()
|
|
406
|
+
self.sdf = sdf
|
|
407
|
+
self.point = torch.nn.Parameter(
|
|
408
|
+
torch.as_tensor(plane_point, dtype=torch.float32)
|
|
409
|
+
)
|
|
410
|
+
self.normal = torch.nn.Parameter(
|
|
411
|
+
torch.as_tensor(plane_normal, dtype=torch.float32)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
415
|
+
pt = self.point.to(device=queries.device, dtype=queries.dtype)
|
|
416
|
+
nm = self.normal.to(device=queries.device, dtype=queries.dtype)
|
|
417
|
+
nm = nm / torch.linalg.norm(nm)
|
|
418
|
+
|
|
419
|
+
# Signed distance to plane
|
|
420
|
+
d = torch.sum((queries - pt) * nm, dim=1, keepdim=True)
|
|
421
|
+
|
|
422
|
+
# Distance from original SDF
|
|
423
|
+
d_orig = self.sdf._compute(queries)
|
|
424
|
+
|
|
425
|
+
# Distance from reflected SDF
|
|
426
|
+
queries_reflected = queries - 2 * d * nm
|
|
427
|
+
d_reflected = self.sdf._compute(queries_reflected)
|
|
428
|
+
|
|
429
|
+
# Take minimum (union of original and mirror)
|
|
430
|
+
return torch.minimum(d_orig, d_reflected)
|
|
431
|
+
|
|
432
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
433
|
+
return self.sdf._get_domain_bounds()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class CircularArraySDF(SDFBase):
|
|
437
|
+
"""Create count copies of an SDF rotated around an arbitrary axis line.
|
|
438
|
+
|
|
439
|
+
The axis line is defined by an ``axis`` direction and a ``base_point``
|
|
440
|
+
lying on that axis. Copies are sampled at evenly spaced angles in the
|
|
441
|
+
half-open interval [start_angle, end_angle).
|
|
442
|
+
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
def __init__(
|
|
446
|
+
self,
|
|
447
|
+
sdf: SDFBase,
|
|
448
|
+
count,
|
|
449
|
+
axis=torch.tensor([0, 0, 1], dtype=torch.float32),
|
|
450
|
+
base_point=torch.tensor([0, 0, 0], dtype=torch.float32),
|
|
451
|
+
start_angle_deg=0.0,
|
|
452
|
+
end_angle_deg=360,
|
|
453
|
+
):
|
|
454
|
+
super().__init__()
|
|
455
|
+
self.sdf = sdf
|
|
456
|
+
self.count = int(count)
|
|
457
|
+
|
|
458
|
+
self.axis = torch.nn.Parameter(torch.as_tensor(axis, dtype=torch.float32))
|
|
459
|
+
self.base_point = torch.nn.Parameter(
|
|
460
|
+
torch.as_tensor(base_point, dtype=torch.float32)
|
|
461
|
+
)
|
|
462
|
+
self.start_angle = torch.nn.Parameter(
|
|
463
|
+
torch.as_tensor(start_angle_deg, dtype=torch.float32)
|
|
464
|
+
)
|
|
465
|
+
self.end_angle = torch.nn.Parameter(
|
|
466
|
+
torch.as_tensor(end_angle_deg, dtype=torch.float32)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@staticmethod
|
|
470
|
+
def _normalize_axis(axis: torch.Tensor) -> torch.Tensor:
|
|
471
|
+
axis_norm = torch.linalg.norm(axis)
|
|
472
|
+
if axis_norm <= 0:
|
|
473
|
+
raise ValueError("Axis must be non-zero")
|
|
474
|
+
return axis / axis_norm
|
|
475
|
+
|
|
476
|
+
@staticmethod
|
|
477
|
+
def _rotate_about_axis(
|
|
478
|
+
points: torch.Tensor, axis_unit: torch.Tensor, angles: torch.Tensor
|
|
479
|
+
) -> torch.Tensor:
|
|
480
|
+
"""Rotate points around an arbitrary axis for a batch of angles."""
|
|
481
|
+
k = axis_unit.view(1, 1, 3)
|
|
482
|
+
p = points.unsqueeze(0).expand(angles.shape[0], -1, -1)
|
|
483
|
+
a = angles.view(-1, 1, 1)
|
|
484
|
+
c = torch.cos(torch.deg2rad(a))
|
|
485
|
+
s = torch.sin(torch.deg2rad(a))
|
|
486
|
+
|
|
487
|
+
cross = torch.cross(k, p, dim=2)
|
|
488
|
+
dot = torch.sum(p * k, dim=2, keepdim=True)
|
|
489
|
+
return p * c + cross * s + k * dot * (1 - c)
|
|
490
|
+
|
|
491
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
492
|
+
if self.count <= 0:
|
|
493
|
+
raise ValueError("count must be a positive integer")
|
|
494
|
+
|
|
495
|
+
axis = self.axis.to(device=queries.device, dtype=queries.dtype)
|
|
496
|
+
axis_unit = self._normalize_axis(axis)
|
|
497
|
+
base = self.base_point.to(device=queries.device, dtype=queries.dtype)
|
|
498
|
+
start = self.start_angle.to(device=queries.device, dtype=queries.dtype)
|
|
499
|
+
end = self.end_angle.to(device=queries.device, dtype=queries.dtype)
|
|
500
|
+
|
|
501
|
+
step = (end - start) / self.count
|
|
502
|
+
angles = start + step * torch.arange(
|
|
503
|
+
self.count, device=queries.device, dtype=queries.dtype
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Pull query points back through inverse rotations and evaluate each copy
|
|
507
|
+
# without changing the number of points passed to the child SDF.
|
|
508
|
+
centered = queries - base
|
|
509
|
+
dmin = None
|
|
510
|
+
for angle in angles:
|
|
511
|
+
rotated = self._rotate_about_axis(centered, axis_unit, (-angle).reshape(1))[
|
|
512
|
+
0
|
|
513
|
+
]
|
|
514
|
+
eval_points = rotated + base
|
|
515
|
+
d = self.sdf._compute(eval_points)
|
|
516
|
+
dmin = d if dmin is None else torch.minimum(dmin, d)
|
|
517
|
+
|
|
518
|
+
return dmin
|
|
519
|
+
|
|
520
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
521
|
+
if self.count <= 0:
|
|
522
|
+
raise ValueError("count must be a positive integer")
|
|
523
|
+
|
|
524
|
+
b = self.sdf._get_domain_bounds()
|
|
525
|
+
minb = torch.minimum(b[0], b[1])
|
|
526
|
+
maxb = torch.maximum(b[0], b[1])
|
|
527
|
+
|
|
528
|
+
corners = torch.stack(
|
|
529
|
+
[
|
|
530
|
+
torch.tensor([x, y, z], dtype=minb.dtype, device=minb.device)
|
|
531
|
+
for x in (minb[0], maxb[0])
|
|
532
|
+
for y in (minb[1], maxb[1])
|
|
533
|
+
for z in (minb[2], maxb[2])
|
|
534
|
+
],
|
|
535
|
+
dim=0,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
axis = self.axis.detach().to(device=minb.device, dtype=minb.dtype)
|
|
539
|
+
axis_unit = self._normalize_axis(axis)
|
|
540
|
+
base = self.base_point.detach().to(device=minb.device, dtype=minb.dtype)
|
|
541
|
+
start = self.start_angle.detach().to(device=minb.device, dtype=minb.dtype)
|
|
542
|
+
end = self.end_angle.detach().to(device=minb.device, dtype=minb.dtype)
|
|
543
|
+
|
|
544
|
+
step = (end - start) / self.count
|
|
545
|
+
angles = start + step * torch.arange(
|
|
546
|
+
self.count, device=minb.device, dtype=minb.dtype
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
centered = corners - base
|
|
550
|
+
rotated = self._rotate_about_axis(centered, axis_unit, angles)
|
|
551
|
+
rotated_world = rotated + base.view(1, 1, 3)
|
|
552
|
+
|
|
553
|
+
lower = torch.min(rotated_world.reshape(-1, 3), dim=0).values
|
|
554
|
+
upper = torch.max(rotated_world.reshape(-1, 3), dim=0).values
|
|
555
|
+
return torch.stack([lower, upper], dim=0)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class RevolveSDF(SDFBase):
|
|
559
|
+
"""Revolve a 2D profile around an axis to create a 3D surface of revolution.
|
|
560
|
+
|
|
561
|
+
Takes a 2D SDF profile and rotates it around a specified axis to generate
|
|
562
|
+
a 3D axisymmetric object. The 2D profile is defined in a plane containing
|
|
563
|
+
the rotation axis.
|
|
564
|
+
|
|
565
|
+
Parameters
|
|
566
|
+
----------
|
|
567
|
+
sdf_2d : SDFBase
|
|
568
|
+
A 2D SDF (geometric_dim=2) representing the profile to revolve.
|
|
569
|
+
The profile should be in the plane where the first coordinate is
|
|
570
|
+
the radial distance from the axis, and the second coordinate is
|
|
571
|
+
the height along the axis.
|
|
572
|
+
axis : torch.Tensor, default [0, 0, 1]
|
|
573
|
+
The 3D axis vector to revolve around. Must be one of the principal
|
|
574
|
+
axes: [1,0,0] (X-axis), [0,1,0] (Y-axis), or [0,0,1] (Z-axis).
|
|
575
|
+
The 2D profile's first coordinate becomes the radial distance from
|
|
576
|
+
this axis.
|
|
577
|
+
offset : float, default 0.0
|
|
578
|
+
Radial offset from the axis. Positive values move the profile away
|
|
579
|
+
from the axis, creating a hole in the center. Use this to create
|
|
580
|
+
hollow objects or to position the profile at a specific radius.
|
|
581
|
+
|
|
582
|
+
Raises
|
|
583
|
+
------
|
|
584
|
+
ValueError
|
|
585
|
+
If sdf_2d is not a 2D SDF, or if axis is not a principal axis.
|
|
586
|
+
|
|
587
|
+
Examples
|
|
588
|
+
--------
|
|
589
|
+
>>> # Create a sphere by revolving a circle around Z-axis
|
|
590
|
+
>>> circle = CircleSDF(center=[0.5, 0], radius=0.5)
|
|
591
|
+
>>> RevolveSDF(circle, axis=[0, 0, 1])
|
|
592
|
+
>>>
|
|
593
|
+
>>> # Create a torus by revolving a circle with offset
|
|
594
|
+
>>> circle = CircleSDF(center=[1.0, 0], radius=0.2)
|
|
595
|
+
>>> RevolveSDF(circle, axis=[0, 0, 1])
|
|
596
|
+
>>>
|
|
597
|
+
>>> # Revolve around X-axis instead
|
|
598
|
+
>>> RevolveSDF(circle, axis=[1, 0, 0])
|
|
599
|
+
|
|
600
|
+
Notes
|
|
601
|
+
-----
|
|
602
|
+
- The 2D profile's first coordinate (x) represents radial distance from axis
|
|
603
|
+
- The 2D profile's second coordinate (y) represents height along the axis
|
|
604
|
+
- Only principal axes ([1,0,0], [0,1,0], [0,0,1]) are supported
|
|
605
|
+
- The profile should be positioned appropriately for the desired shape
|
|
606
|
+
- Use offset to create holes or position the profile away from the axis
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
def __init__(
|
|
610
|
+
self,
|
|
611
|
+
sdf_2d: SDFBase,
|
|
612
|
+
axis=torch.tensor([0, 0, 1], dtype=torch.float32),
|
|
613
|
+
offset=0.0,
|
|
614
|
+
):
|
|
615
|
+
"""Initialize RevolveSDF with a 2D profile.
|
|
616
|
+
|
|
617
|
+
Parameters
|
|
618
|
+
----------
|
|
619
|
+
sdf_2d : SDFBase
|
|
620
|
+
2D SDF profile to revolve.
|
|
621
|
+
axis : torch.Tensor, default [0, 0, 1]
|
|
622
|
+
Axis of revolution (must be principal axis).
|
|
623
|
+
offset : float, default 0.0
|
|
624
|
+
Radial offset from the axis.
|
|
625
|
+
"""
|
|
626
|
+
super().__init__()
|
|
627
|
+
if sdf_2d.geometric_dim != 2:
|
|
628
|
+
raise ValueError("RevolveSDF requires a 2D SDF")
|
|
629
|
+
self.sdf_2d = sdf_2d
|
|
630
|
+
self.axis = torch.nn.Parameter(torch.as_tensor(axis, dtype=torch.float32))
|
|
631
|
+
self.offset = torch.nn.Parameter(torch.as_tensor(offset, dtype=torch.float32))
|
|
632
|
+
|
|
633
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
634
|
+
offset = self.offset.to(device=queries.device, dtype=queries.dtype)
|
|
635
|
+
axis = self.axis.to(device=queries.device, dtype=queries.dtype)
|
|
636
|
+
|
|
637
|
+
if torch.all(
|
|
638
|
+
axis == torch.tensor([0, 0, 1], device=queries.device, dtype=queries.dtype)
|
|
639
|
+
):
|
|
640
|
+
# Use x,y plane distance as radius, z as height in 2D space
|
|
641
|
+
radius = torch.sqrt(queries[:, 0] ** 2 + queries[:, 1] ** 2) - offset
|
|
642
|
+
pts_2d = torch.stack([radius, queries[:, 2]], dim=1)
|
|
643
|
+
elif torch.all(
|
|
644
|
+
axis == torch.tensor([1, 0, 0], device=queries.device, dtype=queries.dtype)
|
|
645
|
+
):
|
|
646
|
+
# Use y,z plane
|
|
647
|
+
radius = torch.sqrt(queries[:, 1] ** 2 + queries[:, 2] ** 2) - offset
|
|
648
|
+
pts_2d = torch.stack([radius, queries[:, 0]], dim=1)
|
|
649
|
+
elif torch.all(
|
|
650
|
+
axis == torch.tensor([0, 1, 0], device=queries.device, dtype=queries.dtype)
|
|
651
|
+
):
|
|
652
|
+
# Use x,z plane
|
|
653
|
+
radius = torch.sqrt(queries[:, 0] ** 2 + queries[:, 2] ** 2) - offset
|
|
654
|
+
pts_2d = torch.stack([radius, queries[:, 1]], dim=1)
|
|
655
|
+
else:
|
|
656
|
+
raise ValueError(
|
|
657
|
+
f"Invalid axis: {self.axis}. Must be [1,0,0], [0,1,0], or [0,0,1]"
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
return self.sdf_2d._compute(pts_2d)
|
|
661
|
+
|
|
662
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
663
|
+
b = self.sdf_2d._get_domain_bounds()
|
|
664
|
+
# The 2D profile's first coordinate is the radial distance from the axis.
|
|
665
|
+
# When revolved, this creates a full circle, so x/y bounds are symmetric.
|
|
666
|
+
max_radius = torch.maximum(b[0, 0].abs(), b[1, 0].abs())
|
|
667
|
+
return torch.stack(
|
|
668
|
+
[
|
|
669
|
+
torch.stack([-max_radius, -max_radius, b[0, 1]]),
|
|
670
|
+
torch.stack([max_radius, max_radius, b[1, 1]]),
|
|
671
|
+
]
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def cubic_bezier_distance(p, control_points, samples=500, chunk_size=1000, refine=True):
|
|
676
|
+
"""
|
|
677
|
+
Compute distance from point p to cubic bezier curve using sampling with refinement.
|
|
678
|
+
|
|
679
|
+
Samples the curve at many points and finds the closest one, then refines locally.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
p: Query point(s), shape (N, 3)
|
|
683
|
+
control_points: Control points tensor, shape (4, 3)
|
|
684
|
+
samples: Number of samples along the curve
|
|
685
|
+
chunk_size: Number of query points to process at a time (for memory efficiency)
|
|
686
|
+
refine: Whether to refine the minimum with local optimization
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Tuple of (dist, t, closest_point) where:
|
|
690
|
+
- dist: distance to curve, shape (N, 1)
|
|
691
|
+
- t: parameter value at closest point, shape (N, 1)
|
|
692
|
+
- closest_point: closest point on curve, shape (N, 3)
|
|
693
|
+
"""
|
|
694
|
+
if p.dim() == 1:
|
|
695
|
+
p = p.unsqueeze(0)
|
|
696
|
+
|
|
697
|
+
# Move control points to same device as p
|
|
698
|
+
control_points = control_points.to(device=p.device, dtype=p.dtype)
|
|
699
|
+
|
|
700
|
+
p0, p1, p2, p3 = control_points
|
|
701
|
+
|
|
702
|
+
# Sample the curve once (shared across all queries)
|
|
703
|
+
t_samples = torch.linspace(0, 1, samples, device=p.device, dtype=p.dtype)
|
|
704
|
+
u_samples = 1 - t_samples
|
|
705
|
+
|
|
706
|
+
# Evaluate cubic bezier at all sample points
|
|
707
|
+
t_vec = t_samples.unsqueeze(1) # (samples, 1)
|
|
708
|
+
u_vec = u_samples.unsqueeze(1) # (samples, 1)
|
|
709
|
+
|
|
710
|
+
curve_points = (
|
|
711
|
+
(u_vec**3) * p0
|
|
712
|
+
+ (3 * u_vec**2 * t_vec) * p1
|
|
713
|
+
+ (3 * u_vec * t_vec**2) * p2
|
|
714
|
+
+ (t_vec**3) * p3
|
|
715
|
+
) # (samples, 3)
|
|
716
|
+
|
|
717
|
+
# Initialize output tensors
|
|
718
|
+
n_points = p.shape[0]
|
|
719
|
+
dtype = p.dtype
|
|
720
|
+
device = p.device
|
|
721
|
+
|
|
722
|
+
min_dists = torch.full((n_points, 1), float("inf"), dtype=dtype, device=device)
|
|
723
|
+
min_idx = torch.zeros(n_points, dtype=torch.long, device=device)
|
|
724
|
+
|
|
725
|
+
# Process in chunks to reduce memory
|
|
726
|
+
for i in range(0, n_points, chunk_size):
|
|
727
|
+
end_idx = min(i + chunk_size, n_points)
|
|
728
|
+
chunk = p[i:end_idx] # (chunk_size, 3)
|
|
729
|
+
|
|
730
|
+
# Compute distances for this chunk
|
|
731
|
+
chunk_expanded = chunk.unsqueeze(1) # (chunk_size, 1, 3)
|
|
732
|
+
curve_expanded = curve_points.unsqueeze(0) # (1, samples, 3)
|
|
733
|
+
|
|
734
|
+
diff = chunk_expanded - curve_expanded # (chunk_size, samples, 3)
|
|
735
|
+
dists_chunk = torch.linalg.norm(diff, dim=2) # (chunk_size, samples)
|
|
736
|
+
|
|
737
|
+
# Find minimum for this chunk
|
|
738
|
+
min_dists_chunk, min_idx_chunk = torch.min(
|
|
739
|
+
dists_chunk, dim=1
|
|
740
|
+
) # (chunk_size), (chunk_size)
|
|
741
|
+
|
|
742
|
+
min_dists[i:end_idx] = min_dists_chunk.unsqueeze(1)
|
|
743
|
+
min_idx[i:end_idx] = min_idx_chunk
|
|
744
|
+
|
|
745
|
+
# Get initial best t and points
|
|
746
|
+
t_closest = t_samples[min_idx].unsqueeze(1) # (N, 1)
|
|
747
|
+
point_closest = curve_points[min_idx] # (N, 3)
|
|
748
|
+
|
|
749
|
+
# Local refinement around the minimum (optional)
|
|
750
|
+
if refine:
|
|
751
|
+
# Process refinement in chunks as well to save memory
|
|
752
|
+
for i in range(0, n_points, chunk_size):
|
|
753
|
+
end_idx = min(i + chunk_size, n_points)
|
|
754
|
+
chunk = p[i:end_idx]
|
|
755
|
+
t_chunk_base = t_closest[i:end_idx]
|
|
756
|
+
|
|
757
|
+
# Create refined samples around each minimum for this chunk
|
|
758
|
+
refine_points = 20
|
|
759
|
+
refine_delta = 0.02 # Larger refinement window
|
|
760
|
+
|
|
761
|
+
t_refine_base = t_chunk_base.squeeze(1) # (chunk,)
|
|
762
|
+
t_refine_start = torch.clamp(t_refine_base - refine_delta, 0, 1)
|
|
763
|
+
t_refine_width = (
|
|
764
|
+
torch.clamp(t_refine_base + refine_delta, 0, 1) - t_refine_start
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
min_dists_chunk = min_dists[i:end_idx].clone()
|
|
768
|
+
min_t_chunk = t_chunk_base.clone()
|
|
769
|
+
|
|
770
|
+
# Evaluate at multiple refinement points for this chunk
|
|
771
|
+
for j in range(refine_points + 1):
|
|
772
|
+
t_refine = t_refine_start + (t_refine_width * j / refine_points)
|
|
773
|
+
t_refine_exp = t_refine.unsqueeze(1) # (chunk, 1)
|
|
774
|
+
|
|
775
|
+
# Evaluate bezier at this t
|
|
776
|
+
u_val = 1 - t_refine_exp
|
|
777
|
+
curve_at_t = (
|
|
778
|
+
(u_val**3) * p0
|
|
779
|
+
+ (3 * u_val**2 * t_refine_exp) * p1
|
|
780
|
+
+ (3 * u_val * t_refine_exp**2) * p2
|
|
781
|
+
+ (t_refine_exp**3) * p3
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
# Compute distances
|
|
785
|
+
diff = chunk - curve_at_t # (chunk, 3)
|
|
786
|
+
dists = torch.linalg.norm(diff, dim=1, keepdim=True)
|
|
787
|
+
|
|
788
|
+
# Update minimum if better
|
|
789
|
+
mask = dists < min_dists_chunk
|
|
790
|
+
min_dists_chunk = torch.where(mask, dists, min_dists_chunk)
|
|
791
|
+
min_t_chunk = torch.where(mask, t_refine_exp, min_t_chunk)
|
|
792
|
+
|
|
793
|
+
# Update global results with refined chunk results
|
|
794
|
+
min_dists[i:end_idx] = min_dists_chunk
|
|
795
|
+
t_closest[i:end_idx] = min_t_chunk
|
|
796
|
+
|
|
797
|
+
# Recompute closest points with refined t values
|
|
798
|
+
t_closest_exp = t_closest
|
|
799
|
+
point_closest = (
|
|
800
|
+
(1 - t_closest_exp) ** 3 * p0
|
|
801
|
+
+ 3 * (1 - t_closest_exp) ** 2 * t_closest_exp * p1
|
|
802
|
+
+ 3 * (1 - t_closest_exp) * t_closest_exp**2 * p2
|
|
803
|
+
+ t_closest_exp**3 * p3
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
return min_dists, t_closest, point_closest
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class SweepSDF(SDFBase):
|
|
810
|
+
"""Sweep a 2D profile along a cubic bezier curve with flat end caps."""
|
|
811
|
+
|
|
812
|
+
def __init__(
|
|
813
|
+
self,
|
|
814
|
+
profile_sdf: SDFBase,
|
|
815
|
+
trajectory,
|
|
816
|
+
bezier_samples=500,
|
|
817
|
+
cap_ends=True,
|
|
818
|
+
chunk_size=1000,
|
|
819
|
+
refine=True,
|
|
820
|
+
):
|
|
821
|
+
super().__init__()
|
|
822
|
+
assert profile_sdf.geometric_dim == 2, "SweepSDF requires a 2D profile SDF"
|
|
823
|
+
self.profile_sdf = profile_sdf
|
|
824
|
+
self.trajectory = trajectory
|
|
825
|
+
self.bezier_samples = bezier_samples
|
|
826
|
+
self.cap_ends = cap_ends
|
|
827
|
+
self.chunk_size = chunk_size
|
|
828
|
+
self.refine = refine
|
|
829
|
+
|
|
830
|
+
def _compute(self, queries: torch.Tensor) -> torch.Tensor:
|
|
831
|
+
# Ensure queries has shape (N, 3)
|
|
832
|
+
original_shape = queries.shape
|
|
833
|
+
if queries.dim() == 1:
|
|
834
|
+
queries = queries.unsqueeze(0)
|
|
835
|
+
|
|
836
|
+
control_points = self.trajectory.control_points
|
|
837
|
+
device = queries.device
|
|
838
|
+
|
|
839
|
+
# Move control points to correct device
|
|
840
|
+
control_points = control_points.to(device=device, dtype=queries.dtype)
|
|
841
|
+
|
|
842
|
+
n_points = queries.shape[0]
|
|
843
|
+
|
|
844
|
+
# Initialize output
|
|
845
|
+
result = torch.zeros(n_points, 1, dtype=queries.dtype, device=device)
|
|
846
|
+
|
|
847
|
+
# Process in chunks to reduce memory
|
|
848
|
+
for i in range(0, n_points, self.chunk_size):
|
|
849
|
+
end_idx = min(i + self.chunk_size, n_points)
|
|
850
|
+
chunk_queries = queries[i:end_idx] # (chunk, 3)
|
|
851
|
+
|
|
852
|
+
# Find closest point and parameter t on bezier for this chunk
|
|
853
|
+
dist_to_curve, t, closest_point = cubic_bezier_distance(
|
|
854
|
+
chunk_queries,
|
|
855
|
+
control_points,
|
|
856
|
+
self.bezier_samples,
|
|
857
|
+
chunk_size=self.chunk_size,
|
|
858
|
+
refine=self.refine,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
# Compute tangent at closest point
|
|
862
|
+
t_values = t[:, 0:1]
|
|
863
|
+
u = 1 - t_values
|
|
864
|
+
p0, p1, p2, p3 = control_points
|
|
865
|
+
|
|
866
|
+
tangent_per_point = (
|
|
867
|
+
(3 * u**2) * (p1 - p0).unsqueeze(0)
|
|
868
|
+
+ (6 * u * t_values) * (p2 - p1).unsqueeze(0)
|
|
869
|
+
+ (3 * t_values**2) * (p3 - p2).unsqueeze(0)
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
tangent_norms = torch.linalg.norm(tangent_per_point, dim=1, keepdim=True)
|
|
873
|
+
tangent = tangent_per_point / (tangent_norms + 1e-10)
|
|
874
|
+
|
|
875
|
+
# Compute normal using curvature (second derivative)
|
|
876
|
+
curvature_per_point = (6 * u) * (p2 - 2 * p1 + p0).unsqueeze(0) + (
|
|
877
|
+
6 * t_values
|
|
878
|
+
) * (p3 - 2 * p2 + p1).unsqueeze(0)
|
|
879
|
+
|
|
880
|
+
normal = torch.zeros_like(chunk_queries)
|
|
881
|
+
curvature_norms = torch.linalg.norm(
|
|
882
|
+
curvature_per_point, dim=1, keepdim=True
|
|
883
|
+
)
|
|
884
|
+
has_curvature = curvature_norms.squeeze(1) > 1e-10
|
|
885
|
+
|
|
886
|
+
if has_curvature.any():
|
|
887
|
+
normal_curv = curvature_per_point / (curvature_norms + 1e-10)
|
|
888
|
+
proj = torch.sum(normal_curv * tangent, dim=1, keepdim=True) * tangent
|
|
889
|
+
normal_curv = normal_curv - proj
|
|
890
|
+
normal_curv_norms = torch.linalg.norm(normal_curv, dim=1, keepdim=True)
|
|
891
|
+
normal_curv = normal_curv / (normal_curv_norms + 1e-10)
|
|
892
|
+
normal = torch.where(
|
|
893
|
+
has_curvature.unsqueeze(1).expand_as(normal), normal_curv, normal
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
no_curvature = ~has_curvature
|
|
897
|
+
if no_curvature.any():
|
|
898
|
+
try_normal = torch.tensor(
|
|
899
|
+
[0.0, 1.0, 0.0], device=device, dtype=chunk_queries.dtype
|
|
900
|
+
)
|
|
901
|
+
try_normal = try_normal.unsqueeze(0).expand(chunk_queries.shape[0], -1)
|
|
902
|
+
|
|
903
|
+
dot_with_tangent = torch.sum(tangent * try_normal, dim=1)
|
|
904
|
+
|
|
905
|
+
parallel_to_y = torch.abs(dot_with_tangent) > 0.9
|
|
906
|
+
n_base = torch.where(
|
|
907
|
+
parallel_to_y.unsqueeze(1).expand_as(try_normal),
|
|
908
|
+
torch.tensor(
|
|
909
|
+
[1.0, 0.0, 0.0], device=device, dtype=chunk_queries.dtype
|
|
910
|
+
)
|
|
911
|
+
.unsqueeze(0)
|
|
912
|
+
.expand(chunk_queries.shape[0], -1),
|
|
913
|
+
try_normal,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
proj = torch.sum(n_base * tangent, dim=1, keepdim=True) * tangent
|
|
917
|
+
n_perp = n_base - proj
|
|
918
|
+
n_perp_norms = torch.linalg.norm(n_perp, dim=1, keepdim=True)
|
|
919
|
+
n_perp = n_perp / (n_perp_norms + 1e-10)
|
|
920
|
+
|
|
921
|
+
normal = torch.where(
|
|
922
|
+
no_curvature.unsqueeze(1).expand_as(normal), n_perp, normal
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
# Compute binormal
|
|
926
|
+
binormal = torch.linalg.cross(tangent, normal, dim=1)
|
|
927
|
+
binormal_norm = torch.linalg.norm(binormal, dim=1, keepdim=True)
|
|
928
|
+
binormal = binormal / (binormal_norm + 1e-10)
|
|
929
|
+
|
|
930
|
+
# Vector from closest point to query
|
|
931
|
+
vec_to_query = chunk_queries - closest_point
|
|
932
|
+
|
|
933
|
+
# Project onto normal and binormal to get profile coordinates
|
|
934
|
+
coord_n = torch.sum(vec_to_query * normal, dim=1, keepdim=True)
|
|
935
|
+
coord_b = torch.sum(vec_to_query * binormal, dim=1, keepdim=True)
|
|
936
|
+
|
|
937
|
+
# Profile SDF expects 2D points (u, v)
|
|
938
|
+
pts_2d = torch.cat([coord_n, coord_b], dim=1)
|
|
939
|
+
|
|
940
|
+
# Evaluate profile SDF
|
|
941
|
+
profile_dist = self.profile_sdf._compute(pts_2d)
|
|
942
|
+
|
|
943
|
+
chunk_result = profile_dist
|
|
944
|
+
|
|
945
|
+
# Add end caps if requested
|
|
946
|
+
if self.cap_ends:
|
|
947
|
+
start_point = control_points[0]
|
|
948
|
+
end_point = control_points[3]
|
|
949
|
+
|
|
950
|
+
# Tangent at t=0: 3(P1 - P0)
|
|
951
|
+
start_tangent = 3 * (control_points[1] - control_points[0])
|
|
952
|
+
start_tangent_norm = torch.linalg.norm(start_tangent)
|
|
953
|
+
if start_tangent_norm > 1e-10:
|
|
954
|
+
start_tangent = start_tangent / start_tangent_norm
|
|
955
|
+
|
|
956
|
+
# Tangent at t=1: 3(P3 - P2)
|
|
957
|
+
end_tangent = 3 * (control_points[3] - control_points[2])
|
|
958
|
+
end_tangent_norm = torch.linalg.norm(end_tangent)
|
|
959
|
+
if end_tangent_norm > 1e-10:
|
|
960
|
+
end_tangent = end_tangent / end_tangent_norm
|
|
961
|
+
|
|
962
|
+
dist_to_start_plane = torch.sum(
|
|
963
|
+
(chunk_queries - start_point) * start_tangent, dim=1, keepdim=True
|
|
964
|
+
)
|
|
965
|
+
start_cap_dist = -dist_to_start_plane
|
|
966
|
+
|
|
967
|
+
dist_to_end_plane = torch.sum(
|
|
968
|
+
(chunk_queries - end_point) * end_tangent, dim=1, keepdim=True
|
|
969
|
+
)
|
|
970
|
+
end_cap_dist = dist_to_end_plane
|
|
971
|
+
|
|
972
|
+
chunk_result = torch.maximum(chunk_result, start_cap_dist)
|
|
973
|
+
chunk_result = torch.maximum(chunk_result, end_cap_dist)
|
|
974
|
+
|
|
975
|
+
result[i:end_idx] = chunk_result
|
|
976
|
+
|
|
977
|
+
return result
|
|
978
|
+
|
|
979
|
+
def _get_domain_bounds(self) -> torch.Tensor:
|
|
980
|
+
cp = self.trajectory.control_points
|
|
981
|
+
curve_min = cp.min(dim=0).values
|
|
982
|
+
curve_max = cp.max(dim=0).values
|
|
983
|
+
|
|
984
|
+
profile_bounds = self.profile_sdf._get_domain_bounds()
|
|
985
|
+
profile_radius = max(abs(profile_bounds[0, 0]), abs(profile_bounds[1, 0]))
|
|
986
|
+
|
|
987
|
+
lower = curve_min - profile_radius
|
|
988
|
+
upper = curve_max + profile_radius
|
|
989
|
+
|
|
990
|
+
return torch.stack([lower, upper])
|