DeepSDFStruct 1.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. DeepSDFStruct/SDF.py +1734 -0
  2. DeepSDFStruct/__init__.py +66 -0
  3. DeepSDFStruct/deep_sdf/__init__.py +69 -0
  4. DeepSDFStruct/deep_sdf/create_screenshots_from_plyfiles.py +65 -0
  5. DeepSDFStruct/deep_sdf/data.py +258 -0
  6. DeepSDFStruct/deep_sdf/metrics/__init__.py +14 -0
  7. DeepSDFStruct/deep_sdf/metrics/mesh_to_analytical.py +24 -0
  8. DeepSDFStruct/deep_sdf/models.py +197 -0
  9. DeepSDFStruct/deep_sdf/networks/__init__.py +8 -0
  10. DeepSDFStruct/deep_sdf/networks/analytic_round_cross.py +105 -0
  11. DeepSDFStruct/deep_sdf/networks/deep_sdf_decoder.py +155 -0
  12. DeepSDFStruct/deep_sdf/networks/hierarchical_deep_sdf_decoder.py +139 -0
  13. DeepSDFStruct/deep_sdf/networks/hierarchical_positional_sdf_decoder.py +190 -0
  14. DeepSDFStruct/deep_sdf/networks/resnet_positional_sdf_decoder.py +108 -0
  15. DeepSDFStruct/deep_sdf/nn_utils.py +70 -0
  16. DeepSDFStruct/deep_sdf/plotting.py +143 -0
  17. DeepSDFStruct/deep_sdf/reconstruction.py +165 -0
  18. DeepSDFStruct/deep_sdf/training.py +840 -0
  19. DeepSDFStruct/deep_sdf/workspace.py +363 -0
  20. DeepSDFStruct/design_of_experiments.py +235 -0
  21. DeepSDFStruct/flexicubes/__init__.py +15 -0
  22. DeepSDFStruct/flexicubes/flexicubes.py +1005 -0
  23. DeepSDFStruct/flexicubes/tables.py +2338 -0
  24. DeepSDFStruct/flexisquares/__init__.py +15 -0
  25. DeepSDFStruct/flexisquares/flexisquares.py +967 -0
  26. DeepSDFStruct/flexisquares/tables.py +63 -0
  27. DeepSDFStruct/lattice_structure.py +254 -0
  28. DeepSDFStruct/local_shapes.py +226 -0
  29. DeepSDFStruct/mesh.py +1184 -0
  30. DeepSDFStruct/optimization.py +357 -0
  31. DeepSDFStruct/parametrization.py +215 -0
  32. DeepSDFStruct/pretrained_models.py +128 -0
  33. DeepSDFStruct/sampling.py +670 -0
  34. DeepSDFStruct/sdf_operations.py +990 -0
  35. DeepSDFStruct/sdf_primitives.py +1416 -0
  36. DeepSDFStruct/splinepy_unitcells/__init__.py +35 -0
  37. DeepSDFStruct/splinepy_unitcells/chi_3D.py +1617 -0
  38. DeepSDFStruct/splinepy_unitcells/cross_lattice.py +165 -0
  39. DeepSDFStruct/splinepy_unitcells/double_lattice_extruded.py +298 -0
  40. DeepSDFStruct/splinepy_unitcells/snappy_3d.py +544 -0
  41. DeepSDFStruct/torch_spline.py +446 -0
  42. DeepSDFStruct/trained_models/analytic_round_cross/LatentCodes/latest.pth +0 -0
  43. DeepSDFStruct/trained_models/analytic_round_cross/ModelParameters/latest.pth +0 -0
  44. DeepSDFStruct/trained_models/analytic_round_cross/OptimizerParameters/latest.pth +0 -0
  45. DeepSDFStruct/trained_models/analytic_round_cross/specs.json +45 -0
  46. DeepSDFStruct/trained_models/chi_and_cross/LatentCodes/latest.pth +0 -0
  47. DeepSDFStruct/trained_models/chi_and_cross/Logs.pth +0 -0
  48. DeepSDFStruct/trained_models/chi_and_cross/ModelParameters/latest.pth +0 -0
  49. DeepSDFStruct/trained_models/chi_and_cross/OptimizerParameters/latest.pth +0 -0
  50. DeepSDFStruct/trained_models/chi_and_cross/specs.json +76 -0
  51. DeepSDFStruct/trained_models/primitives/LatentCodes/latest.pth +0 -0
  52. DeepSDFStruct/trained_models/primitives/ModelParameters/latest.pth +0 -0
  53. DeepSDFStruct/trained_models/primitives/OptimizerParameters/latest.pth +0 -0
  54. DeepSDFStruct/trained_models/primitives/specs.json +46 -0
  55. DeepSDFStruct/trained_models/primitives_2d/LatentCodes/latest.pth +0 -0
  56. DeepSDFStruct/trained_models/primitives_2d/ModelParameters/latest.pth +0 -0
  57. DeepSDFStruct/trained_models/primitives_2d/OptimizerParameters/latest.pth +0 -0
  58. DeepSDFStruct/trained_models/primitives_2d/specs.json +46 -0
  59. DeepSDFStruct/trained_models/round_cross/LatentCodes/latest.pth +0 -0
  60. DeepSDFStruct/trained_models/round_cross/Logs.pth +0 -0
  61. DeepSDFStruct/trained_models/round_cross/ModelParameters/latest.pth +0 -0
  62. DeepSDFStruct/trained_models/round_cross/OptimizerParameters/latest.pth +0 -0
  63. DeepSDFStruct/trained_models/round_cross/specs.json +45 -0
  64. DeepSDFStruct/trained_models/test_experiment/LatentCodes/latent_code_data_map.json +106 -0
  65. DeepSDFStruct/trained_models/test_experiment/specs.json +45 -0
  66. DeepSDFStruct/trained_models/test_experiment/training_summary.json +10 -0
  67. DeepSDFStruct/trained_models/test_experiment_hierarchical/LatentCodes/latent_code_data_map.json +106 -0
  68. DeepSDFStruct/trained_models/test_experiment_hierarchical/specs.json +45 -0
  69. DeepSDFStruct/trained_models/test_experiment_hierarchical/training_summary.json +10 -0
  70. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latent_code_data_map.json +5006 -0
  71. DeepSDFStruct/trained_models/test_experiment_homogenization/LatentCodes/latest.pth +0 -0
  72. DeepSDFStruct/trained_models/test_experiment_homogenization/Logs.png +0 -0
  73. DeepSDFStruct/trained_models/test_experiment_homogenization/ModelParameters/latest.pth +0 -0
  74. DeepSDFStruct/trained_models/test_experiment_homogenization/OptimizerParameters/latest.pth +0 -0
  75. DeepSDFStruct/trained_models/test_experiment_homogenization/specs.json +83 -0
  76. DeepSDFStruct/trained_models/test_experiment_homogenization/training_summary.json +10 -0
  77. DeepSDFStruct/utils.py +96 -0
  78. DeepSDFStruct/visualization/latent_viewer.py +211 -0
  79. DeepSDFStruct/visualization/latent_viewer_cli.py +8 -0
  80. benchmarks/generate_sdf_showcase.py +622 -0
  81. benchmarks/sdf_from_mesh_benchmark.py +52 -0
  82. benchmarks/sdf_from_mesh_comparison.md +33 -0
  83. benchmarks/sdf_showcase/operations/boolean/difference_sphere_box.png +0 -0
  84. benchmarks/sdf_showcase/operations/boolean/union_sphere_box.png +0 -0
  85. benchmarks/sdf_showcase/operations/transformations/circular_array_sphere.png +0 -0
  86. benchmarks/sdf_showcase/operations/transformations/dilate_sphere.png +0 -0
  87. benchmarks/sdf_showcase/operations/transformations/mirror_sphere.png +0 -0
  88. benchmarks/sdf_showcase/operations/transformations/repeat_sphere.png +0 -0
  89. benchmarks/sdf_showcase/operations/transformations/revolve_circle.png +0 -0
  90. benchmarks/sdf_showcase/operations/transformations/shell_sphere.png +0 -0
  91. benchmarks/sdf_showcase/operations/transformations/twist_torus.png +0 -0
  92. benchmarks/sdf_showcase/primitives/2D/circle.png +0 -0
  93. benchmarks/sdf_showcase/primitives/2D/equilateral_triangle.png +0 -0
  94. benchmarks/sdf_showcase/primitives/2D/hexagon.png +0 -0
  95. benchmarks/sdf_showcase/primitives/2D/polygon_pentagon.png +0 -0
  96. benchmarks/sdf_showcase/primitives/2D/rectangle.png +0 -0
  97. benchmarks/sdf_showcase/primitives/2D/rounded_rectangle.png +0 -0
  98. benchmarks/sdf_showcase/primitives/3D/box.png +0 -0
  99. benchmarks/sdf_showcase/primitives/3D/capped_cone.png +0 -0
  100. benchmarks/sdf_showcase/primitives/3D/capped_cylinder.png +0 -0
  101. benchmarks/sdf_showcase/primitives/3D/capsule.png +0 -0
  102. benchmarks/sdf_showcase/primitives/3D/cone.png +0 -0
  103. benchmarks/sdf_showcase/primitives/3D/corner_spheres.png +0 -0
  104. benchmarks/sdf_showcase/primitives/3D/cross_ms.png +0 -0
  105. benchmarks/sdf_showcase/primitives/3D/cylinder.png +0 -0
  106. benchmarks/sdf_showcase/primitives/3D/dodecahedron.png +0 -0
  107. benchmarks/sdf_showcase/primitives/3D/ellipsoid.png +0 -0
  108. benchmarks/sdf_showcase/primitives/3D/icosahedron.png +0 -0
  109. benchmarks/sdf_showcase/primitives/3D/octahedron.png +0 -0
  110. benchmarks/sdf_showcase/primitives/3D/pyramid.png +0 -0
  111. benchmarks/sdf_showcase/primitives/3D/rounded_box.png +0 -0
  112. benchmarks/sdf_showcase/primitives/3D/rounded_cone.png +0 -0
  113. benchmarks/sdf_showcase/primitives/3D/rounded_cylinder.png +0 -0
  114. benchmarks/sdf_showcase/primitives/3D/sphere.png +0 -0
  115. benchmarks/sdf_showcase/primitives/3D/tetrahedron.png +0 -0
  116. benchmarks/sdf_showcase/primitives/3D/torus.png +0 -0
  117. benchmarks/sdf_showcase/primitives/3D/wireframe_box.png +0 -0
  118. deepsdfstruct-1.7.2.dist-info/METADATA +44 -0
  119. deepsdfstruct-1.7.2.dist-info/RECORD +165 -0
  120. deepsdfstruct-1.7.2.dist-info/WHEEL +5 -0
  121. deepsdfstruct-1.7.2.dist-info/licenses/LICENSE +208 -0
  122. deepsdfstruct-1.7.2.dist-info/licenses/NOTICE +67 -0
  123. deepsdfstruct-1.7.2.dist-info/scm_file_list.json +178 -0
  124. deepsdfstruct-1.7.2.dist-info/scm_version.json +8 -0
  125. deepsdfstruct-1.7.2.dist-info/top_level.txt +4 -0
  126. docs/Makefile +20 -0
  127. docs/api_reference.rst +6 -0
  128. docs/conf.py +60 -0
  129. docs/index.rst +136 -0
  130. docs/make.bat +35 -0
  131. docs/qr_code_github.png +0 -0
  132. docs/qr_code_paper.png +0 -0
  133. docs/readme_images/example_output_01.png +0 -0
  134. docs/readme_images/example_output_02.png +0 -0
  135. docs/readme_images/example_output_03.png +0 -0
  136. docs/readme_images/example_output_04.png +0 -0
  137. tests/data/chairs/1005.obj +282 -0
  138. tests/data/chairs/1024.obj +222 -0
  139. tests/data/chairs/README.md +1 -0
  140. tests/data/circular_balls_test_case.stl +0 -0
  141. tests/data/cone.stl +1794 -0
  142. tests/data/example_disconnectd_mesh.inp +1784 -0
  143. tests/data/example_line_mesh.vtk +0 -0
  144. tests/data/stanford_bunny.stl +0 -0
  145. tests/data/sweep_test_case.stl +0 -0
  146. tests/test_DOE.py +49 -0
  147. tests/test_elongate_sdf.py +195 -0
  148. tests/test_flexisquares.py +187 -0
  149. tests/test_generate_dataset.py +76 -0
  150. tests/test_lattice_evaluation.py +137 -0
  151. tests/test_mesh_export.py +137 -0
  152. tests/test_mesh_functions.py +49 -0
  153. tests/test_networks.py +71 -0
  154. tests/test_pretrained_models.py +40 -0
  155. tests/test_reconstruction.py +101 -0
  156. tests/test_sdf_from_mesh.py +52 -0
  157. tests/test_sdf_functions.py +117 -0
  158. tests/test_sdf_primitives.py +668 -0
  159. tests/test_sdf_trimesh_comparison.py +482 -0
  160. tests/test_splinepy_unitcells.py +64 -0
  161. tests/test_structural_optimization.py +178 -0
  162. tests/test_torch_spline.py +216 -0
  163. tests/test_train_model.py +110 -0
  164. tests/test_training_data_ids.py +41 -0
  165. tests/tmp_outputs/temp_file.txt +0 -0
