• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mkofler96 / DeepSDFStruct / 18166301994

01 Oct 2025 02:55PM UTC coverage: 73.652% (+1.7%) from 71.932%
18166301994

push

github

mkofler96
removed deep sdf utils

235 of 321 branches covered (73.21%)

Branch coverage included in aggregate %.

2060 of 2795 relevant lines covered (73.7%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.65
DeepSDFStruct/sampling.py
1
import os
1✔
2
import numpy as np
1✔
3
import json
1✔
4
import pathlib
1✔
5
import typing
1✔
6
import gustaf as gus
1✔
7
import trimesh
1✔
8
from DeepSDFStruct.SDF import SDFfromMesh, SDFBase
1✔
9
import DeepSDFStruct
1✔
10

11
# from analysis.problems.homogenization import computeHomogenizedMaterialProperties
12
import splinepy
1✔
13
import torch
1✔
14
from collections import defaultdict
1✔
15
from tqdm import tqdm
1✔
16
import logging
1✔
17

18

19
logger = logging.getLogger(DeepSDFStruct.__name__)
1✔
20

21

22
class DataSetInfo(typing.TypedDict):
1✔
23
    dataset_name: str
1✔
24
    class_name: str
1✔
25

26

27
class SphereParameters(typing.TypedDict):
1✔
28
    cx: float
1✔
29
    cy: float
1✔
30
    cz: float
1✔
31
    r: float
1✔
32

33

34
class SampledSDF:
1✔
35
    samples: torch.tensor
1✔
36
    distances: torch.tensor
1✔
37

38
    def split_pos_neg(self):
1✔
39
        pos_mask = torch.where(self.distances >= 0.0)[0]
1✔
40
        neg_mask = torch.where(self.distances < 0.0)[0]
1✔
41
        pos = SampledSDF(
1✔
42
            samples=self.samples[pos_mask], distances=self.distances[pos_mask]
43
        )
44
        neg = SampledSDF(
1✔
45
            samples=self.samples[neg_mask], distances=self.distances[neg_mask]
46
        )
47
        return pos, neg
1✔
48

49
    def create_gus_plottable(self):
1✔
50
        vp = gus.Vertices(vertices=self.samples)
×
51
        vp.vertex_data["distance"] = self.distances
×
52
        return vp
×
53

54
    @property
1✔
55
    def stacked(self):
1✔
56
        return torch.hstack((self.samples, self.distances))
1✔
57

58
    def __init__(self, samples, distances):
1✔
59
        self.samples = samples
1✔
60
        self.distances = distances
1✔
61

62
    def __add__(self, other):
1✔
63
        return SampledSDF(
1✔
64
            samples=torch.vstack((self.samples, other.samples)),
65
            distances=torch.vstack((self.distances, other.distances)),
66
        )
67

68

69
def process_single_geometry(args):
1✔
70
    (
×
71
        class_name,
72
        instance_id,
73
        geometry,
74
        outdir,
75
        dataset_name,
76
        unify_multipatches,
77
        compute_mechanical_properties,
78
        n_faces,
79
        n_samples,
80
        sampling_strategy,
81
        show,
82
        get_sdf_from_geometry,
83
        sample_sdf,
84
    ) = args
85

86
    logger.info(f"processing {instance_id} in geometry list {class_name}")
×
87
    file_name = f"{instance_id}.npz"
×
88

89
    folder_name = pathlib.Path(outdir) / dataset_name / class_name
×
90
    fname = folder_name / file_name
×
91

92
    if not os.path.exists(folder_name):
×
93
        os.makedirs(folder_name, exist_ok=True)
×
94

95
    if os.path.isfile(fname) and not show:
×
96
        logger.warning(f"File {fname} already exists")
×
97
        return
×
98

99
    sdf = get_sdf_from_geometry(geometry, n_faces, unify_multipatches)
×
100
    pos, neg = sample_sdf(
×
101
        sdf, show=show, n_samples=n_samples, sampling_strategy=sampling_strategy
102
    )
103

104
    if compute_mechanical_properties:
×
105
        mesh_file_name = f"{instance_id}.mesh"
×
106
        raise NotImplementedError("Compute homogenized material not available yet.")
×
107
        mesh_file_path = folder_name / "homogenization" / instance_id / mesh_file_name
108
        # E = computeHomogenizedMaterialProperties(
109
        #     sdf, mesh_file_path=mesh_file_path, mirror=True
110
        # )
111
        np.savez(fname, neg=neg.stacked, pos=pos.stacked, E=E)
112
    else:
113
        np.savez(fname, neg=neg.stacked, pos=pos.stacked)
×
114

115

116
class SDFSampler:
1✔
117
    def __init__(self, outdir, splitdir, dataset_name, unify_multipatches=True) -> None:
1✔
118
        self.outdir = outdir
1✔
119
        self.splitdir = splitdir
1✔
120
        self.dataset_name = dataset_name
1✔
121
        self.unify_multipatches = unify_multipatches
1✔
122
        self.geometries = {}
1✔
123

124
    def add_class(self, geom_list: list, class_name: str) -> None:
1✔
125
        instances = {}
1✔
126
        for i, geom in enumerate(geom_list):
1✔
127
            instance_name = f"{class_name}_{i:05}"
1✔
128
            instances[instance_name] = geom
1✔
129
        self.geometries[class_name] = instances
1✔
130

131
    def get_SDF_list(self, n_faces=100) -> list[SDFBase]:
1✔
132
        sdf_list = []
×
133
        for class_name, instance_list in self.geometries.items():
×
134
            logger.info(f"processing geometry list {class_name}")
×
135
            for instance_id, geometry in tqdm(
×
136
                instance_list.items(), desc="Processing instances"
137
            ):
138
                sdf = self.get_sdf_from_geometry(
×
139
                    geometry, n_faces, self.unify_multipatches
140
                )
141
                sdf_list.append(sdf)
×
142
        return sdf_list
×
143

144
    def process_geometries(
1✔
145
        self,
146
        sampling_strategy="uniform",
147
        n_faces=100,
148
        n_samples: int = 1e5,
149
        unify_multipatches=True,
150
        compute_mechanical_properties=True,
151
        show=False,
152
    ):
153
        for class_name, instance_list in self.geometries.items():
1✔
154
            logger.info(f"processing geometry list {class_name}")
1✔
155
            for instance_id, geometry in tqdm(
1✔
156
                instance_list.items(), desc="Processing instances"
157
            ):
158

159
                file_name = f"{instance_id}.npz"
1✔
160

161
                folder_name = pathlib.Path(self.outdir) / self.dataset_name / class_name
1✔
162
                fname = folder_name / file_name
1✔
163
                if not os.path.exists(folder_name):
1✔
164
                    os.makedirs(folder_name)
1✔
165
                if os.path.isfile(fname) and show == False:
1✔
166
                    logger.warning(f"File {fname} already exists")
×
167
                    continue
×
168
                sdf = self.get_sdf_from_geometry(
1✔
169
                    geometry, n_faces, self.unify_multipatches
170
                )
171
                pos, neg = self.sample_sdf(
1✔
172
                    sdf,
173
                    show=show,
174
                    n_samples=n_samples,
175
                    sampling_strategy=sampling_strategy,
176
                )
177
                if compute_mechanical_properties:
1✔
178
                    mesh_file_name = f"{instance_id}.mesh"
×
179
                    mesh_file_path = (
×
180
                        folder_name / "homogenization" / instance_id / mesh_file_name
181
                    )
182
                    raise NotImplementedError(
×
183
                        "Compute homogenized material not available yet."
184
                    )
185
                    E = computeHomogenizedMaterialProperties(
186
                        sdf, mesh_file_path=mesh_file_path, mirror=True
187
                    )
188
                    np.savez(fname, neg=neg.stacked, pos=pos.stacked, E=E)
189
                else:
190
                    np.savez(fname, neg=neg.stacked, pos=pos.stacked)
1✔
191

192
    def sample_sdf(
1✔
193
        self,
194
        sdf,
195
        show=False,
196
        n_samples: int = 1e5,
197
        sampling_strategy="uniform",
198
        box_size=None,
199
        stds=[0.0025, 0.00025],
200
    ):
201

202
        sampled_sdf = random_sample_sdf(
1✔
203
            sdf, bounds=(-1, 1), n_samples=int(n_samples), type=sampling_strategy
204
        )
205

206
        pos, neg = sampled_sdf.split_pos_neg()
1✔
207

208
        if show:
1✔
209
            vp_pos = pos.create_gus_plottable()
×
210
            vp_neg = neg.create_gus_plottable()
×
211
            vp_pos.show_options["cmap"] = "coolwarm"
×
212
            vp_neg.show_options["cmap"] = "coolwarm"
×
213
            vp_pos.show_options["vmin"] = -0.1
×
214
            vp_pos.show_options["vmax"] = 0.1
×
215
            vp_neg.show_options["vmin"] = -0.1
×
216
            vp_neg.show_options["vmax"] = 0.1
×
217
            gus.show(vp_neg, vp_pos)
×
218
        return pos, neg
1✔
219

220
    def get_sdf_from_geometry(
1✔
221
        self,
222
        geometry,
223
        n_faces: int,
224
        unify_multipatches: bool = True,
225
        threshold: float = 1e-5,
226
    ) -> SDFBase:
227
        if isinstance(geometry, splinepy.Multipatch):
1✔
228
            if unify_multipatches:
1✔
229
                patch_meshs = []
1✔
230
                for patch in geometry.patches:
1✔
231
                    patch_faces = patch.extract.faces(n_faces)
1✔
232
                    patch_mesh = trimesh.Trimesh(
1✔
233
                        vertices=patch_faces.vertices, faces=patch_faces.faces
234
                    )
235
                    # add all patches as meshs to one boolean addition
236
                    patch_meshs.append(SDFfromMesh(patch_mesh))
1✔
237
                sdf_geom = patch_meshs[0]
1✔
238
                for pm in patch_meshs[1:]:
1✔
239
                    sdf_geom = sdf_geom + pm
1✔
240
            else:
241
                sdf_geom = SDFfromMesh(
×
242
                    geometry.extract.faces(n_faces), threshold=threshold
243
                )
244

245
        else:
246
            raise NotImplementedError(
×
247
                f"Geometry of type {type(geometry)} not supported yet."
248
            )
249

250
        return sdf_geom
1✔
251

252
    def write_json(self, json_fname):
1✔
253
        json_content = defaultdict(lambda: defaultdict(list))
1✔
254
        for class_name, instance_list in self.geometries.items():
1✔
255
            for instance_id, geometry in instance_list.items():
1✔
256
                file_name = f"{instance_id}"
1✔
257
                json_content[self.dataset_name][class_name].append(file_name)
1✔
258
        # json_content = {
259
        #     data_info["dataset_name"]: {data_info["class_name"]: split_files}
260
        # }
261
        json_fname = pathlib.Path(f"{self.splitdir}/{json_fname}")
1✔
262
        if not json_fname.parent.is_dir():
1✔
263
            os.makedirs(json_fname.parent)
1✔
264
        with open(json_fname, "w", encoding="utf-8") as f:
1✔
265
            json.dump(json_content, f, indent=4)
1✔
266

267

268
def move(t_mesh, new_center):
1✔
269
    t_mesh.vertices += new_center - t_mesh.bounding_box.centroid
×
270

271

272
def noisy_sample(t_mesh, std, count):
1✔
273
    return t_mesh.sample(int(count)) + torch.random.normal(
×
274
        scale=std, size=(int(count), 3)
275
    )
276

277

278
def random_points(count):
1✔
279
    """random points in a unit sphere centered at (0, 0, 0)"""
280
    points = torch.random.uniform(-1, 1, (int(count * 3), 3))
×
281
    points = points[torch.linalg.norm(points, axis=1) <= 1]
×
282
    if points.shape[0] < count:
×
283
        print("Too little random sampling points. Resampling.......")
×
284
        random_points(count=count, boundary="unit_sphere")
×
285
    elif points.shape[0] > count:
×
286
        return points[torch.random.choice(points.shape[0], count)]
×
287
    else:
288
        return points
×
289

290

291
def random_points_cube(count, box_size):
1✔
292
    """random points in a cube with size box_size centered at (0, 0, 0)"""
293
    points = torch.random.uniform(-box_size / 2, box_size / 2, (int(count), 3))
×
294
    return points
×
295

296

297
def random_sample_sdf(
1✔
298
    sdf, bounds, n_samples, type="uniform", device="cpu", dtype=torch.float32
299
):
300

301
    bounds = torch.tensor(bounds, dtype=dtype, device=device)
1✔
302
    if type == "plane":
1✔
303
        samples = torch.random.uniform(
×
304
            bounds[0], bounds[1], (n_samples, 2), device=device, dtype=dtype
305
        )
306
        samples = torch.hstack((samples, torch.zeros((n_samples, 1))))
×
307
    elif type == "spherical_gaussian":
1✔
308
        samples = torch.random.randn(n_samples, 3, device=device, dtype=dtype)
×
309
        samples /= torch.linalg.norm(samples, axis=1).reshape(-1, 1)
×
310
        # samples += torch.random.uniform(bounds[0], bounds[1], (n_samples, 3))
311
        samples = samples + torch.random.normal(0, 0.01, (n_samples, 3))
×
312
    elif type == "uniform":
1✔
313
        samples = (
1✔
314
            torch.rand((n_samples, 3), device=device, dtype=dtype)
315
            * (bounds[1] - bounds[0])
316
            + bounds[0]
317
        )
318
    distances = sdf(samples)
1✔
319
    return SampledSDF(samples=samples, distances=distances)
1✔
320

321

322
def sample_mesh_surface(
1✔
323
    sdf: SDFBase,
324
    mesh: gus.Faces,
325
    n_samples: int,
326
    stds: list[float],
327
    device="cpu",
328
    dtype=torch.float32,
329
) -> SampledSDF:
330
    """
331
    Sample noisy points around a mesh surface and evaluate them with a signed distance function (SDF).
332

333
    This function uses trimesh.sample to generate surface samples
334
    and perturbs them with Gaussian noise of varying standard deviations,
335
    and queries the SDF at those points.
336

337
    Args:
338
        sdf (SDFBase): A callable SDF object that takes 3D points and returns signed distances.
339
        mesh (gus.Faces): A mesh object containing the vertices.
340
        n_samples (int): Number of mesh vertices to sample
341
        stds (list[float]): Standard deviations for Gaussian noise added to sampled vertices.
342
            - Typical values: [0.05, 0.0015].
343
            - Larger values spread samples farther from the surface; smaller values keep them closer.
344
        device (str, optional): Torch device to place tensors on (e.g., "cpu" or "cuda").
345
        dtype (torch.dtype, optional): Data type for generated tensors (default: torch.float32).
346

347
    Returns:
348
        SampledSDF: An object containing:
349
            - samples (torch.Tensor): The perturbed sample points of shape (n_samples * len(stds), 3).
350
            - distances (torch.Tensor): The corresponding SDF values at those sample points.
351
    """
352
    samples = []
1✔
353

354
    trim = trimesh.Trimesh(mesh.vertices, mesh.faces)
1✔
355

356
    random_samples = torch.tensor(trim.sample(n_samples), dtype=dtype, device=device)
1✔
357

358
    for std in stds:
1✔
359
        noise = torch.randn((n_samples, 3), device=device, dtype=dtype) * std
1✔
360
        samples.append(random_samples + noise)
1✔
361

362
    queries = torch.vstack(samples)
1✔
363

364
    distances = sdf(queries)
1✔
365

366
    return SampledSDF(samples=queries, distances=distances)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc