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.
Files changed (165) hide show
  1. DeepSDFStruct/SDF.py +1734 -0
  2. DeepSDFStruct/__init__.py +66 -0
  3. DeepSDFStruct/deep_sdf/__init__.py +69 -0
  4. DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
  5. DeepSDFStruct/deep_sdf/data.py +258 -0
  6. DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
  7. DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
  8. DeepSDFStruct/deep_sdf/models.py +197 -0
  9. DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
  10. DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
  11. DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
  12. DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
  13. DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
  14. DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
  15. DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
  16. DeepSDFStruct/deep_sdf/plotting.py +143 -0
  17. DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
  18. DeepSDFStruct/deep_sdf/training.py +840 -0
  19. DeepSDFStruct/deep_sdf/workspace.py +363 -0
  20. DeepSDFStruct/design_of_experiments.py +235 -0
  21. DeepSDFStruct/flexicubes/__init__.py +15 -0
  22. DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
  23. DeepSDFStruct/flexicubes/tables.py +2338 -0
  24. DeepSDFStruct/flexisquares/__init__.py +15 -0
  25. DeepSDFStruct/flexisquares/flexisquares.py +967 -0
  26. DeepSDFStruct/flexisquares/tables.py +63 -0
  27. DeepSDFStruct/lattice_structure.py +254 -0
  28. DeepSDFStruct/local_shapes.py +226 -0
  29. DeepSDFStruct/mesh.py +1184 -0
  30. DeepSDFStruct/optimization.py +357 -0
  31. DeepSDFStruct/parametrization.py +215 -0
  32. DeepSDFStruct/pretrained_models.py +128 -0
  33. DeepSDFStruct/sampling.py +670 -0
  34. DeepSDFStruct/sdf_operations.py +990 -0
  35. DeepSDFStruct/sdf_primitives.py +1416 -0
  36. DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
  37. DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
  38. DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
  39. DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
  40. DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
  41. DeepSDFStruct/torch_spline.py +446 -0
  42. DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
  43. DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
  44. DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
  45. DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
  46. DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
  47. DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
  48. DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
  49. DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
  50. DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
  51. DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
  52. DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
  53. DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
  54. DeepSDFStruct/trained_models/primitives/specs.json +46 -0
  55. DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
  56. DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
  57. DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
  58. DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
  59. DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
  60. DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
  61. DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
  62. DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
  63. DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
  64. DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
  65. DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
  66. DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
  67. DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
  68. DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
  69. DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
  70. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
  71. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
  72. DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
  73. DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
  74. DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
  75. DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
  76. DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
  77. DeepSDFStruct/utils.py +96 -0
  78. DeepSDFStruct/visualization/latent_viewer.py +211 -0
  79. DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
  80. benchmarks/generate_sdf_showcase.py +622 -0
  81. benchmarks/sdf_from_mesh_benchmark.py +52 -0
  82. benchmarks/sdf_from_mesh_comparison.md +33 -0
  83. benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
  84. benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
  85. benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
  86. benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
  87. benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
  88. benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
  89. benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
  90. benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
  91. benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
  92. benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
  93. benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
  94. benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
  95. benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
  96. benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
  97. benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
  98. benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
  99. benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
  100. benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
  101. benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
  102. benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
  103. benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
  104. benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
  105. benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
  106. benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
  107. benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
  108. benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
  109. benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
  110. benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
  111. benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
  112. benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
  113. benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
  114. benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
  115. benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
  116. benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
  117. benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
  118. deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
  119. deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
  120. deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
  121. deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
  122. deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
  123. deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
  124. deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
  125. deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
  126. docs/Makefile +20 -0
  127. docs/api_reference.rst +6 -0
  128. docs/conf.py +60 -0
  129. docs/index.rst +136 -0
  130. docs/make.bat +35 -0
  131. docs/qr_code_github.png +0 -0
  132. docs/qr_code_paper.png +0 -0
  133. docs/readme_images/example_output_01.png +0 -0
  134. docs/readme_images/example_output_02.png +0 -0
  135. docs/readme_images/example_output_03.png +0 -0
  136. docs/readme_images/example_output_04.png +0 -0
  137. tests/data/chairs/1005.obj +282 -0
  138. tests/data/chairs/1024.obj +222 -0
  139. tests/data/chairs/README.md +1 -0
  140. tests/data/circular_balls_test_case.stl +0 -0
  141. tests/data/cone.stl +1794 -0
  142. tests/data/example_disconnectd_mesh.inp +1784 -0
  143. tests/data/example_line_mesh.vtk +0 -0
  144. tests/data/stanford_bunny.stl +0 -0
  145. tests/data/sweep_test_case.stl +0 -0
  146. tests/test_DOE.py +49 -0
  147. tests/test_elongate_sdf.py +195 -0
  148. tests/test_flexisquares.py +187 -0
  149. tests/test_generate_dataset.py +76 -0
  150. tests/test_lattice_evaluation.py +137 -0
  151. tests/test_mesh_export.py +137 -0
  152. tests/test_mesh_functions.py +49 -0
  153. tests/test_networks.py +71 -0
  154. tests/test_pretrained_models.py +40 -0
  155. tests/test_reconstruction.py +101 -0
  156. tests/test_sdf_from_mesh.py +52 -0
  157. tests/test_sdf_functions.py +117 -0
  158. tests/test_sdf_primitives.py +668 -0
  159. tests/test_sdf_trimesh_comparison.py +482 -0
  160. tests/test_splinepy_unitcells.py +64 -0
  161. tests/test_structural_optimization.py +178 -0
  162. tests/test_torch_spline.py +216 -0
  163. tests/test_train_model.py +110 -0
  164. tests/test_training_data_ids.py +41 -0
  165. tests/tmp_outputs/temp_file.txt +0 -0