@@ -0,0 +1,670 @@
1
+ """
2
+ SDF Sampling and Dataset Generation
3
+ ====================================
4
+
5
+ This module provides tools for sampling points from SDF representations and
6
+ generating datasets for training neural networks (DeepSDF models). It supports
7
+ various sampling strategies to create well-distributed training data.
8
+
9
+ Key Features
10
+ ------------
11
+
12
+ Sampling Strategies
13
+ - Uniform sampling in a bounding box
14
+ - Surface-focused sampling near the zero level set
15
+ - Importance sampling based on SDF gradients
16
+ - Sphere-based sampling patterns
17
+ - Combined strategies for balanced datasets
18
+
19
+ Dataset Generation
20
+ - Batch processing of multiple geometries
21
+ - Automatic data normalization and standardization
22
+ - Support for multiple geometry classes
23
+ - Metadata tracking (version, sampling parameters)
24
+ - Export to formats compatible with DeepSDF training
25
+
26
+ The module is designed to generate high-quality training data for implicit
27
+ neural representations, with careful attention to sampling near surfaces
28
+ where accurate reconstruction is most critical.
29
+
30
+ Classes
31
+ -------
32
+ SampledSDF
33
+ Container for sampled points and their SDF values, with utilities
34
+ for splitting by sign and visualization.
35
+
36
+ DataSetInfo
37
+ TypedDict for dataset metadata (name, classes, sampling strategy, etc.).
38
+
39
+ Functions
40
+ ---------
41
+ generate_dataset
42
+ Batch process multiple geometries to create a complete dataset.
43
+ sample_sdf_*
44
+ Various sampling strategies for different use cases.
45
+ """
46
+
47
+ import os
48
+ from functools import partial
49
+ from itertools import starmap
50
+ import multiprocessing
51
+ from dataclasses import dataclass
52
+
53
+
54
+ import vtk
55
+ import numpy as np
56
+ import json
57
+ import pathlib
58
+ import typing
59
+ import gustaf as gus
60
+ import trimesh
61
+ from DeepSDFStruct.SDF import SDFfromMesh, SDFBase
62
+ from DeepSDFStruct.mesh import torchSurfMesh
63
+ import DeepSDFStruct
64
+
65
+ # from analysis.problems.homogenization import computeHomogenizedMaterialProperties
66
+ import splinepy
67
+ import torch
68
+ from collections import defaultdict
69
+ from tqdm import tqdm
70
+ import logging
71
+ import datetime
72
+ from importlib.metadata import version
73
+
74
+ logger = logging.getLogger(DeepSDFStruct.__name__)
75
+
76
+
77
+ class DataSetInfo(typing.TypedDict):
78
+ """Metadata for a generated SDF dataset.
79
+
80
+ Attributes
81
+ ----------
82
+ dataset_name : str
83
+ Unique identifier for the dataset.
84
+ class_names : list of str
85
+ Names of geometry classes in the dataset.
86
+ sampling_strategy : str
87
+ Description of how points were sampled.
88
+ date_created : str
89
+ ISO format timestamp of dataset creation.
90
+ stds : list of float
91
+ Standard deviations used for normalization.
92
+ n_samples : int
93
+ Number of sample points per geometry.
94
+ add_surface_samples : bool
95
+ Whether surface points were included.
96
+ sdf_struct_version : str
97
+ Version of DeepSDFStruct used to create the dataset.
98
+ """
99
+
100
+ dataset_name: str
101
+ class_names: list[str]
102
+ sampling_strategy: str
103
+ date_created: str
104
+ stds: list[float]
105
+ n_samples: int
106
+ add_surface_samples: bool
107
+ sdf_struct_version: str
108
+
109
+
110
+ class SphereParameters(typing.TypedDict):
111
+ """Parameters defining a sampling sphere."""
112
+
113
+ cx: float
114
+ cy: float
115
+ cz: float
116
+ r: float
117
+
118
+
119
+ @dataclass
120
+ class GeometryInstance:
121
+ name: str
122
+ geom: trimesh.Trimesh
123
+ C: np.ndarray | None
124
+
125
+ def __init__(self, name: str, geom: trimesh.Trimesh, C: np.ndarray | None):
126
+ self.name = name
127
+ self.geom = geom
128
+ self.C = C
129
+
130
+
131
+ class SampledSDF:
132
+ """Container for sampled SDF points and their distance values.
133
+
134
+ This class stores point samples and their corresponding SDF values,
135
+ providing utilities for data manipulation, splitting, and visualization.
136
+
137
+ Parameters
138
+ ----------
139
+ samples : torch.Tensor
140
+ Point coordinates of shape (N, 3).
141
+ distances : torch.Tensor
142
+ SDF values at sample points of shape (N, 1).
143
+
144
+ Attributes
145
+ ----------
146
+ samples : torch.Tensor
147
+ The sampled point coordinates.
148
+ distances : torch.Tensor
149
+ The SDF distance values.
150
+
151
+ Methods
152
+ -------
153
+ split_pos_neg()
154
+ Split into separate datasets for inside (negative) and outside
155
+ (positive) points.
156
+ create_gus_plottable()
157
+ Convert to gustaf Vertices for visualization.
158
+ stacked
159
+ Property returning concatenated samples and distances.
160
+
161
+ Examples
162
+ --------
163
+ >>> import torch
164
+ >>> from DeepSDFStruct.sampling import SampledSDF
165
+ >>>
166
+ >>> points = torch.rand(100, 3)
167
+ >>> distances = torch.randn(100, 1)
168
+ >>> sampled = SampledSDF(points, distances)
169
+ >>>
170
+ >>> # Split by sign
171
+ >>> inside, outside = sampled.split_pos_neg()
172
+ >>> print(f"Inside points: {inside.samples.shape[0]}")
173
+ >>> print(f"Outside points: {outside.samples.shape[0]}")
174
+ """
175
+
176
+ samples: torch.Tensor
177
+ distances: torch.Tensor
178
+
179
+ def split_pos_neg(self):
180
+ """Split samples into inside (negative) and outside (positive) points.
181
+
182
+ Returns
183
+ -------
184
+ pos : SampledSDF
185
+ Samples with non-negative distances (outside or on surface).
186
+ neg : SampledSDF
187
+ Samples with negative distances (inside geometry).
188
+ """
189
+ pos_mask = torch.where(self.distances >= 0.0)[0]
190
+ neg_mask = torch.where(self.distances < 0.0)[0]
191
+ pos = SampledSDF(
192
+ samples=self.samples[pos_mask], distances=self.distances[pos_mask]
193
+ )
194
+ neg = SampledSDF(
195
+ samples=self.samples[neg_mask], distances=self.distances[neg_mask]
196
+ )
197
+ return pos, neg
198
+
199
+ def create_gus_plottable(self):
200
+ """Create a gustaf Vertices object for visualization.
201
+
202
+ Returns
203
+ -------
204
+ gustaf.Vertices
205
+ Vertices with distance values stored as vertex data.
206
+ """
207
+ vp = gus.Vertices(vertices=self.samples)
208
+ vp.vertex_data["distance"] = self.distances
209
+ return vp
210
+
211
+ @property
212
+ def stacked(self):
213
+ """Concatenate samples and distances into a single tensor.
214
+
215
+ Returns
216
+ -------
217
+ torch.Tensor
218
+ Tensor of shape (N, 4) with [x, y, z, distance] per row.
219
+ """
220
+ return torch.hstack((self.samples, self.distances))
221
+
222
+ def __init__(self, samples, distances):
223
+ self.samples = samples
224
+ self.distances = distances
225
+
226
+ def __add__(self, other):
227
+ """Concatenate two SampledSDF objects.
228
+
229
+ Parameters
230
+ ----------
231
+ other : SampledSDF
232
+ Another SampledSDF to concatenate.
233
+
234
+ Returns
235
+ -------
236
+ SampledSDF
237
+ Combined dataset with all samples from both objects.
238
+ """
239
+ return SampledSDF(
240
+ samples=torch.vstack((self.samples, other.samples)),
241
+ distances=torch.vstack((self.distances, other.distances)),
242
+ )
243
+
244
+
245
+ def _process_single_geometry_instance(
246
+ geom_instance: GeometryInstance,
247
+ file_name: str,
248
+ folder_name: pathlib.Path,
249
+ scale,
250
+ n_samples,
251
+ sampling_strategy,
252
+ add_surface_samples,
253
+ stds,
254
+ also_save_vtk,
255
+ also_save_mesh,
256
+ ):
257
+ fname = folder_name / file_name
258
+ if not os.path.exists(folder_name):
259
+ os.makedirs(folder_name, exist_ok=True)
260
+ if os.path.isfile(fname):
261
+ logger.warning(f"File {fname} already exists")
262
+ return
263
+ mesh = None
264
+ geom = geom_instance.geom
265
+ if isinstance(geom, SDFBase):
266
+ sdf = geom
267
+ elif isinstance(geom, trimesh.Trimesh):
268
+ mesh = geom
269
+ sdf = SDFfromMesh(mesh, scale=scale)
270
+ if also_save_mesh:
271
+ mesh.export(fname.with_suffix(".stl"))
272
+ else:
273
+ raise NotImplementedError(
274
+ f"Geometry must be either trimesh or SDFBase, but not {type(geom)}."
275
+ )
276
+ sampled_sdf = random_sample_sdf(
277
+ sdf,
278
+ bounds=(-1, 1),
279
+ n_samples=int(n_samples),
280
+ sampling_strategy=sampling_strategy,
281
+ )
282
+ if add_surface_samples:
283
+ if not isinstance(mesh, trimesh.Trimesh):
284
+ logger.warning(
285
+ "Add surface samples was specified, but geometry"
286
+ f"is not given as a mesh but as {type(geom)}"
287
+ )
288
+ else:
289
+ surf_samples = sample_mesh_surface(
290
+ sdf, mesh, int(n_samples // 2), stds, device="cpu", dtype=torch.float32
291
+ )
292
+ sampled_sdf += surf_samples
293
+ pos, neg = sampled_sdf.split_pos_neg()
294
+ if geom_instance.C is not None:
295
+ np.savez(fname, neg=neg.stacked, pos=pos.stacked, C=geom_instance.C)
296
+ else:
297
+ np.savez(fname, neg=neg.stacked, pos=pos.stacked)
298
+ if also_save_vtk:
299
+ save_points_to_vtp(fname.with_suffix(".vtp"), neg=neg.stacked, pos=pos.stacked)
300
+
301
+
302
+ class SDFSampler:
303
+ def __init__(
304
+ self,
305
+ outdir,
306
+ splitdir,
307
+ dataset_name,
308
+ stds=[0.05, 0.025],
309
+ overwrite_existing=False,
310
+ ) -> None:
311
+ self.outdir = outdir
312
+ self.splitdir = splitdir
313
+ self.dataset_name = dataset_name
314
+ self.geometries: dict[str, dict[str, GeometryInstance]] = {}
315
+ self.stds = stds
316
+ folder_name = pathlib.Path(outdir) / dataset_name
317
+ if os.path.exists(folder_name):
318
+ if not overwrite_existing:
319
+ raise IsADirectoryError(
320
+ f"Dataset {folder_name} already exists. "
321
+ "Set overwrite_existing to true to overwrite."
322
+ )
323
+ else:
324
+ os.makedirs(folder_name)
325
+
326
+ def add_class(
327
+ self,
328
+ geom_list: list,
329
+ class_name: str,
330
+ n_faces=100,
331
+ homogenized_c: list[np.ndarray] | None = None,
332
+ ) -> None:
333
+ """
334
+ Adds a geometry to the sampler object. Tries to transform inputs to
335
+ trimesh data. In case the geometry is a spline object, the n_faces
336
+ parameter determines the accuracy of the extracted mesh
337
+ """
338
+ if homogenized_c is not None:
339
+ assert len(geom_list) == len(
340
+ homogenized_c
341
+ ), f"Length of geometries ({len(geom_list)}) does not match length of homogenized elasticity tensors ({len(homogenized_c)})"
342
+ instances = {}
343
+ for i, geom in enumerate(geom_list):
344
+ instance_name = f"{class_name}_{i:05}"
345
+ if isinstance(geom, splinepy.Multipatch | splinepy.spline.Spline):
346
+ geom_gus: gus.Faces = geom.extract.faces(n_faces)
347
+ tris = gus.create.faces.to_simplex(geom_gus)
348
+ geom = trimesh.Trimesh(vertices=tris.vertices, faces=tris.faces)
349
+
350
+ elif isinstance(geom, torchSurfMesh):
351
+ geom = geom.to_trimesh()
352
+
353
+ if homogenized_c is not None:
354
+ C = homogenized_c[i]
355
+ else:
356
+ C = None
357
+ instances[instance_name] = GeometryInstance(instance_name, geom, C)
358
+ self.geometries[class_name] = instances
359
+
360
+ def process_geometries(
361
+ self,
362
+ sampling_strategy="uniform",
363
+ n_faces=100,
364
+ n_samples: int = 100000,
365
+ add_surface_samples=True,
366
+ also_save_vtk=False,
367
+ also_save_mesh=True,
368
+ scale=True,
369
+ n_workers=0,
370
+ ):
371
+ tasks = []
372
+
373
+ for class_name, instance_list in self.geometries.items():
374
+ logger.info(
375
+ f"processing geometry list {class_name} with {len(instance_list)} items."
376
+ )
377
+
378
+ folder_name = pathlib.Path(self.outdir) / self.dataset_name / class_name
379
+
380
+ func = partial(
381
+ _process_single_geometry_instance,
382
+ scale=scale,
383
+ n_samples=n_samples,
384
+ sampling_strategy=sampling_strategy,
385
+ add_surface_samples=add_surface_samples,
386
+ stds=self.stds,
387
+ also_save_vtk=also_save_vtk,
388
+ also_save_mesh=also_save_mesh,
389
+ )
390
+
391
+ tasks = [
392
+ (geometry, f"{instance_id}.npz", folder_name)
393
+ for instance_id, geometry in instance_list.items()
394
+ ]
395
+ if n_workers > 0:
396
+ logger.info(f"starting multiprocessing with {n_workers} workers")
397
+ with multiprocessing.Pool(processes=n_workers) as pool:
398
+ pool.starmap(func, tasks)
399
+ else:
400
+ logger.info("starting serial processing of geometries")
401
+ list(starmap(func, tasks))
402
+
403
+ logger.info(f"done processing geometry list {class_name}")
404
+
405
+ summary = DataSetInfo(
406
+ dataset_name=self.dataset_name,
407
+ class_names=list(self.geometries.keys()),
408
+ sampling_strategy=sampling_strategy,
409
+ date_created=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
410
+ stds=self.stds,
411
+ n_samples=n_samples,
412
+ add_surface_samples=add_surface_samples,
413
+ sdf_struct_version=version("DeepSDFStruct"),
414
+ )
415
+ with open(
416
+ str(pathlib.Path(self.outdir) / self.dataset_name / "summary.json"), "w"
417
+ ) as f:
418
+ json.dump(summary, f, indent=4)
419
+
420
+ def get_meshs_from_folder(self, foldername, mesh_type) -> list:
421
+ """
422
+ Reads all mesh files of a given type (extension) from a folder using meshio.
423
+
424
+ Parameters
425
+ ----------
426
+ foldername : str
427
+ Path to the folder containing the mesh files.
428
+ mesh_type : str
429
+ Mesh file extension (e.g., 'vtk', 'obj', 'stl', 'msh', 'xdmf').
430
+
431
+ Returns
432
+ -------
433
+ list[trimesh.Trimesh]
434
+ A list of trimesh.Trimesh objects loaded from the folder.
435
+ """
436
+ meshes = []
437
+
438
+ # Normalize extension (remove dot if present)
439
+ mesh_type = mesh_type.lstrip(".")
440
+
441
+ # Iterate through all files in the folder
442
+
443
+ for filename in tqdm(os.listdir(foldername), desc="Loading meshs"):
444
+ if filename.lower().endswith("." + mesh_type.lower()):
445
+ filepath = os.path.join(foldername, filename)
446
+ try:
447
+ faces = gus.io.meshio.load(filepath)
448
+ trim = trimesh.Trimesh(faces.vertices, faces.elements)
449
+ meshes.append(trim)
450
+ logger.info(f"Loaded mesh: {filename}")
451
+ except ValueError as e:
452
+ logger.warning(f"Could not read {filename}: {e}")
453
+
454
+ if not meshes:
455
+ print(f"No .{mesh_type} meshes found in {foldername}.")
456
+
457
+ return meshes
458
+
459
+ def write_json(self, json_fname):
460
+ json_content = defaultdict(lambda: defaultdict(list))
461
+ for class_name, instance_list in self.geometries.items():
462
+ for instance_id, geometry in instance_list.items():
463
+ file_name = f"{instance_id}"
464
+ json_content[self.dataset_name][class_name].append(file_name)
465
+ # json_content = {
466
+ # data_info["dataset_name"]: {data_info["class_name"]: split_files}
467
+ # }
468
+ json_fname = pathlib.Path(f"{self.splitdir}/{json_fname}")
469
+ if not json_fname.parent.is_dir():
470
+ os.makedirs(json_fname.parent)
471
+ with open(json_fname, "w", encoding="utf-8") as f:
472
+ json.dump(json_content, f, indent=4)
473
+
474
+
475
+ def move(t_mesh, new_center):
476
+ t_mesh.vertices += new_center - t_mesh.bounding_box.centroid
477
+
478
+
479
+ def noisy_sample(t_mesh, std, count):
480
+ return t_mesh.sample(int(count)) + torch.random.normal(
481
+ scale=std, size=(int(count), 3)
482
+ )
483
+
484
+
485
+ def random_points(count):
486
+ """random points in a unit sphere centered at (0, 0, 0)"""
487
+ points = torch.random.uniform(-1, 1, (int(count * 3), 3))
488
+ points = points[torch.linalg.norm(points, axis=1) <= 1]
489
+ if points.shape[0] < count:
490
+ print("Too little random sampling points. Resampling.......")
491
+ random_points(count=count, boundary="unit_sphere")
492
+ elif points.shape[0] > count:
493
+ return points[torch.random.choice(points.shape[0], count)]
494
+ else:
495
+ return points
496
+
497
+
498
+ def random_points_cube(count, box_size):
499
+ """random points in a cube with size box_size centered at (0, 0, 0)"""
500
+ points = torch.random.uniform(-box_size / 2, box_size / 2, (int(count), 3))
501
+ return points
502
+
503
+
504
+ def random_sample_sdf(
505
+ sdf,
506
+ bounds,
507
+ n_samples,
508
+ sampling_strategy="uniform",
509
+ device="cpu",
510
+ dtype=torch.float32,
511
+ ):
512
+ supported_strategies = ("plane", "spherical_gaussian", "uniform")
513
+ bounds = torch.tensor(bounds, dtype=dtype, device=device)
514
+ if sampling_strategy == "plane":
515
+ samples = torch.random.uniform(
516
+ bounds[0], bounds[1], (n_samples, 2), device=device, dtype=dtype
517
+ )
518
+ samples = torch.hstack((samples, torch.zeros((n_samples, 1))))
519
+ elif sampling_strategy == "spherical_gaussian":
520
+ samples = torch.random.randn(n_samples, 3, device=device, dtype=dtype)
521
+ samples /= torch.linalg.norm(samples, axis=1).reshape(-1, 1)
522
+ # samples += torch.random.uniform(bounds[0], bounds[1], (n_samples, 3))
523
+ samples = samples + torch.random.normal(0, 0.01, (n_samples, 3))
524
+ elif sampling_strategy == "uniform":
525
+ samples = (
526
+ torch.rand((n_samples, 3), device=device, dtype=dtype)
527
+ * (bounds[1] - bounds[0])
528
+ + bounds[0]
529
+ )
530
+ else:
531
+ raise ValueError(
532
+ f"Unsupported sampling strategy '{sampling_strategy}'. "
533
+ f"Supported options are: {supported_strategies}."
534
+ )
535
+ distances = sdf(samples)
536
+ return SampledSDF(samples=samples, distances=distances)
537
+
538
+
539
+ def sample_mesh_surface(
540
+ sdf: SDFBase,
541
+ mesh: trimesh.Trimesh,
542
+ n_samples: int,
543
+ stds: list[float],
544
+ device="cpu",
545
+ dtype=torch.float32,
546
+ ) -> SampledSDF:
547
+ """
548
+ Sample noisy points around a mesh surface and evaluate them with a signed distance function (SDF).
549
+
550
+ This function uses trimesh.sample to generate surface samples
551
+ and perturbs them with Gaussian noise of varying standard deviations,
552
+ and queries the SDF at those points.
553
+
554
+ Args:
555
+ sdf (SDFBase): A callable SDF object that takes 3D points and returns signed distances.
556
+ mesh (trimesh.Trimesh): A mesh object containing the vertices and faces.
557
+ n_samples (int): Number of mesh vertices to sample
558
+ stds (list[float]): Standard deviations for Gaussian noise added to sampled vertices.
559
+ - Typical values: [0.05, 0.0015].
560
+ - Larger values spread samples farther from the surface; smaller values keep them closer.
561
+ device (str, optional): Torch device to place tensors on (e.g., "cpu" or "cuda").
562
+ dtype (torch.dtype, optional): Data type for generated tensors (default: torch.float32).
563
+
564
+ Returns:
565
+ SampledSDF: An object containing:
566
+ - samples (torch.Tensor): The perturbed sample points of shape (n_samples * len(stds), 3).
567
+ - distances (torch.Tensor): The corresponding SDF values at those sample points.
568
+ """
569
+ samples = []
570
+
571
+ points, face_idx = trimesh.sample.sample_surface(mesh, n_samples)
572
+
573
+ surface_points = torch.tensor(points, dtype=dtype, device=device)
574
+
575
+ face_normals = torch.tensor(mesh.face_normals[face_idx], dtype=dtype, device=device)
576
+
577
+ for std in stds:
578
+
579
+ t = torch.randn(n_samples, 1, device=device, dtype=dtype) * std
580
+
581
+ noisy = surface_points + t * face_normals
582
+ samples.append(noisy)
583
+
584
+ queries = torch.vstack(samples)
585
+ distances = sdf(queries)
586
+
587
+ return SampledSDF(samples=queries, distances=distances)
588
+
589
+
590
+ def save_points_to_vtp(filename, neg, pos):
591
+ """
592
+ Save pos/neg SDF sample points as a VTU point cloud using vtkPolyData.
593
+ Each point has an SDF scalar value.
594
+ """
595
+ # Combine points
596
+ all_points = np.vstack((pos, neg))
597
+ coords = all_points[:, :3]
598
+ sdf_vals = all_points[:, 3]
599
+
600
+ # --- Create vtkPoints ---
601
+ vtk_points = vtk.vtkPoints()
602
+ for pt in coords:
603
+ vtk_points.InsertNextPoint(pt)
604
+
605
+ # --- Create PolyData ---
606
+ polydata = vtk.vtkPolyData()
607
+ polydata.SetPoints(vtk_points)
608
+
609
+ # Add vertex cells (required for points in PolyData)
610
+ verts = vtk.vtkCellArray()
611
+ for i in range(len(coords)):
612
+ verts.InsertNextCell(1)
613
+ verts.InsertCellPoint(i)
614
+ polydata.SetVerts(verts)
615
+
616
+ # --- Add SDF scalar values ---
617
+ vtk_array = vtk.vtkDoubleArray()
618
+ vtk_array.SetName("SDF")
619
+ vtk_array.SetNumberOfValues(len(sdf_vals))
620
+ for i, val in enumerate(sdf_vals):
621
+ vtk_array.SetValue(i, val)
622
+ polydata.GetPointData().SetScalars(vtk_array)
623
+
624
+ # --- Write to VTU ---
625
+ writer = vtk.vtkXMLPolyDataWriter()
626
+ writer.SetFileName(filename)
627
+ writer.SetInputData(polydata)
628
+ writer.Write()
629
+
630
+ logger.debug(f"Saved {len(coords)} points with SDF to '{filename}'")
631
+
632
+
633
+ def augment_by_FFD(
634
+ meshs: list[trimesh.Trimesh],
635
+ n_control_points: int = 5,
636
+ std_dev_fraction: float | None = 0.05,
637
+ n_transformations: int = 10,
638
+ save_meshs=False,
639
+ ) -> list[trimesh.Trimesh]:
640
+ """
641
+ Takes list of meshs and augments the meshs by applying a freeform deformation
642
+ """
643
+ new_meshs = []
644
+
645
+ for i_mesh, mesh in enumerate(tqdm(meshs, desc="Augmenting meshs")):
646
+ bbox = mesh.bounds # shape (2, 3)
647
+ # Compute approximate spacing between control points along each axis
648
+ spacing = (bbox[1] - bbox[0]) / (n_control_points - 1)
649
+ # Use a fraction of spacing (e.g., 15%) as std_dev
650
+ std_dev_local = std_dev_fraction * spacing
651
+
652
+ for i_FFD in range(n_transformations):
653
+ ffd = splinepy.FFD()
654
+ ffd.mesh = gus.Faces(mesh.vertices, mesh.faces)
655
+ ffd.spline.insert_knots(0, np.linspace(0, 1, n_control_points)[1:-1])
656
+ ffd.spline.insert_knots(1, np.linspace(0, 1, n_control_points)[1:-1])
657
+ ffd.spline.insert_knots(2, np.linspace(0, 1, n_control_points)[1:-1])
658
+ ffd.spline.elevate_degrees([0, 1, 2])
659
+ ffd.spline.control_points += np.random.normal(
660
+ loc=0.0, scale=std_dev_local, size=ffd.spline.control_points.shape
661
+ )
662
+ new_meshs.append(trimesh.Trimesh(ffd.mesh.vertices, ffd.mesh.faces))
663
+ if save_meshs:
664
+ save_meshs = True
665
+
666
+ # Make sure the directory exists
667
+ os.makedirs("tmp", exist_ok=True)
668
+ gus.io.meshio.export(f"tmp/mesh_{i_mesh}_{i_FFD}.obj", ffd.mesh)
669
+
670
+ return new_meshs