sdf-sampler 0.5.0__tar.gz → 0.6.0__tar.gz

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 (36) hide show
  1. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/PKG-INFO +1 -1
  2. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/pyproject.toml +1 -1
  3. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampling/box.py +8 -4
  4. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/test_sampler.py +158 -0
  5. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/.gitignore +0 -0
  6. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/CHANGELOG.md +0 -0
  7. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/LICENSE +0 -0
  8. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/README.md +0 -0
  9. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/__init__.py +0 -0
  10. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/__main__.py +0 -0
  11. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/__init__.py +0 -0
  12. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/flood_fill.py +0 -0
  13. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/normal_idw.py +0 -0
  14. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/normal_offset.py +0 -0
  15. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/pocket.py +0 -0
  16. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/voxel_grid.py +0 -0
  17. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/algorithms/voxel_regions.py +0 -0
  18. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/analyzer.py +0 -0
  19. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/cli.py +0 -0
  20. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/config.py +0 -0
  21. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/io.py +0 -0
  22. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/models/__init__.py +0 -0
  23. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/models/analysis.py +0 -0
  24. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/models/constraints.py +0 -0
  25. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/models/samples.py +0 -0
  26. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampler.py +0 -0
  27. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampling/__init__.py +0 -0
  28. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampling/brush.py +0 -0
  29. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampling/ray_carve.py +0 -0
  30. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/src/sdf_sampler/sampling/sphere.py +0 -0
  31. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/__init__.py +0 -0
  32. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/test_analyzer.py +0 -0
  33. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/test_equivalence.py +0 -0
  34. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/test_integration.py +0 -0
  35. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/tests/test_models.py +0 -0
  36. {sdf_sampler-0.5.0 → sdf_sampler-0.6.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdf-sampler
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation
5
5
  Project-URL: Repository, https://github.com/Chiark-Collective/sdf-sampler
6
6
  Author-email: Liam <liam@example.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdf-sampler"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Auto-analysis and sampling of point clouds for SDF (Signed Distance Field) training data generation"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -80,17 +80,18 @@ def sample_box_inverse_square(
80
80
  """Generate samples from a box with inverse-square density distribution.
81
81
 
82
82
  Samples more points near the surface (point cloud) and fewer far away.
83
+ Uses actual distance to surface for phi values (signed distance).
83
84
 
84
85
  Args:
85
86
  constraint: Box constraint to sample
86
87
  rng: Random number generator
87
- near_band: Near-band width for offset
88
+ near_band: Near-band width for density weighting (not phi assignment)
88
89
  n_samples: Number of samples to generate
89
90
  surface_tree: KDTree of surface points for distance computation
90
91
  falloff: Falloff exponent (higher = faster falloff)
91
92
 
92
93
  Returns:
93
- List of TrainingSample objects
94
+ List of TrainingSample objects with phi = actual signed distance to surface
94
95
  """
95
96
  samples = []
96
97
  center = np.array(constraint.center)
@@ -109,8 +110,11 @@ def sample_box_inverse_square(
109
110
  weight = (near_band / min_dist) ** falloff
110
111
 
111
112
  if rng.random() < min(1.0, weight):
112
- offset = near_band if constraint.sign == SignConvention.EMPTY else -near_band
113
- phi = offset
113
+ # Use actual distance to surface for phi, with sign based on constraint type
114
+ # EMPTY regions have positive phi (outside surface)
115
+ # SOLID regions have negative phi (inside surface)
116
+ sign = 1.0 if constraint.sign == SignConvention.EMPTY else -1.0
117
+ phi = sign * float(dist_to_surface)
114
118
 
115
119
  samples.append(
116
120
  TrainingSample(
@@ -229,3 +229,161 @@ class TestSamplerSignConvention:
229
229
  for s in samples:
230
230
  assert s.phi > 0, f"EMPTY sample should have positive phi, got {s.phi}"
231
231
  assert s.is_free
232
+
233
+
234
+ class TestBoxInverseSquarePhiValues:
235
+ """Tests that box inverse_square samples use actual distance to surface for phi."""
236
+
237
+ @pytest.fixture
238
+ def plane_surface(self):
239
+ """Flat plane at z=0 for easy distance calculation."""
240
+ x = np.linspace(-2, 2, 50)
241
+ y = np.linspace(-2, 2, 50)
242
+ xx, yy = np.meshgrid(x, y)
243
+ xyz = np.column_stack([xx.ravel(), yy.ravel(), np.zeros(2500)])
244
+ return xyz
245
+
246
+ @pytest.fixture
247
+ def box_above_plane(self):
248
+ """Box constraint above z=0 plane (empty region above surface)."""
249
+ return {
250
+ "type": "box",
251
+ "sign": "empty", # Above surface = empty/positive phi
252
+ "center": (0.0, 0.0, 0.5), # Center at z=0.5
253
+ "half_extents": (1.0, 1.0, 0.5), # Extends from z=0 to z=1
254
+ "weight": 1.0,
255
+ }
256
+
257
+ @pytest.fixture
258
+ def box_below_plane(self):
259
+ """Box constraint below z=0 plane (solid region below surface)."""
260
+ return {
261
+ "type": "box",
262
+ "sign": "solid", # Below surface = solid/negative phi
263
+ "center": (0.0, 0.0, -0.5), # Center at z=-0.5
264
+ "half_extents": (1.0, 1.0, 0.5), # Extends from z=-1 to z=0
265
+ "weight": 1.0,
266
+ }
267
+
268
+ def test_inverse_square_phi_is_actual_distance(self, plane_surface, box_above_plane):
269
+ """Verify phi is based on distance to nearest surface point, not constant near_band."""
270
+ sampler = SDFSampler(config=SamplerConfig(total_samples=500))
271
+ samples = sampler.generate(
272
+ xyz=plane_surface,
273
+ constraints=[box_above_plane],
274
+ strategy=SamplingStrategy.INVERSE_SQUARE,
275
+ seed=42,
276
+ )
277
+
278
+ assert len(samples) > 0, "Should generate samples"
279
+
280
+ for s in samples:
281
+ # For a flat plane at z=0, distance to surface is approximately |z|
282
+ # (exact value depends on nearest point in the discrete point cloud)
283
+ z_distance = abs(s.z)
284
+
285
+ # phi should be approximately |z| (within grid spacing tolerance)
286
+ # Grid spacing is ~0.08 units, so allow some tolerance
287
+ assert abs(s.phi - z_distance) < 0.1, (
288
+ f"phi should be approximately equal to |z| distance. "
289
+ f"Got phi={s.phi}, z={s.z}, expected ~{z_distance}"
290
+ )
291
+
292
+ # phi should definitely be positive for empty constraint
293
+ assert s.phi > 0, f"EMPTY sample should have positive phi, got {s.phi}"
294
+
295
+ def test_inverse_square_phi_varies_with_distance(self, plane_surface, box_above_plane):
296
+ """Verify phi values vary based on sample distance from surface."""
297
+ sampler = SDFSampler(config=SamplerConfig(total_samples=500))
298
+ samples = sampler.generate(
299
+ xyz=plane_surface,
300
+ constraints=[box_above_plane],
301
+ strategy=SamplingStrategy.INVERSE_SQUARE,
302
+ seed=42,
303
+ )
304
+
305
+ phi_values = [s.phi for s in samples]
306
+
307
+ # Phi should vary (not be constant ±near_band)
308
+ phi_std = np.std(phi_values)
309
+ assert phi_std > 0.01, (
310
+ f"phi values should vary with distance, got std={phi_std}. "
311
+ "This suggests phi is constant (bug: using near_band instead of distance)"
312
+ )
313
+
314
+ # Should have a range of values, not just near_band=0.02
315
+ phi_min, phi_max = min(phi_values), max(phi_values)
316
+ phi_range = phi_max - phi_min
317
+ assert phi_range > 0.1, (
318
+ f"phi range should be > 0.1, got {phi_range}. "
319
+ "Values: min={phi_min}, max={phi_max}"
320
+ )
321
+
322
+ def test_inverse_square_solid_has_negative_phi(self, plane_surface, box_below_plane):
323
+ """Verify solid box samples have negative phi with magnitude proportional to distance."""
324
+ sampler = SDFSampler(config=SamplerConfig(total_samples=500))
325
+ samples = sampler.generate(
326
+ xyz=plane_surface,
327
+ constraints=[box_below_plane],
328
+ strategy=SamplingStrategy.INVERSE_SQUARE,
329
+ seed=42,
330
+ )
331
+
332
+ for s in samples:
333
+ # SOLID constraint should always have negative phi
334
+ assert s.phi < 0, f"SOLID sample should have negative phi, got {s.phi}"
335
+
336
+ # For flat plane at z=0, solid samples are at z<0
337
+ # Distance to nearest surface point is approximately |z|
338
+ z_distance = abs(s.z)
339
+
340
+ # phi magnitude should be approximately |z| (within grid spacing tolerance)
341
+ assert abs(abs(s.phi) - z_distance) < 0.1, (
342
+ f"phi magnitude should be approximately |z|. "
343
+ f"Got phi={s.phi}, z={s.z}, expected ~{-z_distance}"
344
+ )
345
+
346
+ def test_inverse_square_phi_correlates_with_z_coordinate(self, plane_surface, box_above_plane):
347
+ """For plane at z=0, phi should be correlated with |z| coordinate."""
348
+ sampler = SDFSampler(config=SamplerConfig(total_samples=200))
349
+ samples = sampler.generate(
350
+ xyz=plane_surface,
351
+ constraints=[box_above_plane],
352
+ strategy=SamplingStrategy.INVERSE_SQUARE,
353
+ seed=123,
354
+ )
355
+
356
+ # Collect z values and phi values
357
+ z_values = np.array([abs(s.z) for s in samples])
358
+ phi_values = np.array([s.phi for s in samples])
359
+
360
+ # phi should be positively correlated with |z|
361
+ # (samples further from z=0 should have larger phi)
362
+ correlation = np.corrcoef(z_values, phi_values)[0, 1]
363
+ assert correlation > 0.9, (
364
+ f"phi should be strongly correlated with |z|. "
365
+ f"Got correlation={correlation:.3f}"
366
+ )
367
+
368
+ def test_inverse_square_not_constant_near_band(self, plane_surface, box_above_plane):
369
+ """Explicitly verify phi is NOT the constant near_band value."""
370
+ near_band = 0.02 # Default near_band value
371
+ sampler = SDFSampler(config=SamplerConfig(total_samples=200, near_band=near_band))
372
+ samples = sampler.generate(
373
+ xyz=plane_surface,
374
+ constraints=[box_above_plane],
375
+ strategy=SamplingStrategy.INVERSE_SQUARE,
376
+ seed=42,
377
+ )
378
+
379
+ # Count how many samples have phi approximately equal to near_band
380
+ near_band_count = sum(1 for s in samples if abs(abs(s.phi) - near_band) < 0.001)
381
+ total = len(samples)
382
+
383
+ # With actual distance-based phi, very few samples should be exactly at near_band
384
+ # (only those that happen to be exactly 0.02 away from surface)
385
+ ratio = near_band_count / total
386
+ assert ratio < 0.1, (
387
+ f"{near_band_count}/{total} ({ratio:.0%}) samples have phi≈±near_band. "
388
+ "This suggests phi is still using constant near_band instead of actual distance."
389
+ )
File without changes
File without changes
File without changes
File without changes
File without changes