DeepSDFStruct 1.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DeepSDFStruct/SDF.py +1734 -0
- DeepSDFStruct/__init__.py +66 -0
- DeepSDFStruct/deep_sdf/__init__.py +69 -0
- DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
- DeepSDFStruct/deep_sdf/data.py +258 -0
- DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
- DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
- DeepSDFStruct/deep_sdf/models.py +197 -0
- DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
- DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
- DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
- DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
- DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
- DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
- DeepSDFStruct/deep_sdf/plotting.py +143 -0
- DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
- DeepSDFStruct/deep_sdf/training.py +840 -0
- DeepSDFStruct/deep_sdf/workspace.py +363 -0
- DeepSDFStruct/design_of_experiments.py +235 -0
- DeepSDFStruct/flexicubes/__init__.py +15 -0
- DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
- DeepSDFStruct/flexicubes/tables.py +2338 -0
- DeepSDFStruct/flexisquares/__init__.py +15 -0
- DeepSDFStruct/flexisquares/flexisquares.py +967 -0
- DeepSDFStruct/flexisquares/tables.py +63 -0
- DeepSDFStruct/lattice_structure.py +254 -0
- DeepSDFStruct/local_shapes.py +226 -0
- DeepSDFStruct/mesh.py +1184 -0
- DeepSDFStruct/optimization.py +357 -0
- DeepSDFStruct/parametrization.py +215 -0
- DeepSDFStruct/pretrained_models.py +128 -0
- DeepSDFStruct/sampling.py +670 -0
- DeepSDFStruct/sdf_operations.py +990 -0
- DeepSDFStruct/sdf_primitives.py +1416 -0
- DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
- DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
- DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
- DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
- DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
- DeepSDFStruct/torch_spline.py +446 -0
- DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
- DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives/specs.json +46 -0
- DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
- DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
- DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
- DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
- DeepSDFStruct/utils.py +96 -0
- DeepSDFStruct/visualization/latent_viewer.py +211 -0
- DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
- benchmarks/generate_sdf_showcase.py +622 -0
- benchmarks/sdf_from_mesh_benchmark.py +52 -0
- benchmarks/sdf_from_mesh_comparison.md +33 -0
- benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
- benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
- benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
- deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
- deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
- deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
- deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
- deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
- deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
- deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
- deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
- docs/Makefile +20 -0
- docs/api_reference.rst +6 -0
- docs/conf.py +60 -0
- docs/index.rst +136 -0
- docs/make.bat +35 -0
- docs/qr_code_github.png +0 -0
- docs/qr_code_paper.png +0 -0
- docs/readme_images/example_output_01.png +0 -0
- docs/readme_images/example_output_02.png +0 -0
- docs/readme_images/example_output_03.png +0 -0
- docs/readme_images/example_output_04.png +0 -0
- tests/data/chairs/1005.obj +282 -0
- tests/data/chairs/1024.obj +222 -0
- tests/data/chairs/README.md +1 -0
- tests/data/circular_balls_test_case.stl +0 -0
- tests/data/cone.stl +1794 -0
- tests/data/example_disconnectd_mesh.inp +1784 -0
- tests/data/example_line_mesh.vtk +0 -0
- tests/data/stanford_bunny.stl +0 -0
- tests/data/sweep_test_case.stl +0 -0
- tests/test_DOE.py +49 -0
- tests/test_elongate_sdf.py +195 -0
- tests/test_flexisquares.py +187 -0
- tests/test_generate_dataset.py +76 -0
- tests/test_lattice_evaluation.py +137 -0
- tests/test_mesh_export.py +137 -0
- tests/test_mesh_functions.py +49 -0
- tests/test_networks.py +71 -0
- tests/test_pretrained_models.py +40 -0
- tests/test_reconstruction.py +101 -0
- tests/test_sdf_from_mesh.py +52 -0
- tests/test_sdf_functions.py +117 -0
- tests/test_sdf_primitives.py +668 -0
- tests/test_sdf_trimesh_comparison.py +482 -0
- tests/test_splinepy_unitcells.py +64 -0
- tests/test_structural_optimization.py +178 -0
- tests/test_torch_spline.py +216 -0
- tests/test_train_model.py +110 -0
- tests/test_training_data_ids.py +41 -0
- tests/tmp_outputs/temp_file.txt +0 -0
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)}")
|