@@ -0,0 +1,1416 @@
1
+ """
2
+ Primitive SDF Shapes
3
+ ====================
4
+
5
+ This module provides basic geometric primitive SDFs that can be used as building
6
+ blocks for more complex geometries. All primitives inherit from SDFBase and can
7
+ be combined using boolean operations.
8
+
9
+ Available Primitives
10
+ --------------------
11
+ - SphereSDF: Sphere with specified center and radius
12
+ - CylinderSDF: Infinite cylinder along a coordinate axis
13
+ - TorusSDF: Torus with major and minor radii
14
+ - PlaneSDF: Half-space defined by a point and normal vector
15
+ - CornerSpheresSDF: Cube with spherical cutouts at corners
16
+ - CrossMsSDF: Cross-shaped structure (intersection of three cylinders)
17
+ - RoundedBoxSDF: 3D axis-aligned box with rounded corners
18
+ - WireframeBoxSDF: 3D wireframe box SDF
19
+ - CapsuleSDF: SDF for a capsule (a line segment with a radius)
20
+ - EllipsoidSDF: SDF for an ellipsoid
21
+ - PyramidSDF: SDF for a pyramid
22
+
23
+ All primitives support PyTorch's automatic differentiation and can be used
24
+ in optimization workflows.
25
+ """
26
+
27
+ from DeepSDFStruct.SDF import SDFBase
28
+ import DeepSDFStruct
29
+ import logging
30
+ import torch
31
+ import numpy as np
32
+
33
+ logger = logging.getLogger(DeepSDFStruct.__name__)
34
+
35
+
36
+ class SphereSDF(SDFBase):
37
+ """Signed distance function for a sphere.
38
+
39
+ Computes the signed distance from query points to a sphere surface.
40
+ The distance is negative inside the sphere, zero on the surface,
41
+ and positive outside.
42
+
43
+ Parameters
44
+ ----------
45
+ center : array-like of shape (3,)
46
+ Center point of the sphere in 3D space.
47
+ radius : float
48
+ Radius of the sphere.
49
+
50
+ Examples
51
+ --------
52
+ >>> # Sphere centered at origin with radius 0.5
53
+ >>> SphereSDF(center=[0, 0, 0], radius=0.5)
54
+ >>>
55
+ >>> # Sphere at different location with larger radius
56
+ >>> SphereSDF(center=[1, 0, 0], radius=1.0)
57
+ """
58
+
59
+ def __init__(self, center, radius):
60
+ super().__init__()
61
+ # make center and radius trainable parameters and ensure correct dtype
62
+ c = torch.as_tensor(center, dtype=torch.float32)
63
+ r = torch.as_tensor(radius, dtype=torch.float32)
64
+ self.center = torch.nn.Parameter(c)
65
+ self.r = torch.nn.Parameter(r.reshape(())) # scalar parameter
66
+
67
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
68
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
69
+ # ensure computations use same dtype/device as queries
70
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
71
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
72
+ return (torch.linalg.norm(queries - center, dim=1) - r).reshape(-1, 1)
73
+
74
+ def _get_domain_bounds(self) -> torch.Tensor:
75
+ # Use same dtype/device as parameters
76
+ center = self.center
77
+ r = self.r
78
+
79
+ # Compute axis-aligned bounding box of the sphere
80
+ lower = center - r
81
+ upper = center + r
82
+
83
+ return torch.stack([lower, upper], dim=0)
84
+
85
+
86
+ class BoxSDF(SDFBase):
87
+ """Signed distance function for a 3D axis-aligned box.
88
+
89
+ The box is defined by its center point and extents (full widths) along each axis.
90
+ Both center and extents are trainable torch parameters.
91
+
92
+ Parameters
93
+ ----------
94
+ center : array-like of shape (3,)
95
+ Center point of the box in 3D space.
96
+ extents : array-like of shape (3,)
97
+ Full widths of the box along x, y, z axes.
98
+
99
+ Examples
100
+ --------
101
+ >>> # Box centered at origin with dimensions 2x1x1
102
+ >>> BoxSDF(center=[0, 0, 0], extents=[2.0, 1.0, 1.0])
103
+ >>>
104
+ >>> # Elongated box in x-direction
105
+ >>> BoxSDF(center=[1, 0, 0], extents=[3.0, 0.5, 0.5])
106
+ """
107
+
108
+ def __init__(self, center, extents):
109
+ """Initialize BoxSDF with center and extents.
110
+
111
+ Parameters
112
+ ----------
113
+ center : array-like of shape (3,)
114
+ Center point of the box in 3D space.
115
+ extents : array-like of shape (3,)
116
+ Full widths of the box along x, y, z axes.
117
+ """
118
+ super().__init__(geometric_dim=3)
119
+
120
+ c = torch.as_tensor(center, dtype=torch.float32)
121
+ e = torch.as_tensor(extents, dtype=torch.float32)
122
+
123
+ if c.numel() != 3 or e.numel() != 3:
124
+ raise ValueError("center and extents must be length-3 for BoxSDF")
125
+
126
+ self.center = torch.nn.Parameter(c.reshape(3))
127
+ self.extents = torch.nn.Parameter(e.reshape(3))
128
+
129
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
130
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
131
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
132
+ half = self.extents.to(device=queries.device, dtype=queries.dtype) / 2.0
133
+
134
+ q = queries - center # (N,3)
135
+ d = torch.abs(q) - half # (N,3)
136
+
137
+ zero = torch.tensor(0.0, device=queries.device, dtype=queries.dtype)
138
+
139
+ # outside distance
140
+ d_clamped = torch.maximum(d, zero)
141
+ outside_dist = torch.linalg.norm(d_clamped, dim=1)
142
+
143
+ # inside distance (negative inside)
144
+ inside_dist = torch.minimum(
145
+ torch.maximum(torch.maximum(d[:, 0], d[:, 1]), d[:, 2]), zero
146
+ )
147
+
148
+ sdf = (outside_dist + inside_dist).reshape(-1, 1)
149
+ return sdf
150
+
151
+ def _get_domain_bounds(self) -> torch.Tensor:
152
+ center = self.center.detach()
153
+ half = self.extents.detach() / 2.0
154
+
155
+ lower = center - half
156
+ upper = center + half
157
+
158
+ return torch.stack([lower, upper], dim=0)
159
+
160
+
161
+ class CylinderSDF(SDFBase):
162
+ """Signed distance function for a finite cylinder with exact end caps.
163
+
164
+ Creates a cylinder defined by two endpoints and a radius.
165
+
166
+ Parameters
167
+ ----------
168
+ point_a : array-like of shape (3,)
169
+ First endpoint of the cylinder's axis.
170
+ point_b : array-like of shape (3,)
171
+ Second endpoint of the cylinder's axis.
172
+ radius : float
173
+ Radius of the cylinder.
174
+
175
+ Examples
176
+ --------
177
+ >>> # Cylinder along z-axis from -1 to 1 with radius 0.5
178
+ >>> CylinderSDF(point_a=[0, 0, -1], point_b=[0, 0, 1], radius=0.5)
179
+ >>>
180
+ >>> # Cylinder along x-axis
181
+ >>> CylinderSDF(point_a=[-1, 0, 0], point_b=[1, 0, 0], radius=0.3)
182
+ >>>
183
+ >>> # Diagonal cylinder
184
+ >>> CylinderSDF(point_a=[0, 0, 0], point_b=[1, 1, 1], radius=0.2)
185
+ """
186
+
187
+ def __init__(self, point_a, point_b, radius):
188
+ super().__init__()
189
+ a = torch.as_tensor(point_a, dtype=torch.float32)
190
+ b = torch.as_tensor(point_b, dtype=torch.float32)
191
+ r = torch.as_tensor(radius, dtype=torch.float32)
192
+ self.point_a = torch.nn.Parameter(a)
193
+ self.point_b = torch.nn.Parameter(b)
194
+ self.radius = torch.nn.Parameter(r.reshape(()))
195
+
196
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
197
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
198
+ a = self.point_a.to(device=queries.device, dtype=queries.dtype)
199
+ b = self.point_b.to(device=queries.device, dtype=queries.dtype)
200
+ r = self.radius.to(device=queries.device, dtype=queries.dtype)
201
+
202
+ ba = b - a
203
+ pa = queries - a
204
+
205
+ # Squared length of ba
206
+ baba = torch.sum(ba**2)
207
+
208
+ # Projection of pa onto ba
209
+ paba = torch.sum(pa * ba, dim=1)
210
+
211
+ # Distance to cylinder side
212
+ # x = length(pa * baba - ba * paba) - radius * baba
213
+ pa_scaled = pa * baba
214
+ ba_paba = ba * paba.unsqueeze(1)
215
+ x = torch.sqrt(torch.sum((pa_scaled - ba_paba) ** 2, dim=1)) - r * baba
216
+
217
+ # Distance to end caps
218
+ # y = |paba - baba * 0.5| - baba * 0.5
219
+ y = torch.abs(paba - baba * 0.5) - baba * 0.5
220
+
221
+ # Combine distances
222
+ x2 = x**2
223
+ y2 = y**2 * baba
224
+
225
+ d = torch.where(
226
+ torch.maximum(x, y) < 0,
227
+ -torch.minimum(x2, y2),
228
+ torch.where(x > 0, x2, torch.tensor(0.0, device=x.device))
229
+ + torch.where(y > 0, y2, torch.tensor(0.0, device=y.device)),
230
+ )
231
+
232
+ return (torch.sign(d) * torch.sqrt(torch.abs(d)) / baba).reshape(-1, 1)
233
+
234
+ def _get_domain_bounds(self) -> torch.Tensor:
235
+ a = self.point_a.detach()
236
+ b = self.point_b.detach()
237
+ r = self.radius.detach()
238
+ lower = torch.minimum(a, b) - r
239
+ upper = torch.maximum(a, b) + r
240
+ return torch.stack([lower, upper])
241
+
242
+
243
+ class ConeSDF(SDFBase):
244
+ """Signed distance function for a finite cone.
245
+
246
+ The cone is defined by:
247
+ - apex point
248
+ - axis direction
249
+ - height
250
+ - base radius
251
+
252
+ Parameters
253
+ ----------
254
+ apexpoint : array-like of shape (3,)
255
+ Apex position of the cone.
256
+ axis : array-like of shape (3,)
257
+ Direction vector of the cone axis.
258
+ radius : float
259
+ Radius at the base of the cone.
260
+ height : float
261
+ Height of the cone from apex to base.
262
+
263
+ Example
264
+ -------
265
+ >>> cone = ConeSDF(apexpoint=[0,0,0], axis=[0,1,0], radius=1.0, height=2.0)
266
+ """
267
+
268
+ def __init__(self, apexpoint, axis, radius, height):
269
+ super().__init__()
270
+ apexpoint = torch.as_tensor(apexpoint, dtype=torch.float32)
271
+ axis = torch.as_tensor(axis, dtype=torch.float32)
272
+ radius = torch.as_tensor([radius], dtype=torch.float32)
273
+ height = torch.as_tensor([height], dtype=torch.float32)
274
+ self.point = torch.nn.Parameter(apexpoint)
275
+ self.axis = torch.nn.Parameter(axis)
276
+ self.radius = torch.nn.Parameter(radius)
277
+ self.height = torch.nn.Parameter(height)
278
+ self.geometric_dim = 3
279
+
280
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
281
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
282
+ point = self.point.to(device=queries.device, dtype=queries.dtype)
283
+ axis = self.axis.to(device=queries.device, dtype=queries.dtype)
284
+ radius = self.radius.to(device=queries.device, dtype=queries.dtype)
285
+ height = self.height.to(device=queries.device, dtype=queries.dtype)
286
+
287
+ # Normalize axis
288
+ axis_norm = torch.linalg.norm(axis)
289
+ if axis_norm == 0:
290
+ raise ValueError("Cone axis vector must be non-zero")
291
+ axis_unit = axis / axis_norm
292
+
293
+ # Vector from apex to query points
294
+ diff = queries - point # (N,3)
295
+
296
+ # Axial projection
297
+ y = torch.sum(diff * axis_unit, dim=1, keepdim=True) # (N,1)
298
+
299
+ # Radial component
300
+ radial_vec = diff - y * axis_unit
301
+ x = torch.linalg.norm(radial_vec, dim=1, keepdim=True)
302
+
303
+ # Linear radius scaling along height
304
+ # radius(y) = (y/h) * r
305
+ k = radius / height
306
+ r_at_y = k * y
307
+
308
+ # Distance to infinite cone surface
309
+ d_side = x - r_at_y
310
+
311
+ # Distance to base plane
312
+ d_base = y - height
313
+
314
+ # Distance to apex cap (prevent below apex)
315
+ d_apex = -y
316
+
317
+ # Outside distance (Euclidean corner handling)
318
+ external = torch.linalg.norm(
319
+ torch.clamp(torch.cat([d_side, d_base], dim=1), min=0), dim=1, keepdim=True
320
+ )
321
+
322
+ # Inside distance
323
+ internal = torch.clamp(torch.max(d_side, torch.max(d_base, d_apex)), max=0)
324
+
325
+ return external + internal
326
+
327
+ def _get_domain_bounds(self) -> torch.Tensor:
328
+ """
329
+ Conservative bounding box.
330
+ """
331
+ extent = max(self.radius, self.height)
332
+ min_bound = self.point - extent
333
+ max_bound = self.point + extent
334
+ return torch.stack([min_bound, max_bound])
335
+
336
+
337
+ class TorusSDF(SDFBase):
338
+ """Signed distance function for a torus (doughnut shape).
339
+
340
+ Creates a torus defined by its center, orientation axis, and two radii.
341
+ The torus is rotationally symmetric around the axis direction.
342
+
343
+ Parameters
344
+ ----------
345
+ center : array-like of shape (3,)
346
+ Center point of the torus in 3D space.
347
+ axis : array-like of shape (3,)
348
+ Axis vector normal to the torus ring plane. The torus is
349
+ rotationally symmetric around this axis. Will be normalized.
350
+ major_radius : float
351
+ Distance from the center to the tube center (R). This is the
352
+ radius of the ring itself. Must be positive.
353
+ minor_radius : float
354
+ Radius of the tube cross-section (r). This is the thickness
355
+ of the ring. Must be positive and typically less than major_radius.
356
+
357
+ Examples
358
+ --------
359
+ >>> # Torus centered at origin, lying in XY plane (axis along Z)
360
+ >>> TorusSDF(center=[0, 0, 0], axis=[0, 0, 1], major_radius=1.0, minor_radius=0.2)
361
+ >>>
362
+ >>> # Tilted torus with axis at an angle
363
+ >>> TorusSDF(center=[0, 0, 0], axis=[1, 0, 1], major_radius=1.0, minor_radius=0.1)
364
+
365
+ Notes
366
+ -----
367
+ - The torus lies in a plane perpendicular to the axis vector
368
+ - major_radius > minor_radius creates a ring torus (standard doughnut)
369
+ - major_radius = minor_radius creates a horn torus (hole closes)
370
+ - major_radius < minor_radius creates a self-intersecting spindle torus
371
+ - All parameters are trainable for optimization
372
+ """
373
+
374
+ def __init__(self, center, axis, major_radius, minor_radius):
375
+ """Initialize TorusSDF.
376
+
377
+ Parameters
378
+ ----------
379
+ center : array-like of shape (3,)
380
+ Center of the torus.
381
+ axis : array-like of shape (3,)
382
+ Axis normal to the torus ring plane.
383
+ major_radius : float
384
+ Distance from center to tube center (R).
385
+ minor_radius : float
386
+ Tube radius (r).
387
+ """
388
+ super().__init__()
389
+ # store as trainable parameters
390
+ c = torch.as_tensor(center, dtype=torch.float32)
391
+ a = torch.as_tensor(axis, dtype=torch.float32)
392
+ R = torch.as_tensor(major_radius, dtype=torch.float32)
393
+ r = torch.as_tensor(minor_radius, dtype=torch.float32)
394
+
395
+ self.center = torch.nn.Parameter(c)
396
+ self.axis = torch.nn.Parameter(a)
397
+ self.R = torch.nn.Parameter(R.reshape(())) # scalar
398
+ self.r = torch.nn.Parameter(r.reshape(())) # scalar
399
+
400
+ self.geometric_dim = 3
401
+
402
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
403
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
404
+ # align dtype/device
405
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
406
+ axis = self.axis.to(device=queries.device, dtype=queries.dtype)
407
+ R = self.R.to(device=queries.device, dtype=queries.dtype)
408
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
409
+
410
+ # normalize axis
411
+ axis_norm = torch.linalg.norm(axis)
412
+ if axis_norm.item() == 0:
413
+ raise ValueError("Torus axis must be non-zero")
414
+ axis_unit = axis / axis_norm
415
+
416
+ diff = queries - center # (N,3)
417
+
418
+ # axial projection (height from ring plane)
419
+ y = torch.sum(diff * axis_unit, dim=1, keepdim=True) # (N,1)
420
+
421
+ # radial length in ring plane
422
+ radial_vec = diff - y * axis_unit
423
+ radial_len = torch.linalg.norm(radial_vec, dim=1, keepdim=True) # (N,1)
424
+
425
+ q = torch.cat([radial_len - R, y], dim=1) # (N,2)
426
+ out = torch.linalg.norm(q, dim=1, keepdim=True) - r # (N,1)
427
+ return out.reshape(-1, 1)
428
+
429
+ def _get_domain_bounds(self) -> torch.Tensor:
430
+ # tight AABB accounting for torus orientation
431
+ center = self.center
432
+ R = self.R
433
+ r = self.r
434
+
435
+ # normalize axis
436
+ axis = self.axis
437
+ axis_unit = axis / torch.linalg.norm(axis)
438
+
439
+ # extent along axis direction is only the tube radius
440
+ extent_along_axis = r
441
+
442
+ # extent perpendicular to axis is major + minor radius
443
+ extent_perp = R + r
444
+
445
+ # For each coordinate axis i, the extent is:
446
+ # sqrt(extent_perp^2 * (1 - axis[i]^2) + extent_along^2 * axis[i]^2)
447
+ # This accounts for how much the torus extends in each coordinate direction
448
+ axis_sq = axis_unit**2
449
+ extent = torch.sqrt(
450
+ extent_perp**2 * (1 - axis_sq) + extent_along_axis**2 * axis_sq
451
+ )
452
+
453
+ lower = center - extent
454
+ upper = center + extent
455
+ return torch.stack([lower, upper], dim=0)
456
+
457
+
458
+ class PlaneSDF(SDFBase):
459
+ """Signed distance function for a half-space defined by a point and normal vector.
460
+
461
+ Computes the signed distance from query points to a plane. The distance is
462
+ negative on the side the normal points toward, zero on the plane, and
463
+ positive on the opposite side.
464
+
465
+ Parameters
466
+ ----------
467
+ point : array-like of shape (3,)
468
+ A point on the plane.
469
+ normal : array-like of shape (3,)
470
+ Normal vector defining the plane orientation (will be normalized).
471
+
472
+ Examples
473
+ --------
474
+ >>> from DeepSDFStruct.sdf_primitives import PlaneSDF
475
+ >>> import torch
476
+ >>>
477
+ >>> # Plane at z=0 with normal pointing up
478
+ >>> plane = PlaneSDF(point=[0, 0, 0], normal=[0, 0, 1])
479
+ >>> points = torch.tensor([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0]])
480
+ >>> distances = plane(points)
481
+ >>> print(distances) # [0.0, 1.0, -1.0]
482
+ """
483
+
484
+ def __init__(self, point, normal):
485
+ """Initialize PlaneSDF with a point and normal vector.
486
+
487
+ Parameters
488
+ ----------
489
+ point : array-like of shape (3,)
490
+ A point on the plane.
491
+ normal : array-like of shape (3,)
492
+ Normal vector defining the plane orientation.
493
+ """
494
+ super().__init__()
495
+ self.point = torch.tensor(point, dtype=torch.float32)
496
+ self.normal = torch.tensor(normal, dtype=torch.float32)
497
+ self.normal = self.normal / torch.linalg.norm(self.normal)
498
+
499
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
500
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
501
+ return torch.matmul(queries - self.point, self.normal).reshape(-1, 1)
502
+
503
+ def _get_domain_bounds(self) -> torch.Tensor:
504
+ return torch.tensor([[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]])
505
+
506
+
507
+ class CornerSpheresSDF(SDFBase):
508
+ """Signed distance function for a cube with spherical cutouts at the corners.
509
+
510
+ Creates a cube with rounded corners by subtracting spheres from each of
511
+ the 8 corners. The result is a cube-like shape with smooth corner transitions.
512
+
513
+ Parameters
514
+ ----------
515
+ radius : float
516
+ Radius of the spherical cutouts at each corner.
517
+ limit : float, default 1.0
518
+ Half-size of the cube (distance from center to each face).
519
+
520
+ Examples
521
+ --------
522
+ >>> from DeepSDFStruct.sdf_primitives import CornerSpheresSDF
523
+ >>> import torch
524
+ >>>
525
+ >>> corner_spheres = CornerSpheresSDF(radius=0.3, limit=1.0)
526
+ >>> points = torch.tensor([[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]])
527
+ >>> distances = corner_spheres(points)
528
+ """
529
+
530
+ def __init__(self, radius, limit=1.0):
531
+ """Initialize CornerSpheresSDF with corner sphere radius and cube size.
532
+
533
+ Parameters
534
+ ----------
535
+ radius : float
536
+ Radius of the spherical cutouts at each corner.
537
+ limit : float, default 1.0
538
+ Half-size of the cube.
539
+ """
540
+ super().__init__()
541
+ self.r = radius
542
+ self.limit = limit
543
+
544
+ # define the 8 corners of the cube
545
+ self.corners = torch.tensor(
546
+ [[x, y, z] for x in [-1, 1] for y in [-1, 1] for z in [-1, 1]],
547
+ dtype=torch.float32,
548
+ )
549
+
550
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
551
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
552
+
553
+ # start with the cube SDF
554
+ output = torch.linalg.norm(queries, dim=1, ord=float("inf")) - self.limit
555
+
556
+ # subtract spheres at corners
557
+ for corner in self.corners:
558
+ sphere_like = torch.linalg.norm(queries - corner, dim=1, ord=3) - self.r
559
+ output = torch.maximum(output, -sphere_like)
560
+
561
+ return output.reshape(-1, 1)
562
+
563
+ def _get_domain_bounds(self) -> torch.Tensor:
564
+ return torch.tensor([[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]])
565
+
566
+
567
+ class CrossMsSDF(SDFBase):
568
+ """Signed distance function for a cross-shaped structure.
569
+
570
+ Creates a 3D cross by computing the intersection of three cylinders
571
+ aligned along the x, y, and z axes. The result is a symmetric
572
+ cross-like structure centered at the origin.
573
+
574
+ Parameters
575
+ ----------
576
+ radius : float
577
+ Radius of each cylinder forming the cross.
578
+
579
+ Examples
580
+ --------
581
+ >>> from DeepSDFStruct.sdf_primitives import CrossMsSDF
582
+ >>> import torch
583
+ >>>
584
+ >>> cross = CrossMsSDF(radius=0.2)
585
+ >>> points = torch.tensor([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])
586
+ >>> distances = cross(points)
587
+ """
588
+
589
+ def __init__(self, radius):
590
+ """Initialize CrossMsSDF with cylinder radius.
591
+
592
+ Parameters
593
+ ----------
594
+ radius : float
595
+ Radius of each cylinder.
596
+ """
597
+ super().__init__()
598
+ self.r = radius
599
+
600
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
601
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
602
+ # start with the L∞ norm
603
+ output = torch.linalg.norm(queries, dim=1, ord=float("inf"))
604
+
605
+ # x-axis cylinder
606
+ cylinder_x = torch.sqrt(queries[:, 1] ** 2 + queries[:, 2] ** 2) - self.r
607
+ output = torch.minimum(output, cylinder_x)
608
+
609
+ # y-axis cylinder
610
+ cylinder_y = torch.sqrt(queries[:, 0] ** 2 + queries[:, 2] ** 2) - self.r
611
+ output = torch.minimum(output, cylinder_y)
612
+
613
+ # z-axis cylinder
614
+ cylinder_z = torch.sqrt(queries[:, 0] ** 2 + queries[:, 1] ** 2) - self.r
615
+ output = torch.minimum(output, cylinder_z)
616
+
617
+ return output.reshape(-1, 1)
618
+
619
+ def _get_domain_bounds(self) -> torch.Tensor:
620
+ return torch.tensor([[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]])
621
+
622
+
623
+ class RoundedBoxSDF(SDFBase):
624
+ """3D axis-aligned box with rounded corners."""
625
+
626
+ def __init__(self, center, extents, radius):
627
+ super().__init__(geometric_dim=3)
628
+ c = torch.as_tensor(center, dtype=torch.float32)
629
+ e = torch.as_tensor(extents, dtype=torch.float32)
630
+ r = torch.as_tensor(radius, dtype=torch.float32)
631
+ self.center = torch.nn.Parameter(c.reshape(3))
632
+ self.extents = torch.nn.Parameter(e.reshape(3))
633
+ self.radius = torch.nn.Parameter(r.reshape(()))
634
+
635
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
636
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
637
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
638
+ extents = self.extents.to(device=queries.device, dtype=queries.dtype)
639
+ radius = self.radius.to(device=queries.device, dtype=queries.dtype)
640
+
641
+ q = torch.abs(queries - center) - extents / 2.0 + radius
642
+
643
+ outside_dist = torch.linalg.norm(torch.clamp(q, min=0.0), dim=1)
644
+ inside_dist = torch.minimum(torch.max(q, dim=1).values, torch.tensor(0.0))
645
+
646
+ return (outside_dist + inside_dist - radius).reshape(-1, 1)
647
+
648
+ def _get_domain_bounds(self) -> torch.Tensor:
649
+ center = self.center.detach()
650
+ half = self.extents.detach() / 2.0
651
+ lower = center - half
652
+ upper = center + half
653
+ return torch.stack([lower, upper], dim=0)
654
+
655
+
656
+ class WireframeBoxSDF(SDFBase):
657
+ """3D wireframe box SDF."""
658
+
659
+ def __init__(self, center, extents, thickness):
660
+ super().__init__(geometric_dim=3)
661
+ c = torch.as_tensor(center, dtype=torch.float32)
662
+ e = torch.as_tensor(extents, dtype=torch.float32)
663
+ t = torch.as_tensor(thickness, dtype=torch.float32)
664
+ self.center = torch.nn.Parameter(c.reshape(3))
665
+ self.extents = torch.nn.Parameter(e.reshape(3))
666
+ self.thickness = torch.nn.Parameter(t.reshape(()))
667
+
668
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
669
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
670
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
671
+ extents = self.extents.to(device=queries.device, dtype=queries.dtype)
672
+ thickness = self.thickness.to(device=queries.device, dtype=queries.dtype)
673
+
674
+ p = torch.abs(queries - center) - extents / 2.0 - thickness / 2.0
675
+ q = torch.abs(p + thickness / 2.0) - thickness / 2.0
676
+
677
+ px, py, pz = p[:, 0], p[:, 1], p[:, 2]
678
+ qx, qy, qz = q[:, 0], q[:, 1], q[:, 2]
679
+
680
+ def g(a, b, c):
681
+ return torch.linalg.norm(
682
+ torch.clamp(torch.stack([a, b, c], dim=1), min=0.0), dim=1
683
+ ) + torch.minimum(torch.max(torch.max(a, b), c), torch.tensor(0.0))
684
+
685
+ return torch.minimum(
686
+ torch.minimum(g(px, qy, qz), g(qx, py, qz)), g(qx, qy, pz)
687
+ ).reshape(-1, 1)
688
+
689
+ def _get_domain_bounds(self) -> torch.Tensor:
690
+ center = self.center.detach()
691
+ half = self.extents.detach() / 2.0
692
+ lower = center - half
693
+ upper = center + half
694
+ return torch.stack([lower, upper], dim=0)
695
+
696
+
697
+ class CapsuleSDF(SDFBase):
698
+ """SDF for a capsule (a line segment with a radius)."""
699
+
700
+ def __init__(self, point_a, point_b, radius):
701
+ super().__init__(geometric_dim=3)
702
+ a = torch.as_tensor(point_a, dtype=torch.float32)
703
+ b = torch.as_tensor(point_b, dtype=torch.float32)
704
+ r = torch.as_tensor(radius, dtype=torch.float32)
705
+ self.point_a = torch.nn.Parameter(a.reshape(3))
706
+ self.point_b = torch.nn.Parameter(b.reshape(3))
707
+ self.radius = torch.nn.Parameter(r.reshape(()))
708
+
709
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
710
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
711
+ point_a = self.point_a.to(device=queries.device, dtype=queries.dtype)
712
+ point_b = self.point_b.to(device=queries.device, dtype=queries.dtype)
713
+ radius = self.radius.to(device=queries.device, dtype=queries.dtype)
714
+
715
+ pa = queries - point_a
716
+ ba = point_b - point_a
717
+
718
+ h = torch.clamp(
719
+ torch.sum(pa * ba, dim=1) / torch.sum(ba * ba), 0.0, 1.0
720
+ ).reshape(-1, 1)
721
+
722
+ return (torch.linalg.norm(pa - h * ba, dim=1) - radius).reshape(-1, 1)
723
+
724
+ def _get_domain_bounds(self) -> torch.Tensor:
725
+ a = self.point_a.detach()
726
+ b = self.point_b.detach()
727
+ r = self.radius.detach()
728
+ lower = torch.min(a, b) - r
729
+ upper = torch.max(a, b) + r
730
+ return torch.stack([lower, upper], dim=0)
731
+
732
+
733
+ class EllipsoidSDF(SDFBase):
734
+ """SDF for an ellipsoid."""
735
+
736
+ def __init__(self, center, extents):
737
+ super().__init__(geometric_dim=3)
738
+ c = torch.as_tensor(center, dtype=torch.float32)
739
+ e = torch.as_tensor(extents, dtype=torch.float32)
740
+ self.center = torch.nn.Parameter(c.reshape(3))
741
+ self.extents = torch.nn.Parameter(e.reshape(3))
742
+
743
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
744
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
745
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
746
+ extents = self.extents.to(device=queries.device, dtype=queries.dtype)
747
+
748
+ p = queries - center
749
+ k0 = torch.linalg.norm(p / extents, dim=1)
750
+ k1 = torch.linalg.norm(p / (extents * extents), dim=1)
751
+
752
+ return (k0 * (k0 - 1.0) / k1).reshape(-1, 1)
753
+
754
+ def _get_domain_bounds(self) -> torch.Tensor:
755
+ center = self.center.detach()
756
+ extents = self.extents.detach()
757
+ lower = center - extents
758
+ upper = center + extents
759
+ return torch.stack([lower, upper], dim=0)
760
+
761
+
762
+ class PyramidSDF(SDFBase):
763
+ """SDF for a pyramid."""
764
+
765
+ def __init__(self, height):
766
+ super().__init__(geometric_dim=3)
767
+ h = torch.as_tensor(height, dtype=torch.float32)
768
+ self.height = torch.nn.Parameter(h.reshape(()))
769
+
770
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
771
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
772
+ h = self.height.to(device=queries.device, dtype=queries.dtype)
773
+
774
+ a = torch.abs(queries[:, :2]) - 0.5
775
+
776
+ # This part is tricky to translate directly without a direct equivalent of the numpy advanced indexing in torch
777
+ # For now, let's stick to a simplified version or assume a square base.
778
+ # The original implementation handles non-square bases by swapping coordinates.
779
+
780
+ px = a[:, 0]
781
+ pz = a[:, 1]
782
+ py = queries[:, 2]
783
+
784
+ m2 = h * h + 0.25
785
+
786
+ qx = pz
787
+ qy = h * py - 0.5 * px
788
+ qz = h * px + 0.5 * py
789
+
790
+ s = torch.clamp(-qx, min=0.0)
791
+ t = torch.clamp((qy - 0.5 * pz) / (m2 + 0.25), 0.0, 1.0)
792
+
793
+ a_dist = m2 * (qx + s) ** 2 + qy**2
794
+ b_dist = m2 * (qx + 0.5 * t) ** 2 + (qy - m2 * t) ** 2
795
+
796
+ d2 = torch.where(
797
+ (qy < 0) & (-qx * m2 - qy * 0.5 < 0),
798
+ torch.min(a_dist, b_dist),
799
+ torch.tensor(0.0, device=queries.device),
800
+ )
801
+
802
+ return (
803
+ torch.sqrt((d2 + qz**2) / m2) * torch.sign(torch.max(qz, -py))
804
+ ).reshape(-1, 1)
805
+
806
+ def _get_domain_bounds(self) -> torch.Tensor:
807
+ h = self.height.detach()
808
+ return torch.tensor([[-0.5, -0.5, 0], [0.5, 0.5, h]])
809
+
810
+
811
+ class CircleSDF(SDFBase):
812
+ """SDF for a 2D circle."""
813
+
814
+ def __init__(self, center, radius):
815
+ super().__init__(geometric_dim=2)
816
+ self.center = torch.nn.Parameter(torch.as_tensor(center, dtype=torch.float32))
817
+ self.radius = torch.nn.Parameter(
818
+ torch.as_tensor(radius, dtype=torch.float32).reshape(())
819
+ )
820
+
821
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
822
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
823
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
824
+ radius = self.radius.to(device=queries.device, dtype=queries.dtype)
825
+ return (torch.linalg.norm(queries - center, dim=1) - radius).reshape(-1, 1)
826
+
827
+ def _get_domain_bounds(self) -> torch.Tensor:
828
+ r = self.radius
829
+ c = self.center
830
+ return torch.stack([c - r, c + r])
831
+
832
+
833
+ class RectangleSDF(SDFBase):
834
+ """SDF for a 2D rectangle with center and extents."""
835
+
836
+ def __init__(self, center, extents):
837
+ super().__init__(geometric_dim=2)
838
+ self.center = torch.nn.Parameter(torch.as_tensor(center, dtype=torch.float32))
839
+ self.extents = torch.nn.Parameter(torch.as_tensor(extents, dtype=torch.float32))
840
+
841
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
842
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
843
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
844
+ half = self.extents.to(device=queries.device, dtype=queries.dtype) / 2.0
845
+ q = torch.abs(queries - center) - half
846
+ outside = torch.linalg.norm(torch.clamp(q, min=0.0), dim=1)
847
+ inside = torch.minimum(torch.maximum(q[:, 0], q[:, 1]), torch.tensor(0.0))
848
+ return (outside + inside).reshape(-1, 1)
849
+
850
+ def _get_domain_bounds(self) -> torch.Tensor:
851
+ c = self.center
852
+ e = self.extents
853
+ return torch.stack([c - e / 2, c + e / 2])
854
+
855
+
856
+ class LineSDF(SDFBase):
857
+ """SDF for a 2D line (infinite line defined by normal vector and point)."""
858
+
859
+ def __init__(self, normal, point):
860
+ super().__init__(geometric_dim=2)
861
+ self.normal = torch.nn.Parameter(torch.as_tensor(normal, dtype=torch.float32))
862
+ self.point = torch.nn.Parameter(torch.as_tensor(point, dtype=torch.float32))
863
+
864
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
865
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
866
+ n = self.normal.to(device=queries.device, dtype=queries.dtype)
867
+ p = self.point.to(device=queries.device, dtype=queries.dtype)
868
+ n_normalized = n / torch.linalg.norm(n)
869
+ return torch.matmul(queries - p, n_normalized).reshape(-1, 1)
870
+
871
+ def _get_domain_bounds(self) -> torch.Tensor:
872
+ return torch.tensor([[-1.0, -1.0], [1.0, 1.0]])
873
+
874
+
875
+ class RoundedRectangleSDF(SDFBase):
876
+ """SDF for a 2D rectangle with rounded corners."""
877
+
878
+ def __init__(self, center, extents, radius):
879
+ super().__init__(geometric_dim=2)
880
+ self.center = torch.nn.Parameter(torch.as_tensor(center, dtype=torch.float32))
881
+ self.extents = torch.nn.Parameter(torch.as_tensor(extents, dtype=torch.float32))
882
+
883
+ # Support single radius or tuple of 4 radii (one per quadrant)
884
+ if isinstance(radius, (tuple, list)):
885
+ if len(radius) == 1:
886
+ self.radii = torch.nn.Parameter(
887
+ torch.as_tensor([radius[0]] * 4, dtype=torch.float32)
888
+ )
889
+ elif len(radius) == 4:
890
+ self.radii = torch.nn.Parameter(
891
+ torch.as_tensor(radius, dtype=torch.float32)
892
+ )
893
+ else:
894
+ raise ValueError("radius must be a single value or a tuple of 4 values")
895
+ else:
896
+ self.radii = torch.nn.Parameter(
897
+ torch.as_tensor([radius] * 4, dtype=torch.float32)
898
+ )
899
+
900
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
901
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
902
+ center = self.center.to(device=queries.device, dtype=queries.dtype)
903
+ extents = self.extents.to(device=queries.device, dtype=queries.dtype)
904
+ radii = self.radii.to(device=queries.device, dtype=queries.dtype)
905
+
906
+ q = torch.abs(queries - center) - extents / 2.0
907
+
908
+ # Select appropriate radius based on quadrant
909
+ x, y = q[:, 0], q[:, 1]
910
+ zeros = torch.zeros_like(x)
911
+ r0 = torch.where((x > 0) & (y > 0), radii[0], zeros)
912
+ r1 = torch.where((x > 0) & (y <= 0), radii[1], zeros)
913
+ r2 = torch.where((x <= 0) & (y <= 0), radii[2], zeros)
914
+ r3 = torch.where((x <= 0) & (y > 0), radii[3], zeros)
915
+ r = r0 + r1 + r2 + r3
916
+
917
+ q = q + r.unsqueeze(1)
918
+ outside = torch.linalg.norm(torch.clamp(q, min=0.0), dim=1)
919
+ inside = torch.minimum(torch.maximum(q[:, 0], q[:, 1]), torch.tensor(0.0))
920
+ return (outside + inside - r).reshape(-1, 1)
921
+
922
+ def _get_domain_bounds(self) -> torch.Tensor:
923
+ c = self.center
924
+ e = self.extents
925
+ return torch.stack([c - e / 2, c + e / 2])
926
+
927
+
928
+ class EquilateralTriangleSDF(SDFBase):
929
+ """SDF for a regular equilateral triangle."""
930
+
931
+ def __init__(self, size=1.0):
932
+ super().__init__(geometric_dim=2)
933
+ self.size = torch.nn.Parameter(
934
+ torch.as_tensor(size, dtype=torch.float32).reshape(())
935
+ )
936
+
937
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
938
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
939
+ size = self.size.to(device=queries.device, dtype=queries.dtype)
940
+ r = size
941
+ k = torch.tensor(np.sqrt(3.0), device=queries.device, dtype=queries.dtype)
942
+ p = queries.clone()
943
+
944
+ # Apply abs and offset
945
+ p[:, 0] = torch.abs(p[:, 0]) - r
946
+ p[:, 1] = p[:, 1] + r / k
947
+
948
+ # Conditional transformation
949
+ w = (p[:, 0] + k * p[:, 1]) > 0
950
+ q = torch.stack([p[:, 0] - k * p[:, 1], -k * p[:, 0] - p[:, 1]], dim=1) / 2.0
951
+ p = torch.where(w.unsqueeze(1), q, p)
952
+
953
+ # Clamp x
954
+ p[:, 0] = p[:, 0] - torch.clamp(p[:, 0], min=-2.0 * r, max=torch.zeros_like(r))
955
+
956
+ # Compute signed distance
957
+ return (-torch.linalg.norm(p, dim=1) * torch.sign(p[:, 1])).reshape(-1, 1)
958
+
959
+ def _get_domain_bounds(self) -> torch.Tensor:
960
+ s = self.size
961
+ return torch.tensor([[-s, -s], [s, s]])
962
+
963
+
964
+ class HexagonSDF(SDFBase):
965
+ """SDF for a regular hexagon."""
966
+
967
+ def __init__(self, size=1.0):
968
+ super().__init__(geometric_dim=2)
969
+ self.size = torch.nn.Parameter(
970
+ torch.as_tensor(size, dtype=torch.float32).reshape(())
971
+ )
972
+
973
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
974
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
975
+ r = self.size.to(device=queries.device, dtype=queries.dtype)
976
+ k = torch.tensor(
977
+ [-np.sqrt(3.0) / 2.0, 0.5, np.tan(np.pi / 6.0)],
978
+ device=queries.device,
979
+ dtype=queries.dtype,
980
+ )
981
+ p = torch.abs(queries)
982
+ p = p - 2.0 * torch.clamp(torch.sum(p * k[:2], dim=1, keepdim=True), max=0) * k[
983
+ :2
984
+ ].unsqueeze(0)
985
+ p = p - torch.stack(
986
+ [
987
+ torch.clamp(p[:, 0], min=-k[2] * r, max=k[2] * r),
988
+ torch.ones_like(p[:, 0]) * r,
989
+ ],
990
+ dim=1,
991
+ )
992
+ return (torch.linalg.norm(p, dim=1) * torch.sign(p[:, 1])).reshape(-1, 1)
993
+
994
+ def _get_domain_bounds(self) -> torch.Tensor:
995
+ s = self.size
996
+ return torch.tensor([[-s, -s], [s, s]])
997
+
998
+
999
+ class PolygonSDF(SDFBase):
1000
+ """SDF for a general convex polygon defined by vertices."""
1001
+
1002
+ def __init__(self, vertices):
1003
+ super().__init__(geometric_dim=2)
1004
+ if len(vertices) < 3:
1005
+ raise ValueError("Polygon needs at least 3 vertices")
1006
+ self.vertices = torch.nn.Parameter(
1007
+ torch.as_tensor(vertices, dtype=torch.float32)
1008
+ )
1009
+
1010
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1011
+ """
1012
+ Signed distance for convex polygon using winding number method.
1013
+ Returns negative values for points inside the polygon.
1014
+ """
1015
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1016
+ points = self.vertices.to(device=queries.device, dtype=queries.dtype)
1017
+ p = queries
1018
+ n = len(points)
1019
+
1020
+ # Initialize distance squared to distance from first vertex
1021
+ d = torch.sum((p - points[0]) ** 2, dim=1)
1022
+
1023
+ # Initialize sign (positive = outside)
1024
+ s = torch.ones(len(p), device=p.device, dtype=p.dtype)
1025
+
1026
+ for i in range(n):
1027
+ j = (i + n - 1) % n
1028
+ vi = points[i]
1029
+ vj = points[j]
1030
+ e = vj - vi
1031
+ w = p - vi
1032
+
1033
+ # Compute distance to edge segment
1034
+ # Project w onto e, clamp to [0, 1], then compute perpendicular distance
1035
+ e_len_sq = torch.sum(e**2)
1036
+ if e_len_sq > 1e-12:
1037
+ t = torch.clamp(torch.sum(w * e, dim=1) / e_len_sq, 0, 1)
1038
+ b = w - t.unsqueeze(1) * e
1039
+ d = torch.minimum(d, torch.sum(b**2, dim=1))
1040
+
1041
+ # Winding number computation using cross products
1042
+ # Check conditions: (p.y >= vi.y), (p.y < vj.y), (e.x * w.y > e.y * w.x)
1043
+ c1 = p[:, 1] >= vi[1]
1044
+ c2 = p[:, 1] < vj[1]
1045
+ c3 = e[0] * w[:, 1] > e[1] * w[:, 0]
1046
+
1047
+ # Flip sign if point crosses edge (all conditions true or all false)
1048
+ all_c = c1 & c2 & c3
1049
+ all_not_c = (~c1) & (~c2) & (~c3)
1050
+ condition = all_c | all_not_c
1051
+ s = torch.where(condition, -s, s)
1052
+
1053
+ return (s * torch.sqrt(d)).reshape(-1, 1)
1054
+
1055
+ def _get_domain_bounds(self) -> torch.Tensor:
1056
+ v = self.vertices.detach()
1057
+ return torch.stack([v.min(dim=0).values, v.max(dim=0).values])
1058
+
1059
+
1060
+ class SlabSDF(SDFBase):
1061
+ """SDF for a slab (clipping box defined by axis-aligned planes)."""
1062
+
1063
+ def __init__(self, x0=None, y0=None, z0=None, x1=None, y1=None, z1=None):
1064
+ super().__init__()
1065
+ # Bounds for each axis
1066
+ self.bounds = {}
1067
+ if x0 is not None:
1068
+ self.bounds["x0"] = (
1069
+ float(x0) if not isinstance(x0, torch.nn.Parameter) else x0
1070
+ )
1071
+ if x1 is not None:
1072
+ self.bounds["x1"] = (
1073
+ float(x1) if not isinstance(x1, torch.nn.Parameter) else x1
1074
+ )
1075
+ if y0 is not None:
1076
+ self.bounds["y0"] = (
1077
+ float(y0) if not isinstance(y0, torch.nn.Parameter) else y0
1078
+ )
1079
+ if y1 is not None:
1080
+ self.bounds["y1"] = (
1081
+ float(y1) if not isinstance(y1, torch.nn.Parameter) else y1
1082
+ )
1083
+ if z0 is not None:
1084
+ self.bounds["z0"] = (
1085
+ float(z0) if not isinstance(z0, torch.nn.Parameter) else z0
1086
+ )
1087
+ if z1 is not None:
1088
+ self.bounds["z1"] = (
1089
+ float(z1) if not isinstance(z1, torch.nn.Parameter) else z1
1090
+ )
1091
+
1092
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1093
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1094
+ dist = torch.full(
1095
+ (len(queries),), -float("inf"), device=queries.device, dtype=queries.dtype
1096
+ )
1097
+
1098
+ # For each plane constraint
1099
+ for axis, side, bound_key, direction in [
1100
+ (torch.tensor([1, 0, 0], dtype=torch.float32), 0, "x0", 1),
1101
+ (torch.tensor([1, 0, 0], dtype=torch.float32), 1, "x1", -1),
1102
+ (torch.tensor([0, 1, 0], dtype=torch.float32), 0, "y0", 1),
1103
+ (torch.tensor([0, 1, 0], dtype=torch.float32), 1, "y1", -1),
1104
+ (torch.tensor([0, 0, 1], dtype=torch.float32), 0, "z0", 1),
1105
+ (torch.tensor([0, 0, 1], dtype=torch.float32), 1, "z1", -1),
1106
+ ]:
1107
+ bound = self.bounds.get(bound_key)
1108
+ if bound is None:
1109
+ continue
1110
+
1111
+ idx = 0
1112
+ if torch.all(axis == torch.tensor([1, 0, 0])):
1113
+ idx = 0
1114
+ elif torch.all(axis == torch.tensor([0, 1, 0])):
1115
+ idx = 1
1116
+ elif torch.all(axis == torch.tensor([0, 0, 1])):
1117
+ idx = 2
1118
+ if side == 0:
1119
+ plane_dist = bound - queries[:, idx]
1120
+ dist = torch.maximum(dist, plane_dist)
1121
+ else:
1122
+ plane_dist = queries[:, idx] - bound
1123
+ dist = torch.maximum(dist, plane_dist)
1124
+
1125
+ return dist.reshape(-1, 1)
1126
+
1127
+ def _get_domain_bounds(self) -> torch.Tensor:
1128
+ return torch.tensor([[-1e9, -1e9, -1e9], [1e9, 1e9, 1e9]])
1129
+
1130
+
1131
+ class RoundedCylinderSDF(SDFBase):
1132
+ """SDF for a cylinder with smooth rounded transitions."""
1133
+
1134
+ def __init__(self, ra, rb, h):
1135
+ super().__init__()
1136
+ self.ra = torch.nn.Parameter(torch.as_tensor(ra, dtype=torch.float32))
1137
+ self.rb = torch.nn.Parameter(torch.as_tensor(rb, dtype=torch.float32))
1138
+ self.h = torch.nn.Parameter(torch.as_tensor(h, dtype=torch.float32))
1139
+
1140
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1141
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1142
+ ra = self.ra.to(device=queries.device, dtype=queries.dtype)
1143
+ rb = self.rb.to(device=queries.device, dtype=queries.dtype)
1144
+ h = self.h.to(device=queries.device, dtype=queries.dtype)
1145
+
1146
+ d = torch.stack(
1147
+ [
1148
+ torch.sqrt(queries[:, 0] ** 2 + queries[:, 1] ** 2) - ra + rb,
1149
+ torch.abs(queries[:, 2]) - h / 2 + rb,
1150
+ ],
1151
+ dim=1,
1152
+ )
1153
+
1154
+ outside = torch.linalg.norm(torch.clamp(d, min=0.0), dim=1)
1155
+ inside = torch.minimum(torch.maximum(d[:, 0], d[:, 1]), torch.tensor(0.0))
1156
+ return (outside + inside - rb).reshape(-1, 1)
1157
+
1158
+ def _get_domain_bounds(self) -> torch.Tensor:
1159
+ r = max(self.ra.detach().item(), self.rb.detach().item())
1160
+ h = self.h.detach().item()
1161
+ return torch.tensor([[-r, -r, -h / 2], [r, r, h / 2]])
1162
+
1163
+
1164
+ class CappedConeSDF(SDFBase):
1165
+ """Signed distance function for a finite cone with exact end caps.
1166
+
1167
+ Creates a cone frustum (or full cone if rb=0) defined by two endpoints
1168
+ and radii at each end. The cone has flat end caps at both ends.
1169
+
1170
+ Parameters
1171
+ ----------
1172
+ point_a : array-like of shape (3,)
1173
+ First endpoint of the cone axis (base center).
1174
+ point_b : array-like of shape (3,)
1175
+ Second endpoint of the cone axis (top center).
1176
+ ra : float
1177
+ Radius at point_a (base radius). Must be non-negative.
1178
+ rb : float
1179
+ Radius at point_b (top radius). Use 0 for a sharp cone tip.
1180
+
1181
+ Examples
1182
+ --------
1183
+ >>> # Cone from (0,0,0) to (0,0,2) with base radius 0.5 and sharp tip
1184
+ >>> CappedConeSDF(point_a=[0, 0, 0], point_b=[0, 0, 2], ra=0.5, rb=0.0)
1185
+ >>>
1186
+ >>> # Cone frustum with radii at both ends
1187
+ >>> CappedConeSDF(point_a=[0, 0, 0], point_b=[0, 0, 1], ra=0.3, rb=0.1)
1188
+
1189
+ Notes
1190
+ -----
1191
+ - The cone axis is the line segment from point_a to point_b
1192
+ - ra and rb control the radii at each end independently
1193
+ - Set rb=0 for a cone with a sharp tip
1194
+ - Set ra=rb for a cylinder (use CylinderSDF instead for better accuracy)
1195
+ """
1196
+
1197
+ def __init__(self, point_a, point_b, ra, rb):
1198
+ """Initialize CappedConeSDF.
1199
+
1200
+ Parameters
1201
+ ----------
1202
+ point_a : array-like of shape (3,)
1203
+ First endpoint of the cone axis.
1204
+ point_b : array-like of shape (3,)
1205
+ Second endpoint of the cone axis.
1206
+ ra : float
1207
+ Radius at point_a.
1208
+ rb : float
1209
+ Radius at point_b.
1210
+ """
1211
+ super().__init__()
1212
+ self.point_a = torch.nn.Parameter(torch.as_tensor(point_a, dtype=torch.float32))
1213
+ self.point_b = torch.nn.Parameter(torch.as_tensor(point_b, dtype=torch.float32))
1214
+ self.ra = torch.nn.Parameter(torch.as_tensor(ra, dtype=torch.float32))
1215
+ self.rb = torch.nn.Parameter(torch.as_tensor(rb, dtype=torch.float32))
1216
+
1217
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1218
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1219
+ a = self.point_a.to(device=queries.device, dtype=queries.dtype)
1220
+ b = self.point_b.to(device=queries.device, dtype=queries.dtype)
1221
+ ra = self.ra.to(device=queries.device, dtype=queries.dtype)
1222
+ rb = self.rb.to(device=queries.device, dtype=queries.dtype)
1223
+
1224
+ rba = rb - ra
1225
+ baba = torch.sum((b - a) ** 2)
1226
+ p = queries
1227
+ papa = torch.sum((p - a) ** 2, dim=1)
1228
+ paba = torch.sum((p - a) * (b - a), dim=1) / baba
1229
+
1230
+ # Distance to cone side
1231
+ q = torch.sqrt(torch.clamp(papa - paba * paba * baba, min=0.0))
1232
+ ca = torch.maximum(torch.tensor(0.0), q - torch.where(paba < 0.5, ra, rb))
1233
+ cay = torch.abs(paba - 0.5) - 0.5
1234
+
1235
+ cax = ca
1236
+ cbx = (
1237
+ q
1238
+ - ra
1239
+ - (torch.clamp((rba * (q - ra) + paba * baba) / (rba * rba + baba), 0, 1))
1240
+ * rba
1241
+ )
1242
+ cby = paba - torch.clamp(
1243
+ (rba * (q - ra) + paba * baba) / (rba * rba + baba), 0, 1
1244
+ )
1245
+
1246
+ s = torch.where((cbx < 0) & (cay < 0), -1, 1)
1247
+
1248
+ a_dist = cax**2 + cay**2 * baba
1249
+ b_dist = cbx**2 + cby**2 * baba
1250
+
1251
+ # Determine which distance to use
1252
+ mask = (ca < 0) & (cay < 0)
1253
+ d2 = torch.where(mask, torch.tensor(0.0), torch.minimum(a_dist, b_dist))
1254
+
1255
+ d = torch.sign(d2) * torch.sqrt(torch.abs(d2)) * s
1256
+ return d.reshape(-1, 1)
1257
+
1258
+ def _get_domain_bounds(self) -> torch.Tensor:
1259
+ r = max(self.ra.detach().item(), self.rb.detach().item())
1260
+ h = torch.linalg.norm(self.point_b.detach() - self.point_a.detach())
1261
+ return torch.tensor([[-r, -r, 0], [r, r, h]])
1262
+
1263
+
1264
+ class RoundedConeSDF(SDFBase):
1265
+ """SDF for a cone with smooth rounded transitions."""
1266
+
1267
+ def __init__(self, r1, r2, h):
1268
+ super().__init__()
1269
+ self.r1 = torch.nn.Parameter(torch.as_tensor(r1, dtype=torch.float32))
1270
+ self.r2 = torch.nn.Parameter(torch.as_tensor(r2, dtype=torch.float32))
1271
+ self.h = torch.nn.Parameter(torch.as_tensor(h, dtype=torch.float32))
1272
+
1273
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1274
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1275
+ r1 = self.r1.to(device=queries.device, dtype=queries.dtype)
1276
+ r2 = self.r2.to(device=queries.device, dtype=queries.dtype)
1277
+ h = self.h.to(device=queries.device, dtype=queries.dtype)
1278
+
1279
+ q = torch.stack(
1280
+ [torch.sqrt(queries[:, 0] ** 2 + queries[:, 1] ** 2), queries[:, 2]], dim=1
1281
+ )
1282
+ b = (r1 - r2) / h
1283
+ a = torch.sqrt(1 - b * b)
1284
+ k = torch.sum(q * torch.stack([-b, a]), dim=1)
1285
+
1286
+ c1 = torch.linalg.norm(q, dim=1) - r1
1287
+ c2 = torch.linalg.norm(q - torch.stack([torch.tensor(0.0), h]), dim=1) - r2
1288
+ c3 = torch.sum(q * torch.stack([a, b]), dim=1) - r1
1289
+
1290
+ return torch.where(k < 0, c1, torch.where(k > a * h, c2, c3)).reshape(-1, 1)
1291
+
1292
+ def _get_domain_bounds(self) -> torch.Tensor:
1293
+ r = max(self.r1.detach().item(), self.r2.detach().item())
1294
+ h = self.h.detach().item()
1295
+ return torch.tensor([[-r, -r, 0], [r, r, h]])
1296
+
1297
+
1298
+ class TetrahedronSDF(SDFBase):
1299
+ """SDF for a regular tetrahedron."""
1300
+
1301
+ def __init__(self, r=1.0):
1302
+ super().__init__()
1303
+ self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
1304
+
1305
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1306
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1307
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
1308
+ result = (
1309
+ torch.maximum(
1310
+ torch.abs(queries[:, 0] + queries[:, 1]) - queries[:, 2],
1311
+ torch.abs(queries[:, 0] - queries[:, 1]) + queries[:, 2],
1312
+ )
1313
+ - r
1314
+ )
1315
+ return result.reshape(-1, 1) / torch.sqrt(torch.tensor(3.0))
1316
+
1317
+ def _get_domain_bounds(self) -> torch.Tensor:
1318
+ r = self.r.item()
1319
+ return torch.tensor([[-r, -r, -r], [r, r, r]])
1320
+
1321
+
1322
+ class OctahedronSDF(SDFBase):
1323
+ """SDF for a regular octahedron."""
1324
+
1325
+ def __init__(self, r=1.0):
1326
+ super().__init__()
1327
+ self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
1328
+
1329
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1330
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1331
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
1332
+ return (
1333
+ (torch.sum(torch.abs(queries), dim=1) - r)
1334
+ * torch.tan(torch.tensor(np.pi / 6))
1335
+ ).reshape(-1, 1)
1336
+
1337
+ def _get_domain_bounds(self) -> torch.Tensor:
1338
+ r = self.r.item()
1339
+ return torch.tensor([[-r, -r, -r], [r, r, r]])
1340
+
1341
+
1342
+ class DodecahedronSDF(SDFBase):
1343
+ """SDF for a regular dodecahedron."""
1344
+
1345
+ def __init__(self, r=1.0):
1346
+ super().__init__()
1347
+ self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
1348
+
1349
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1350
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1351
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
1352
+
1353
+ # Golden ratio
1354
+ golden = (1 + torch.sqrt(torch.tensor(5.0))) / 2
1355
+ # Normalized vertices of dodecahedron face normals
1356
+ vn = torch.tensor([golden, 1, 0], dtype=torch.float32)
1357
+ vn = vn / torch.linalg.norm(vn)
1358
+
1359
+ p = torch.abs(queries / r)
1360
+ a = torch.sum(p * vn, dim=1)
1361
+ b = torch.sum(
1362
+ p * torch.tensor([vn[1], vn[2], vn[0]], dtype=torch.float32), dim=1
1363
+ )
1364
+ c = torch.sum(
1365
+ p * torch.tensor([vn[2], vn[0], vn[1]], dtype=torch.float32), dim=1
1366
+ )
1367
+
1368
+ q = (torch.maximum(torch.maximum(a, b), c) - vn[0]) * r
1369
+ return q.reshape(-1, 1)
1370
+
1371
+ def _get_domain_bounds(self) -> torch.Tensor:
1372
+ r = self.r.item()
1373
+ return torch.tensor([[-r, -r, -r], [r, r, r]])
1374
+
1375
+
1376
+ class IcosahedronSDF(SDFBase):
1377
+ """SDF for a regular icosahedron."""
1378
+
1379
+ def __init__(self, r=1.0):
1380
+ super().__init__()
1381
+ self.r = torch.nn.Parameter(torch.as_tensor(r, dtype=torch.float32))
1382
+
1383
+ def _compute(self, queries: torch.Tensor) -> torch.Tensor:
1384
+ logger.debug(f"{type(self).__name__}._compute - {queries.shape[0]} points")
1385
+ r = self.r.to(device=queries.device, dtype=queries.dtype)
1386
+ r_scaled = r * torch.tensor(0.8506507174597755)
1387
+
1388
+ # Normalized icosahedron vertices
1389
+ vn_raw = torch.tensor(
1390
+ [(torch.sqrt(torch.tensor(5.0)) + 3) / 2, 1, 0], dtype=torch.float32
1391
+ )
1392
+ vn = vn_raw / torch.linalg.norm(vn_raw)
1393
+
1394
+ w = torch.sqrt(torch.tensor(3.0)) / 3
1395
+
1396
+ p = torch.abs(queries / r_scaled)
1397
+ a = torch.sum(p * vn, dim=1)
1398
+
1399
+ vn2 = torch.tensor([vn[1], vn[2], vn[0]], dtype=torch.float32)
1400
+ b = torch.sum(p * vn2, dim=1)
1401
+
1402
+ vn3 = torch.tensor([vn[2], vn[0], vn[1]], dtype=torch.float32)
1403
+ c = torch.sum(p * vn3, dim=1)
1404
+
1405
+ d = torch.sum(p * torch.tensor([w, w, w]), dim=1) - vn[0]
1406
+
1407
+ return (
1408
+ torch.maximum(torch.maximum(torch.maximum(a, b), c) - vn[0], d).reshape(
1409
+ -1, 1
1410
+ )
1411
+ * r_scaled
1412
+ )
1413
+
1414
+ def _get_domain_bounds(self) -> torch.Tensor:
1415
+ r = self.r.item() * 0.8506507174597755
1416
+ return torch.tensor([[-r, -r, -r], [r, r, r]])