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
DeepSDFStruct/mesh.py ADDED
@@ -0,0 +1,1184 @@
1
+ """
2
+ Mesh Generation and Processing
3
+ ===============================
4
+
5
+ This module provides comprehensive tools for generating, processing, and exporting
6
+ meshes from SDF representations. It supports both surface (triangle) meshes and
7
+ volume (tetrahedral) meshes, with advanced algorithms for high-quality mesh extraction.
8
+
9
+ Key Capabilities
10
+ ----------------
11
+
12
+ Mesh Extraction
13
+ - FlexiCubes: State-of-the-art dual contouring for smooth, feature-preserving 3D meshes
14
+ - FlexiSquares: 2D mesh extraction for cross-sections and planar geometries
15
+ - Marching Cubes: Traditional isosurface extraction (via skimage)
16
+
17
+ Mesh Processing
18
+ - Tetrahedral meshing for finite element analysis
19
+ - Mesh cleanup and repair (disconnected regions, degenerate elements)
20
+ - Mesh decimation and simplification
21
+ - Normal computation and smoothing
22
+
23
+ Export Formats
24
+ - VTK (.vtk) for visualization in ParaView
25
+ - Abaqus (.inp) for finite element analysis
26
+ - PLY (.ply) for general 3D interchange
27
+ - MFEM format for MFEM solvers
28
+
29
+ The module provides both PyTorch-based mesh representations (torchLineMesh,
30
+ torchSurfMesh, torchVolumeMesh) for differentiable operations and conversion
31
+ to standard formats (gustaf, trimesh) for I/O and visualization.
32
+ """
33
+
34
+ import logging
35
+ import torch as _torch
36
+ import torch.autograd.functional
37
+ from torch.func import functional_call, jacrev, jacfwd
38
+ import tetgenpy
39
+ import numpy as np
40
+ import matplotlib.tri as mtri
41
+ import matplotlib.pyplot as plt
42
+ import napf
43
+ import gustaf as gus
44
+ import pathlib
45
+ import os
46
+ import skimage
47
+ import triangle
48
+ import trimesh
49
+ import vtk
50
+ from typing import Optional, Tuple, Union
51
+ import scipy
52
+ import scipy.sparse.csgraph
53
+
54
+ from functools import partial
55
+
56
+ from DeepSDFStruct.flexicubes.flexicubes import FlexiCubes
57
+ from DeepSDFStruct.flexisquares.flexisquares import FlexiSquares
58
+ from DeepSDFStruct.lattice_structure import LatticeSDFStruct
59
+ from DeepSDFStruct.SDF import SDFBase
60
+ from DeepSDFStruct.torch_spline import TorchSpline, TorchScaling
61
+ import DeepSDFStruct
62
+
63
+ logger = logging.getLogger(DeepSDFStruct.__name__)
64
+
65
+
66
+ class torchLineMesh:
67
+ """PyTorch-based line mesh representation for differentiable operations.
68
+
69
+ Stores line segments with vertices and connectivity, supporting
70
+ gradient propagation for optimization tasks.
71
+
72
+ Parameters
73
+ ----------
74
+ vertices : torch.Tensor
75
+ Vertex coordinates of shape (N, 3).
76
+ lines : torch.Tensor
77
+ Line connectivity of shape (M, 2), where each row contains
78
+ indices into the vertices array.
79
+
80
+ Methods
81
+ -------
82
+ to_gus()
83
+ Convert to gustaf Edges format for visualization and I/O.
84
+ """
85
+
86
+ def __init__(self, vertices: _torch.Tensor, lines: _torch.Tensor):
87
+ self.vertices = vertices
88
+ self.lines = lines
89
+
90
+ def to_gus(self):
91
+ return gus.Edges(self.vertices.detach().cpu(), self.lines.detach().cpu())
92
+
93
+ def triangulate(self, x_nx2, s_n, bbox_vertices=None, tolerance=0.05):
94
+ holes = x_nx2[torch.where(s_n > tolerance)[0], :]
95
+ # N_elements = 1000 * np.prod(tiling)
96
+ # surf_area = 2
97
+ # max_a = round(surf_area / N_elements, 5)
98
+ # max_a = 1e-4
99
+
100
+ # Compute bounding box
101
+ xmin, ymin = x_nx2.min(dim=0).values.detach()
102
+ xmax, ymax = x_nx2.max(dim=0).values.detach()
103
+
104
+ # Add 4 corner vertices for the bounding rectangle
105
+ bbox_vertices = torch.tensor(
106
+ [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]],
107
+ dtype=self.vertices.dtype,
108
+ device=self.vertices.device,
109
+ )
110
+
111
+ # Offset for new vertex indices
112
+ offset = self.vertices.shape[0]
113
+ vertices_with_boundary = torch.cat([self.vertices, bbox_vertices], dim=0)
114
+
115
+ # Create segments for the rectangle boundary
116
+ bbox_segments = torch.tensor(
117
+ [
118
+ [offset + 0, offset + 1],
119
+ [offset + 1, offset + 2],
120
+ [offset + 2, offset + 3],
121
+ [offset + 3, offset + 0],
122
+ ],
123
+ dtype=self.lines.dtype,
124
+ device=self.lines.device,
125
+ )
126
+
127
+ # Combine with original segments
128
+ segments_with_boundary = torch.cat([self.lines, bbox_segments], dim=0)
129
+
130
+ # compute the mean side length of the first 100 line segments
131
+ first_100_lines = self.lines[: min(100, self.lines.shape[0]), :]
132
+ first_100_line_vertices = self.vertices[first_100_lines]
133
+ mean_side_length = torch.norm(
134
+ first_100_line_vertices[:, 0, :] - first_100_line_vertices[:, 1, :], dim=1
135
+ ).mean()
136
+
137
+ a = (np.sqrt(3) / 4) * mean_side_length.item() ** 2
138
+ triangle_string = f"pqa{a}"
139
+
140
+ A = dict(
141
+ vertices=vertices_with_boundary.detach().cpu().numpy(),
142
+ segments=segments_with_boundary.detach().cpu().numpy(),
143
+ holes=holes.detach().cpu().numpy(),
144
+ )
145
+
146
+ logger.info("Calling triangulate with " + triangle_string)
147
+ B = triangle.triangulate(A, triangle_string)
148
+
149
+ return torchSurfMesh(
150
+ torch.tensor(
151
+ B["vertices"], dtype=self.vertices.dtype, device=self.vertices.device
152
+ ),
153
+ torch.tensor(
154
+ B["triangles"], device=self.vertices.device, dtype=self.lines.dtype
155
+ ),
156
+ )
157
+
158
+ def plot(self, ax=None, show_vertices=True):
159
+ """
160
+ Plot a torchLineMesh using matplotlib.
161
+
162
+ Parameters
163
+ ----------
164
+ ax : matplotlib axis, optional
165
+ Existing axis to draw on. If None, creates a new figure.
166
+ show_vertices : bool
167
+ Whether to draw vertex points.
168
+ show_indices : bool
169
+ Whether to annotate vertex indices.
170
+ """
171
+ V = self.vertices.detach().cpu().numpy()
172
+ L = self.lines.detach().cpu().numpy()
173
+
174
+ if ax is None:
175
+ fig, ax = plt.subplots()
176
+
177
+ # Plot each line segment
178
+ for i, j in L:
179
+ x = [V[i, 0], V[j, 0]]
180
+ y = [V[i, 1], V[j, 1]]
181
+ ax.plot(x, y, linewidth=0.5, color="black", linestyle="-")
182
+
183
+ # Plot vertices
184
+ if show_vertices:
185
+ ax.scatter(V[:, 0], V[:, 1], s=1, color="black")
186
+
187
+ ax.set_aspect("equal")
188
+
189
+
190
+ class torchSurfMesh:
191
+ """PyTorch-based surface mesh representation for differentiable operations.
192
+
193
+ Stores triangle mesh with vertices and face connectivity, supporting
194
+ gradient propagation for shape optimization and learning tasks.
195
+
196
+ Parameters
197
+ ----------
198
+ vertices : torch.Tensor
199
+ Vertex coordinates of shape (N, 3).
200
+ faces : torch.Tensor
201
+ Triangle face connectivity of shape (M, 3), where each row
202
+ contains indices into the vertices array.
203
+
204
+ Methods
205
+ -------
206
+ to_gus()
207
+ Convert to gustaf Faces format for visualization and I/O.
208
+ """
209
+
210
+ def __init__(self, vertices: _torch.Tensor, faces: _torch.Tensor):
211
+ self.vertices = vertices
212
+ self.faces = faces
213
+
214
+ def to_gus(self):
215
+ return gus.Faces(self.vertices.detach().cpu(), self.faces.detach().cpu())
216
+
217
+ def to_trimesh(self):
218
+ gus_mesh = self.to_gus()
219
+ return trimesh.Trimesh(gus_mesh.vertices, gus_mesh.faces)
220
+
221
+ def clean(self, clean_jacobian=True):
222
+ """
223
+ Remove unused vertices and remap face indices.
224
+
225
+ Returns
226
+ -------
227
+ torchSurfMesh
228
+ A new cleaned mesh with only the vertices used by faces.
229
+ """
230
+ n_elements_orig = self.faces.shape[0]
231
+ n_vertices_orig = self.vertices.shape[0]
232
+ if clean_jacobian:
233
+ v0 = self.vertices[self.faces[:, 0]]
234
+ v1 = self.vertices[self.faces[:, 1]]
235
+ v2 = self.vertices[self.faces[:, 2]]
236
+
237
+ if self.vertices.shape[1] == 2:
238
+ # 2D Jacobian: signed area of triangle
239
+ # det( [v1-v0, v2-v0] ) = e1.x*e2.y - e1.y*e2.x
240
+ e1 = v1 - v0
241
+ e2 = v2 - v0
242
+ jac_det = e1[:, 0] * e2[:, 1] - e1[:, 1] * e2[:, 0]
243
+ elif self.vertices.shape[1] == 3:
244
+ # 3D Jakob: sign from oriented area using cross product direction
245
+ e1 = v1 - v0
246
+ e2 = v2 - v0
247
+ cross = torch.cross(e1, e2, dim=-1) # (F,3)
248
+ # Signed Jacobian: oriented area relative to a stable axis
249
+ # Use the largest-magnitude axis to avoid degeneracy
250
+ abs_cross = cross.abs()
251
+ axis = abs_cross.argmax(dim=1) # (F,)
252
+
253
+ signed_area = cross[torch.arange(cross.size(0)), axis]
254
+ jac_det = signed_area # sign preserved
255
+
256
+ # remove elements with zero surface area
257
+ valid_mask = jac_det > 0
258
+ self.faces = self.faces[valid_mask]
259
+
260
+ # Find unique vertices referenced by faces
261
+ used_idx = _torch.unique(self.faces.reshape(-1))
262
+
263
+ # Build mapping from old indices → new compacted indices
264
+ new_index = -_torch.ones(
265
+ self.vertices.shape[0], dtype=_torch.long, device=self.vertices.device
266
+ )
267
+ new_index[used_idx] = _torch.arange(
268
+ used_idx.numel(), device=self.vertices.device
269
+ )
270
+
271
+ # Compact vertex array
272
+ new_vertices = self.vertices[used_idx]
273
+
274
+ # Remap face indices
275
+ new_faces = new_index[self.faces]
276
+ n_elements_after = new_faces.shape[0]
277
+ n_vertices_after = new_vertices.shape[0]
278
+ info_string = ""
279
+ if n_elements_after < n_elements_orig:
280
+ info_string += f"removed {n_elements_orig-n_elements_after} elements"
281
+ if n_vertices_after < n_vertices_orig:
282
+ info_string += f" removed {n_vertices_orig-n_vertices_after} vertices"
283
+ if info_string != "":
284
+ logger.info(info_string)
285
+ return torchSurfMesh(new_vertices, new_faces)
286
+
287
+ def plot(self, ax=None, show_vertices=False, linewidth=0.2):
288
+ """
289
+ Plot a torchSurfMesh using matplotlib.
290
+
291
+ Parameters
292
+ ----------
293
+ ax : matplotlib axis, optional
294
+ Existing axis to draw on. If None, creates a new figure.
295
+ show_vertices : bool
296
+ Whether to draw vertex points.
297
+ show_indices : bool
298
+ Whether to annotate vertex indices.
299
+ """
300
+ if ax is None:
301
+ fig, ax = plt.subplots()
302
+ V = self.vertices.detach().cpu().numpy()
303
+ T = self.faces.detach().cpu().numpy()
304
+ x = V[:, 0]
305
+ y = V[:, 1]
306
+ triang = mtri.Triangulation(x, y, T)
307
+ ax.triplot(triang, linewidth=linewidth)
308
+ if show_vertices:
309
+ ax.scatter(x, y, s=5)
310
+ ax.set_aspect("equal")
311
+
312
+ def export(self, filename):
313
+ gus.io.meshio.export(filename, self.to_gus())
314
+
315
+
316
+ class torchVolumeMesh:
317
+ """PyTorch-based volume mesh representation for differentiable operations.
318
+
319
+ Stores tetrahedral mesh with vertices and element connectivity, supporting
320
+ gradient propagation through finite element analysis and other volume-based
321
+ operations.
322
+
323
+ Parameters
324
+ ----------
325
+ vertices : torch.Tensor
326
+ Vertex coordinates of shape (N, 3).
327
+ volumes : torch.Tensor
328
+ Tetrahedral element connectivity of shape (M, 4), where each row
329
+ contains indices into the vertices array.
330
+
331
+ Methods
332
+ -------
333
+ to_gus()
334
+ Convert to gustaf Volumes format for visualization and I/O.
335
+ remove_disconnected_regions(support_node, clear_unused)
336
+ Remove disconnected mesh components, optionally keeping only
337
+ the region connected to a specific node.
338
+ """
339
+
340
+ def __init__(self, vertices: _torch.Tensor, volumes: _torch.Tensor):
341
+ self.vertices = vertices
342
+ self.volumes = volumes
343
+
344
+ def to_gus(self):
345
+ return gus.Volumes(self.vertices.detach().cpu(), self.volumes.detach().cpu())
346
+
347
+ def to_trimesh(self):
348
+ gus_mesh = self.to_gus()
349
+ return trimesh.Trimesh(gus_mesh.vertices, gus_mesh.volumes)
350
+
351
+ def remove_disconnected_regions(
352
+ self, support_node: int | None = None, clear_unused=True
353
+ ):
354
+ """Remove disconnected parts from the mesh.
355
+
356
+ Uses graph connectivity analysis to identify and remove mesh regions
357
+ that are not connected to the main component or a specified support node.
358
+ Useful for cleaning up meshes after optimization or level set extraction.
359
+
360
+ Parameters
361
+ ----------
362
+ support_node : int, optional
363
+ If specified, keeps only the connected component containing this
364
+ vertex index. If None, keeps the largest component.
365
+ clear_unused : bool, default True
366
+ If True, removes unreferenced vertices after pruning elements.
367
+
368
+ Raises
369
+ ------
370
+ NotImplementedError
371
+ If mesh contains non-tetrahedral elements.
372
+ ValueError
373
+ If support_node is out of range.
374
+
375
+ Notes
376
+ -----
377
+ Currently only supports tetrahedral elements (4 nodes per element).
378
+ For now, unreferenced vertices are kept even when clear_unused=True.
379
+ """
380
+ if self.volumes.shape[1] != 4:
381
+ raise NotImplementedError("Cleanup only supports tetrahedral elements yet.")
382
+ if support_node is not None:
383
+ if support_node > self.vertices.shape[0]:
384
+ raise ValueError(
385
+ "Support node must be part of vertices. "
386
+ "Support Node: {support_node}, N Vertices: {self.vertices.shape[0]}"
387
+ )
388
+ edges = torch.cat(
389
+ [
390
+ self.volumes[:, [0, 1]],
391
+ self.volumes[:, [1, 2]],
392
+ self.volumes[:, [2, 3]],
393
+ self.volumes[:, [3, 0]],
394
+ ],
395
+ dim=0,
396
+ )
397
+
398
+ # Make it undirected
399
+ edges = torch.cat([edges, edges[:, [1, 0]]], dim=0)
400
+ num_nodes = self.vertices.shape[0]
401
+ row, col = edges.T.cpu().numpy()
402
+ data = np.ones(len(row), dtype=np.int8)
403
+ adj = scipy.sparse.coo_matrix((data, (row, col)), shape=(num_nodes, num_nodes))
404
+
405
+ # Compute connected components
406
+ num_comp, component_labels = scipy.sparse.csgraph.connected_components(
407
+ adj, directed=False
408
+ )
409
+ if support_node is None:
410
+ values, counts = np.unique(component_labels, return_counts=True)
411
+ sorted_idx = np.argsort(-counts) # negative for descending
412
+
413
+ most_frequent = values[sorted_idx[0]]
414
+ if values.shape[0] < 2:
415
+ logger.info("No disconnected regions found")
416
+ return
417
+ second_most_frequent = values[sorted_idx[1]]
418
+
419
+ # safety check that the second most frequent is not considerably large
420
+ if counts[most_frequent] < (counts[second_most_frequent] * 1.5):
421
+ logger.warning(
422
+ f"Main body contains {counts[most_frequent]} nodes and the "
423
+ f"second largest {counts[second_most_frequent]}. "
424
+ "Conside Adding a support node."
425
+ )
426
+ remaining_body = most_frequent
427
+ else:
428
+ remaining_body = component_labels[support_node]
429
+
430
+ valid_node_ids = torch.arange(self.vertices.shape[0])[
431
+ component_labels == remaining_body
432
+ ]
433
+ mask = torch.isin(self.volumes, valid_node_ids)
434
+ row_mask = mask.all(dim=1)
435
+ removed = (~row_mask).sum()
436
+ logger.info(f"Removed {removed} elements from disconnected regions.")
437
+ self.volumes = self.volumes[row_mask]
438
+ if clear_unused:
439
+ self.clear_unreferenced_nodes()
440
+
441
+ def clear_unreferenced_nodes(self):
442
+ used_nodes, inverse = torch.unique(self.volumes, return_inverse=True)
443
+ remapped_volumes = inverse.view(self.volumes.shape)
444
+ self.vertices = self.vertices[used_nodes]
445
+ self.volumes = remapped_volumes
446
+
447
+ def export(self, filename):
448
+ gus.io.meshio.export(filename, self.to_gus())
449
+
450
+
451
+ def tetrahedralize_surface(surface_mesh: gus.Faces) -> tuple[gus.Volumes, np.ndarray]:
452
+ logger.debug("Tetrahedralizing surface mesh")
453
+ t_in = tetgenpy.TetgenIO()
454
+ t_in.setup_plc(surface_mesh.vertices, surface_mesh.faces.tolist())
455
+ # gus.show(dmesh)
456
+ switch_command = "pYq"
457
+ if logging.DEBUG <= logging.root.level:
458
+ switch_command += "Q"
459
+ t_out = tetgenpy.tetrahedralize(switch_command, t_in) # pqa
460
+
461
+ tets = np.vstack(t_out.tetrahedra())
462
+ verts = t_out.points()
463
+
464
+ kdt = napf.KDT(tree_data=verts, metric=1)
465
+
466
+ distances, face_indices = kdt.knn_search(
467
+ queries=surface_mesh.vertices, kneighbors=1, nthread=4
468
+ )
469
+ tol = 1e-6
470
+ if distances.max() > tol:
471
+ Warning("Not all surface nodes as included in the volumetric mesh.")
472
+ volumes = gus.Volumes(verts, tets)
473
+ surface_mesh_indices = face_indices
474
+ return volumes, surface_mesh_indices
475
+
476
+
477
+ def export_volume_mesh(volume_mesh: gus.Volumes, filename: str, export_abaqus=False):
478
+ """
479
+ export a mesh and adds corresponding boundary conditions
480
+ """
481
+ filepath = pathlib.Path(filename)
482
+ if not os.path.isdir(filepath.parent):
483
+ os.makedirs(filepath.parent)
484
+ logger.debug(
485
+ f"Exporting mesh with {len(volume_mesh.volumes)} elements, {len(volume_mesh.vertices)} vertices to {filepath}"
486
+ )
487
+ gus.io.mfem.export(str(filepath), volume_mesh)
488
+
489
+
490
+ def export_abaqus_surf_mesh(surf_mesh: gus.Faces, filename: str):
491
+ """
492
+ export a mesh and adds corresponding boundary conditions
493
+ """
494
+ filepath = pathlib.Path(filename)
495
+ if not os.path.isdir(filepath.parent):
496
+ os.makedirs(filepath.parent)
497
+ gus.io.meshio.export(str(filepath.with_suffix(".inp")), surf_mesh)
498
+
499
+
500
+ def generate_2D_surf_mesh(
501
+ sdf: SDFBase, n_squares: int, n_elements: int = 50000, bounds=None
502
+ ):
503
+ n_points = 1000
504
+ if bounds is None:
505
+ bounds = sdf._get_domain_bounds()
506
+ x = np.linspace(bounds[0, 0], bounds[1, 0], n_points)
507
+ y = np.linspace(bounds[0, 1], bounds[1, 1], n_points)
508
+ xx, yy = np.meshgrid(x, y)
509
+ xx, yy = np.meshgrid(x, y)
510
+ queries = np.hstack([xx.reshape(-1, 1), yy.reshape(-1, 1)])
511
+ evaluated_sdf = sdf(queries)
512
+ evaluated_sdf_orig_shape = evaluated_sdf.reshape(xx.shape)
513
+ A = sdf_to_triangle_dict(x, y, evaluated_sdf_orig_shape, level=0)
514
+ domain_area = (bounds[1, 0] - bounds[0, 0]) * (bounds[1, 1] - bounds[0, 1])
515
+ min_a = domain_area / n_elements
516
+ triangle_string = f"pqa{min_a}"
517
+ logger.info("Calling triangulate with " + triangle_string)
518
+ B = triangle.triangulate(A, triangle_string)
519
+ mesh = gus.Faces(vertices=B["vertices"], faces=B["triangles"])
520
+ return mesh
521
+
522
+
523
+ def sdf_to_triangle_dict(x, y, sdf, level=0.0, pad_value=1.0, collinear_tol=1e-8):
524
+ """
525
+ Convert an SDF array to a PSLG dict for triangle.triangulate.
526
+ Includes domain boundary and hole polygons.
527
+
528
+ Args:
529
+ sdf (np.ndarray): 2D signed distance field.
530
+ level (float): Contour level (usually 0 for boundary).
531
+ pad_value (float): Value outside domain to close boundaries.
532
+
533
+ Returns:
534
+ dict: {"vertices": ..., "segments": ..., "holes": ...}
535
+ """
536
+ H, W = sdf.shape
537
+
538
+ # Pad so contours touching domain edge get closed
539
+ padded = np.pad(sdf, 1, mode="constant", constant_values=pad_value)
540
+ contours = skimage.measure.find_contours(padded, level)
541
+ contours = [c - 1 for c in contours] # shift back after padding
542
+
543
+ vertices = []
544
+ segments = []
545
+ holes = []
546
+
547
+ v_offset = 0
548
+
549
+ for contour in contours:
550
+ # contour[:, 0] = row indices in [0, H), map to y
551
+ # contour[:, 1] = col indices in [0, W), map to x
552
+ poly_x = np.interp(contour[:, 1], np.arange(W), x)
553
+ poly_y = np.interp(contour[:, 0], np.arange(H), y)
554
+ poly = np.column_stack([poly_x, poly_y])
555
+ poly = prune_collinear(poly, tol=collinear_tol)
556
+
557
+ n = len(poly)
558
+ vertices.extend(poly.tolist())
559
+ segments.extend([[v_offset + i, v_offset + (i + 1) % n] for i in range(n)])
560
+
561
+ # Orientation heuristic
562
+ area = 0.5 * np.sum(
563
+ poly[:, 0] * np.roll(poly[:, 1], -1) - poly[:, 1] * np.roll(poly[:, 0], -1)
564
+ )
565
+ if area > 0:
566
+ centroid = poly.mean(axis=0)
567
+ holes.append(centroid.tolist())
568
+
569
+ v_offset += n
570
+
571
+ _, unique_indices, counts = np.unique(
572
+ vertices, axis=0, return_index=True, return_counts=True
573
+ )
574
+ duplicate_indices = np.where(counts > 1)[0]
575
+
576
+ if len(duplicate_indices) > 0:
577
+ logger.info(f"Found {len(duplicate_indices)} duplicate vertices:")
578
+ for idx in duplicate_indices:
579
+ logger.debug(vertices[idx])
580
+ else:
581
+ print("No duplicate vertices found.")
582
+
583
+ vertices, idx = np.unique(vertices, axis=0, return_inverse=True)
584
+ segments = np.array([[idx[s[0]], idx[s[1]]] for s in segments])
585
+
586
+ return dict(
587
+ vertices=np.array(vertices), segments=np.array(segments), holes=np.array(holes)
588
+ )
589
+
590
+
591
+ def prune_collinear(points, tol=1e-9):
592
+ """
593
+ Remove nearly collinear points from a polyline (closed loop).
594
+ Args:
595
+ points (ndarray): Nx2 array of (x,y) vertices.
596
+ tol (float): area tolerance. Smaller = stricter (keep more points).
597
+ Returns:
598
+ ndarray: pruned Nx2 array of vertices.
599
+ """
600
+ if len(points) <= 3:
601
+ return points # nothing to prune
602
+
603
+ pruned = [points[0]]
604
+ for i in range(1, len(points) - 1):
605
+ a, b, c = points[i - 1], points[i], points[i + 1]
606
+ # Compute twice the triangle area formed by (a,b,c)
607
+ area = abs((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]))
608
+ if area > tol:
609
+ pruned.append(b)
610
+ pruned.append(points[-1])
611
+ return np.array(pruned)
612
+
613
+
614
+ def process_N_base_input(N, tiling, bounds=None, dim=3):
615
+ if isinstance(N, list):
616
+ if len(N) != dim:
617
+ raise ValueError("Number of grid points must be a list of 3 integers")
618
+ N = _torch.tensor(N)
619
+ N_mod = N * tiling + 1
620
+ elif isinstance(N, int):
621
+ N = _torch.tensor([N] * dim)
622
+ if bounds is not None:
623
+ bounds_device = bounds.device if hasattr(bounds, "device") else "cpu"
624
+ if bounds_device != "cpu":
625
+ N = N.to(bounds_device)
626
+ tiling = tiling.to(bounds_device)
627
+ extents = bounds[1] - bounds[0]
628
+ max_extent = extents.max()
629
+ if max_extent == 0:
630
+ raise ValueError("Bounds must have non-zero extent")
631
+ ratios = extents / max_extent
632
+ N_adaptive = _torch.ceil(N.float() * ratios * tiling.float()).int() + 1
633
+ N_mod = _torch.clamp(N_adaptive, min=4)
634
+ else:
635
+ N_mod = N * tiling + 1
636
+ else:
637
+ raise ValueError("Number of grid points must be a list or an integer")
638
+ return N_mod
639
+
640
+
641
+ def _prepare_flexicubes_querypoints(
642
+ N, device=None, bounds=None, extr_type="flexicubes"
643
+ ):
644
+ """
645
+ takes the tiling and a resolution as input
646
+ output: DeepSDFStruct.flexicubes constructor, samples and cube indices
647
+ the points are located in the region [0,1] with a margin of 0.025
648
+ -> [-0.025, 1.025]
649
+ """
650
+ # check_tiling_input(tiling)
651
+ if extr_type not in ["flexicubes", "flexisquares"]:
652
+ raise TypeError(
653
+ f"Argument extr_type must be either flexicubes or flexisquares, not {extr_type}"
654
+ )
655
+ if device is None:
656
+ device = "cuda" if _torch.cuda.is_available() else "cpu"
657
+ if extr_type == "flexicubes":
658
+ flexi_cubes_constructor = FlexiCubes(device=device)
659
+ else:
660
+ flexi_cubes_constructor = FlexiSquares(device=device)
661
+
662
+ samples, cube_idx = flexi_cubes_constructor.construct_voxel_grid(
663
+ resolution=tuple(N), bounds=bounds
664
+ )
665
+
666
+ return flexi_cubes_constructor, samples, cube_idx
667
+
668
+
669
+ def create_3D_mesh(
670
+ sdf: SDFBase,
671
+ N_base,
672
+ mesh_type: str,
673
+ differentiate=False,
674
+ device="cpu",
675
+ bounds=None,
676
+ diffmode="fwd",
677
+ deformation_function: None | TorchSpline | TorchScaling = None,
678
+ ) -> Tuple[Union[torchSurfMesh, torchVolumeMesh], Optional[_torch.Tensor]]:
679
+ """Generate a 3D mesh from an SDF using FlexiCubes dual contouring.
680
+
681
+ This is the main entry point for extracting high-quality meshes from SDF
682
+ representations. It uses the FlexiCubes algorithm, which produces smooth,
683
+ feature-preserving meshes that are superior to marching cubes.
684
+
685
+ The function supports both surface meshes (triangles) and volume meshes
686
+ (tetrahedra) and can compute gradients for optimization if requested.
687
+
688
+ Parameters
689
+ ----------
690
+ sdf : SDFBase
691
+ The signed distance function to mesh. Can be any SDFBase subclass
692
+ including primitives, lattice structures, or learned representations.
693
+ N_base : int or list of int
694
+ Base resolution. Behavior depends on input type:
695
+
696
+ - If **int**: Base resolution for the **largest dimension**. Other dimensions
697
+ are adaptively scaled based on bounding box proportions to maintain
698
+ consistent spatial resolution. Minimum 4 points per dimension.
699
+ Example: For bounds 10x1x1 with N_base=64, resolution ~{64, 7, 7}.
700
+
701
+ - If **list [nx, ny, nz]**: Explicit resolution per dimension. No adaptive
702
+ scaling is applied.
703
+
704
+ For lattice structures, the final resolution is N × tiling + 1.
705
+ mesh_type : {'surface', 'volume'}
706
+ Type of mesh to generate:
707
+ - 'surface': Triangle surface mesh (torchSurfMesh)
708
+ - 'volume': Tetrahedral volume mesh (torchVolumeMesh)
709
+ differentiate : bool, default False
710
+ If True, computes and returns gradients of vertex positions with
711
+ respect to SDF parameters, enabling gradient-based optimization.
712
+ device : str, default 'cpu'
713
+ Device for computation ('cpu' or 'cuda').
714
+ bounds : array-like of shape (2, 3), optional
715
+ Spatial bounds [[xmin, ymin, zmin], [xmax, ymax, zmax]].
716
+ If None, uses the SDF's domain bounds.
717
+
718
+ Returns
719
+ -------
720
+ mesh : torchSurfMesh or torchVolumeMesh
721
+ The extracted mesh with vertices and connectivity.
722
+ dVerts_dParams : torch.Tensor or None
723
+ If differentiate=True, returns gradients of vertex positions with
724
+ respect to parameters. Otherwise returns None.
725
+
726
+ Examples
727
+ --------
728
+ >>> from DeepSDFStruct.sdf_primitives import SphereSDF
729
+ >>> from DeepSDFStruct.mesh import create_3D_mesh
730
+ >>>
731
+ >>> # Create a sphere SDF
732
+ >>> sphere = SphereSDF(center=[0, 0, 0], radius=1.0)
733
+ >>>
734
+ >>> # Extract surface mesh
735
+ >>> mesh, _ = create_3D_mesh(sphere, N_base=64, mesh_type='surface')
736
+ >>> print(f"Vertices: {mesh.vertices.shape}")
737
+ >>> print(f"Faces: {mesh.faces.shape}")
738
+ >>>
739
+ >>> # Extract volume mesh for FEA
740
+ >>> vol_mesh, _ = create_3D_mesh(sphere, N_base=32, mesh_type='volume')
741
+ >>> print(f"Tetrahedra: {vol_mesh.volumes.shape}")
742
+
743
+ Notes
744
+ -----
745
+ FlexiCubes produces higher quality meshes than marching cubes by:
746
+ - Using flexible vertex positions within each cube
747
+ - Preserving sharp features and corners
748
+ - Minimizing mesh artifacts and degeneracies
749
+
750
+ For lattice structures, the resolution is automatically scaled by the
751
+ tiling factor to maintain consistent resolution per unit cell.
752
+ """
753
+ lattice = find_lattice_sdf(sdf)
754
+ if lattice is None:
755
+ tiling = _torch.tensor([1, 1, 1])
756
+ else:
757
+ tiling = _torch.tensor(lattice.tiling)
758
+
759
+ if mesh_type == "surface":
760
+ output_tetmesh = False
761
+ elif mesh_type == "volume":
762
+ output_tetmesh = True
763
+ else:
764
+ raise RuntimeError(
765
+ f"mesh_type {mesh_type} unavailable. Must be either surface or volume"
766
+ )
767
+
768
+ if bounds is None:
769
+ bounds = sdf._get_domain_bounds()
770
+ extended_bounds = bounds.clone()
771
+ off = (extended_bounds[1] - extended_bounds[0]) * 0.05
772
+ extended_bounds[0] -= off
773
+ extended_bounds[1] += off
774
+
775
+ N = process_N_base_input(N_base, tiling, bounds=extended_bounds)
776
+
777
+ constructor, samples, cube_idx = _prepare_flexicubes_querypoints(
778
+ N, device=device, bounds=extended_bounds
779
+ )
780
+ dVerts_dParams = None
781
+
782
+ buffers = dict(sdf.named_buffers())
783
+ verts_fn = partial(
784
+ _verts_from_params,
785
+ buffers=buffers,
786
+ sdf=sdf,
787
+ samples=samples,
788
+ constructor=constructor,
789
+ cube_idx=cube_idx,
790
+ N=N,
791
+ return_faces=False,
792
+ output_tetmesh=output_tetmesh,
793
+ deformation_function=deformation_function,
794
+ )
795
+ # returns faces or volumes depending on the output_tetmesh flag
796
+ # if output_tetmesh -> returns volumes
797
+ # if not output_tetmesh -> returns faces
798
+ verts, faces_or_volumes = get_verts(
799
+ sdf=sdf,
800
+ samples=samples,
801
+ constructor=constructor,
802
+ cube_idx=cube_idx,
803
+ N=N,
804
+ return_faces=True,
805
+ output_tetmesh=output_tetmesh,
806
+ deformation_function=deformation_function,
807
+ )
808
+
809
+ if differentiate:
810
+ if diffmode == "rev":
811
+ dVerts_dParams = jacrev(verts_fn)(dict(sdf.named_parameters()))
812
+ elif diffmode == "fwd":
813
+ dVerts_dParams = jacfwd(verts_fn)(dict(sdf.named_parameters()))
814
+ else:
815
+ raise NotImplementedError("diffmode must be either fwd or rev")
816
+ if output_tetmesh:
817
+ return torchVolumeMesh(verts, faces_or_volumes), dVerts_dParams
818
+ else:
819
+ return torchSurfMesh(verts, faces_or_volumes), dVerts_dParams
820
+
821
+
822
+ def find_lattice_sdf(module) -> LatticeSDFStruct | None:
823
+ for m in module.modules():
824
+ if isinstance(m, LatticeSDFStruct):
825
+ return m
826
+ return None
827
+
828
+
829
+ def create_2D_mesh(
830
+ sdf: SDFBase,
831
+ N_base,
832
+ mesh_type: str,
833
+ differentiate=False,
834
+ device=None,
835
+ bounds=None,
836
+ diffmode="fwd",
837
+ n_smoothing_iterations=5,
838
+ deformation_function: TorchSpline | TorchScaling | None = None,
839
+ ) -> Tuple[Union[torchLineMesh, torchSurfMesh], Optional[torch.Tensor]]:
840
+
841
+ if device is not None:
842
+ print("Warning: the argument device is no longer used.")
843
+
844
+ lattice = find_lattice_sdf(sdf)
845
+ if lattice is None:
846
+ tiling = _torch.tensor([1, 1])
847
+ else:
848
+ tiling = _torch.tensor(lattice.tiling)
849
+
850
+ if mesh_type in ["line", "surface_triangle"]:
851
+ output_tetmesh = False
852
+ elif mesh_type == "surface":
853
+ output_tetmesh = True
854
+ else:
855
+ raise RuntimeError(
856
+ f"mesh_type {mesh_type} unavailable. Must be either line or surface"
857
+ )
858
+
859
+ N = process_N_base_input(N_base, tiling, dim=2)
860
+
861
+ constructor, samples, cube_idx = _prepare_flexicubes_querypoints(
862
+ N, device=sdf.get_device(), bounds=bounds, extr_type="flexisquares"
863
+ )
864
+ dVerts_dParams = None
865
+
866
+ buffers = dict(sdf.named_buffers())
867
+ verts_fn = partial(
868
+ _verts_from_params,
869
+ buffers=buffers,
870
+ sdf=sdf,
871
+ samples=samples,
872
+ constructor=constructor,
873
+ cube_idx=cube_idx,
874
+ N=N,
875
+ return_faces=False,
876
+ output_tetmesh=output_tetmesh,
877
+ n_smoothing_iterations=n_smoothing_iterations,
878
+ deformation_function=deformation_function,
879
+ )
880
+ # returns faces or volumes depending on the output_tetmesh flag
881
+ # if output_tetmesh -> returns volumes
882
+ # if not output_tetmesh -> returns faces
883
+ sdf_values = sdf(samples)
884
+
885
+ verts_local, faces_or_volumes, _ = constructor(
886
+ voxelgrid_vertices=samples,
887
+ scalar_field=sdf_values.reshape(-1),
888
+ cube_idx=cube_idx,
889
+ resolution=tuple(N),
890
+ output_tetmesh=output_tetmesh,
891
+ n_smoothing_iterations=n_smoothing_iterations,
892
+ )
893
+
894
+ if deformation_function is not None:
895
+ verts_local = deformation_function.forward(verts_local)
896
+ with torch.no_grad():
897
+ samples_deformed = deformation_function.forward(samples)
898
+
899
+ if differentiate:
900
+ if diffmode == "rev":
901
+ dVerts_dParams = jacrev(verts_fn)(dict(sdf.named_parameters()))
902
+ elif diffmode == "fwd":
903
+ dVerts_dParams = jacfwd(verts_fn)(dict(sdf.named_parameters()))
904
+ else:
905
+ raise NotImplementedError("diffmode must be either fwd or rev")
906
+
907
+ if mesh_type == "line":
908
+ return torchLineMesh(verts_local, faces_or_volumes), dVerts_dParams
909
+ elif mesh_type == "surface":
910
+ return torchSurfMesh(verts_local, faces_or_volumes), dVerts_dParams
911
+ elif mesh_type == "surface_triangle":
912
+ line_mesh = torchLineMesh(verts_local, faces_or_volumes)
913
+ gus.io.meshio.export("lines.inp", line_mesh.to_gus())
914
+ surf_mesh = line_mesh.triangulate(samples_deformed, sdf_values.reshape(-1))
915
+ gus.io.meshio.export(
916
+ "surfs_straight_after_triangulation.inp", surf_mesh.to_gus()
917
+ )
918
+ surf_mesh.vertices.requires_grad = True
919
+ dist = torch.cdist(
920
+ surf_mesh.vertices.double(),
921
+ line_mesh.vertices.double(),
922
+ compute_mode="use_mm_for_euclid_dist",
923
+ ) # (N, M)
924
+
925
+ # find nearest original vertex index
926
+ min_dist, min_idx = dist.min(dim=1)
927
+ # mask where surf vertex matches original vertex
928
+ mask = min_dist < 1e-5 # or some tolerance
929
+ if mask.sum() != line_mesh.vertices.shape[0]:
930
+ raise ValueError("Not all boundary vertices were found in the surface mesh")
931
+ orig_idx = min_idx
932
+ surf_mesh.vertices = torch.where(
933
+ mask[:, None], line_mesh.vertices[orig_idx], surf_mesh.vertices
934
+ )
935
+ gus.io.meshio.export("surfs_straight_after_replacement.inp", surf_mesh.to_gus())
936
+ return surf_mesh, dVerts_dParams
937
+ else:
938
+ raise RuntimeError(
939
+ f"mesh_type {mesh_type} unavailable. Must be either line or surface"
940
+ )
941
+
942
+
943
+ def _verts_from_params(
944
+ parameters: _torch.Tensor,
945
+ buffers: _torch.Tensor,
946
+ sdf: SDFBase,
947
+ samples: _torch.Tensor,
948
+ constructor: FlexiCubes | FlexiSquares,
949
+ cube_idx: _torch.Tensor,
950
+ N,
951
+ return_faces=False,
952
+ output_tetmesh=False,
953
+ n_smoothing_iterations=5,
954
+ deformation_function: TorchSpline | TorchScaling | None = None,
955
+ ):
956
+
957
+ sdf_values = functional_call(sdf, (parameters, buffers), (samples,))
958
+
959
+ verts_local, faces, _ = constructor(
960
+ voxelgrid_vertices=samples,
961
+ scalar_field=sdf_values.reshape(-1),
962
+ cube_idx=cube_idx,
963
+ resolution=tuple(N),
964
+ output_tetmesh=output_tetmesh,
965
+ n_smoothing_iterations=n_smoothing_iterations,
966
+ )
967
+
968
+ if deformation_function is not None:
969
+ verts_local = deformation_function.forward(verts_local)
970
+ if return_faces:
971
+ return verts_local, faces
972
+ else:
973
+ return verts_local
974
+
975
+
976
+ def get_verts(
977
+ sdf: SDFBase,
978
+ samples: _torch.Tensor,
979
+ constructor: FlexiCubes,
980
+ cube_idx: _torch.Tensor,
981
+ N,
982
+ return_faces=False,
983
+ output_tetmesh=False,
984
+ deformation_function: TorchSpline | TorchScaling | None = None,
985
+ ):
986
+ sdf_values = sdf(samples)
987
+
988
+ verts_local, faces, _ = constructor(
989
+ voxelgrid_vertices=samples,
990
+ scalar_field=sdf_values.reshape(-1),
991
+ cube_idx=cube_idx,
992
+ resolution=tuple(N),
993
+ output_tetmesh=output_tetmesh,
994
+ )
995
+
996
+ if deformation_function is not None:
997
+ verts_local = deformation_function.forward(verts_local)
998
+ if return_faces:
999
+ return verts_local, faces
1000
+ else:
1001
+ return verts_local
1002
+
1003
+
1004
+ def export_sdf_grid_vtk(sdf: SDFBase, filename, N=64, bounds=None):
1005
+ if bounds is None:
1006
+ bounds = sdf._get_domain_bounds()
1007
+ if bounds is None:
1008
+ if sdf.geometric_dim == 2:
1009
+ bounds = np.array([[0, 0], [1, 1]])
1010
+ elif sdf.geometric_dim == 3:
1011
+ bounds = np.array([[0, 0, 0], [1, 1, 1]])
1012
+ else:
1013
+ raise ValueError("geometric dimension of sdf must be either 2 or 3")
1014
+ if isinstance(bounds, torch.Tensor):
1015
+ bounds = bounds.detach().cpu().numpy()
1016
+
1017
+ if isinstance(bounds, list):
1018
+ bounds = np.array(bounds)
1019
+ # Generate grid points
1020
+ x = np.linspace(bounds[0, 0], bounds[1, 0], N)
1021
+ y = np.linspace(bounds[0, 1], bounds[1, 1], N)
1022
+ if sdf.geometric_dim == 3:
1023
+ z = np.linspace(bounds[0, 2], bounds[1, 2], N)
1024
+ xx, yy, zz = np.meshgrid(x, y, z, indexing="ij")
1025
+ points = np.vstack([xx.ravel(), yy.ravel(), zz.ravel()]).T
1026
+ else:
1027
+ xx, yy = np.meshgrid(x, y)
1028
+ points = np.vstack([xx.ravel(), yy.ravel()]).T
1029
+
1030
+ # Evaluate SDF
1031
+ with _torch.no_grad():
1032
+ sdf_vals = sdf(_torch.tensor(points, device=sdf.get_device()))
1033
+ sdf_vals = sdf_vals.detach().cpu().numpy().reshape(-1)
1034
+
1035
+ # Create vtkPoints
1036
+ vtk_points = vtk.vtkPoints()
1037
+ for pt in points:
1038
+ vtk_point = pt.tolist()
1039
+ if sdf.geometric_dim == 2:
1040
+ vtk_point.append(0)
1041
+ vtk_points.InsertNextPoint(vtk_point)
1042
+
1043
+ # Create structured grid
1044
+ grid = vtk.vtkStructuredGrid()
1045
+ if sdf.geometric_dim == 3:
1046
+ grid.SetDimensions(N, N, N)
1047
+ else:
1048
+ grid.SetDimensions(N, N, 1)
1049
+ grid.SetPoints(vtk_points)
1050
+
1051
+ # Add SDF scalar field
1052
+ vtk_array = vtk.vtkDoubleArray()
1053
+ vtk_array.SetName("SDF")
1054
+ vtk_array.SetNumberOfValues(len(sdf_vals))
1055
+ for i, val in enumerate(sdf_vals):
1056
+ vtk_array.SetValue(i, val)
1057
+ grid.GetPointData().SetScalars(vtk_array)
1058
+
1059
+ # Write to file
1060
+ writer = vtk.vtkStructuredGridWriter()
1061
+ writer.SetFileName(filename)
1062
+ writer.SetInputData(grid)
1063
+ writer.Write()
1064
+ logger.info(f"SDF structured grid saved to {filename}")
1065
+
1066
+
1067
+ def _export_surface_mesh_vtk(verts, faces, filename, dSurf: dict = None):
1068
+ """
1069
+ verts: (N, 3) torch tensor
1070
+ faces: (M, 3) torch tensor
1071
+ dSurf: (N, 3, ..,..) torch tensor
1072
+ """
1073
+ vtk_points = vtk.vtkPoints()
1074
+ for v in verts:
1075
+ vert = v.tolist()
1076
+ if len(vert) == 2:
1077
+ vert.append(0.0)
1078
+ vtk_points.InsertNextPoint(vert)
1079
+
1080
+ vtk_cells = vtk.vtkCellArray()
1081
+ for f in faces:
1082
+ triangle = vtk.vtkTriangle()
1083
+ triangle.GetPointIds().SetId(0, f[0])
1084
+ triangle.GetPointIds().SetId(1, f[1])
1085
+ triangle.GetPointIds().SetId(2, f[2])
1086
+ vtk_cells.InsertNextCell(triangle)
1087
+
1088
+ polydata = vtk.vtkPolyData()
1089
+ polydata.SetPoints(vtk_points)
1090
+ polydata.SetPolys(vtk_cells)
1091
+ if dSurf is not None:
1092
+ for param_name, dSurf_dparam in dSurf.items():
1093
+ # Add derivative vectors as a vector field
1094
+ extra_dims = dSurf_dparam.shape[2:]
1095
+ N = int(np.prod(extra_dims))
1096
+
1097
+ # reshape to [n_nodes, N_derivatives, 3]
1098
+ dSurf_flat_with_nan = dSurf_dparam.flatten(2)
1099
+ # remove nans
1100
+ dSurf_flat = torch.nan_to_num(dSurf_flat_with_nan)
1101
+ indices = [np.unravel_index(i, extra_dims) for i in range(N)]
1102
+ assert dSurf_flat.shape[2] == len(indices)
1103
+ for j, idx in enumerate(indices):
1104
+ idx_str = "".join(str(i + 1) for i in idx)
1105
+ name = f"derivative_{param_name}_[{idx_str}]"
1106
+
1107
+ vectors = vtk.vtkDoubleArray()
1108
+ vectors.SetNumberOfComponents(3)
1109
+ vectors.SetName(name)
1110
+
1111
+ for vec in dSurf_flat[:, :, j]:
1112
+ vecND = vec.tolist()
1113
+ if len(vecND) == 2:
1114
+ vecND.append(0.0)
1115
+ if len(vecND) != 3:
1116
+ raise ValueError("Vector to be inserted must have 3 entries.")
1117
+
1118
+ vectors.InsertNextTuple(vecND)
1119
+
1120
+ polydata.GetPointData().AddArray(vectors)
1121
+ polydata.GetPointData().SetActiveVectors(name)
1122
+ # Write to file
1123
+ writer = vtk.vtkPolyDataWriter()
1124
+ writer.SetFileName(filename)
1125
+ writer.SetInputData(polydata)
1126
+ writer.Write()
1127
+ print(f"Mesh saved to {filename}")
1128
+
1129
+
1130
+ def export_surface_mesh(
1131
+ filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
1132
+ mesh: gus.Faces | torchSurfMesh,
1133
+ dSurf=None,
1134
+ ):
1135
+ export_filename = pathlib.Path(filename)
1136
+ ext = export_filename.suffix.lower()
1137
+ if isinstance(mesh, torchSurfMesh):
1138
+ mesh = mesh.to_gus()
1139
+ match ext:
1140
+ case ".vtk":
1141
+ _export_surface_mesh_vtk(mesh.vertices, mesh.faces, export_filename, dSurf)
1142
+ case _:
1143
+ gus.io.meshio.export(export_filename, mesh)
1144
+
1145
+
1146
+ def mergeMeshs(mesh1, mesh2, tol=1e-10):
1147
+ vertices1 = mesh1.vertices
1148
+ vertices2 = mesh2.vertices
1149
+
1150
+ diff = vertices2.unsqueeze(1) - vertices1.unsqueeze(0)
1151
+ dist2 = (diff**2).sum(dim=-1)
1152
+ min_dist, nearest_idx = dist2.min(dim=1)
1153
+
1154
+ duplicates = min_dist < tol
1155
+
1156
+ mapping = torch.empty(vertices2.shape[0], dtype=torch.long, device=vertices1.device)
1157
+ mapping[duplicates] = nearest_idx[duplicates]
1158
+
1159
+ unique_vertices_mask = ~duplicates
1160
+ mapping[unique_vertices_mask] = torch.arange(
1161
+ vertices1.shape[0],
1162
+ vertices1.shape[0] + unique_vertices_mask.sum(),
1163
+ device=vertices1.device,
1164
+ )
1165
+
1166
+ merged_vertices = torch.cat([vertices1, vertices2[unique_vertices_mask]], dim=0)
1167
+
1168
+ if isinstance(mesh1, torchLineMesh):
1169
+ merged_lines = mapping[mesh2.lines]
1170
+ merged_lines = torch.cat([mesh1.lines, merged_lines], dim=0)
1171
+ return torchLineMesh(merged_vertices, merged_lines)
1172
+
1173
+ elif isinstance(mesh1, torchSurfMesh):
1174
+ merged_faces = mapping[mesh2.faces]
1175
+ merged_faces = torch.cat([mesh1.faces, merged_faces], dim=0)
1176
+ return torchSurfMesh(merged_vertices, merged_faces)
1177
+
1178
+ elif isinstance(mesh1, torchVolumeMesh):
1179
+ merged_volumes = mapping[mesh2.volumes]
1180
+ merged_volumes = torch.cat([mesh1.volumes, merged_volumes], dim=0)
1181
+ return torchVolumeMesh(merged_vertices, merged_volumes)
1182
+
1183
+ else:
1184
+ raise TypeError(f"Unsupported mesh type: {type(mesh1)}")