sdf-sampler 0.3.0__tar.gz → 0.4.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.3.0 → sdf_sampler-0.4.0}/CHANGELOG.md +11 -2
  2. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/PKG-INFO +1 -1
  3. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/pyproject.toml +1 -1
  4. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/__init__.py +1 -1
  5. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/analyzer.py +2 -1
  6. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/cli.py +9 -9
  7. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/analysis.py +7 -0
  8. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampler.py +9 -7
  9. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_equivalence.py +117 -0
  10. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/uv.lock +1 -1
  11. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/.gitignore +0 -0
  12. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/LICENSE +0 -0
  13. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/README.md +0 -0
  14. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/__main__.py +0 -0
  15. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/__init__.py +0 -0
  16. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/flood_fill.py +0 -0
  17. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/normal_idw.py +0 -0
  18. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/normal_offset.py +0 -0
  19. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/pocket.py +0 -0
  20. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/voxel_grid.py +0 -0
  21. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/algorithms/voxel_regions.py +0 -0
  22. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/config.py +0 -0
  23. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/io.py +0 -0
  24. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/__init__.py +0 -0
  25. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/constraints.py +0 -0
  26. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/models/samples.py +0 -0
  27. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/__init__.py +0 -0
  28. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/box.py +0 -0
  29. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/brush.py +0 -0
  30. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/ray_carve.py +0 -0
  31. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/src/sdf_sampler/sampling/sphere.py +0 -0
  32. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/__init__.py +0 -0
  33. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_analyzer.py +0 -0
  34. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_integration.py +0 -0
  35. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_models.py +0 -0
  36. {sdf_sampler-0.3.0 → sdf_sampler-0.4.0}/tests/test_sampler.py +0 -0
@@ -5,6 +5,15 @@ All notable changes to sdf-sampler will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2025-01-30
9
+
10
+ ### Changed
11
+
12
+ - **Default algorithms no longer include `normal_idw`** - The `normal_idw` algorithm is now opt-in only. Default algorithms are: `flood_fill`, `voxel_regions`, `normal_offset`. To use `normal_idw`, explicitly pass `algorithms=["normal_idw"]` or include it in your algorithm list.
13
+ - **Surface point count is now a direct count** - Replaced `surface_point_ratio` with `surface_point_count`. Instead of specifying a percentage, you now specify the exact number of surface points to include.
14
+ - CLI: `--surface-point-count 1000` (default: 1000)
15
+ - SDK: `sampler.generate(..., include_surface_points=True, surface_point_count=1000)`
16
+
8
17
  ## [0.3.0] - 2025-01-29
9
18
 
10
19
  ### Added
@@ -15,8 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
24
  - Output mode control: `--flood-fill-output`, `--voxel-regions-output` (boxes/samples/both)
16
25
  - **Surface point inclusion**
17
26
  - `--include-surface-points` flag to include original points with phi=0
18
- - `--surface-point-ratio` to control fraction included (default 10%)
19
- - SDK: `sampler.generate(..., include_surface_points=True, surface_point_ratio=0.1)`
27
+ - `--surface-point-count` to specify number of surface points (default 1000)
28
+ - SDK: `sampler.generate(..., include_surface_points=True, surface_point_count=1000)`
20
29
 
21
30
  ## [0.2.0] - 2025-01-29
22
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdf-sampler
3
- Version: 0.3.0
3
+ Version: 0.4.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.3.0"
3
+ version = "0.4.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" }
@@ -47,7 +47,7 @@ from sdf_sampler.models import (
47
47
  )
48
48
  from sdf_sampler.sampler import SDFSampler
49
49
 
50
- __version__ = "0.3.0"
50
+ __version__ = "0.4.0"
51
51
 
52
52
  __all__ = [
53
53
  # Main classes
@@ -14,6 +14,7 @@ from sdf_sampler.algorithms.voxel_regions import generate_voxel_region_constrain
14
14
  from sdf_sampler.config import AnalyzerConfig, AutoAnalysisOptions
15
15
  from sdf_sampler.models.analysis import (
16
16
  ALL_ALGORITHMS,
17
+ DEFAULT_ALGORITHMS,
17
18
  AlgorithmStats,
18
19
  AlgorithmType,
19
20
  AnalysisResult,
@@ -93,7 +94,7 @@ class SDFAnalyzer:
93
94
  raise ValueError(f"normals shape {normals.shape} doesn't match xyz {xyz.shape}")
94
95
 
95
96
  # Determine which algorithms to run
96
- algo_list = algorithms if algorithms else [a.value for a in ALL_ALGORITHMS]
97
+ algo_list = algorithms if algorithms else [a.value for a in DEFAULT_ALGORITHMS]
97
98
  algo_list = [a for a in algo_list if a in [alg.value for alg in ALL_ALGORITHMS]]
98
99
 
99
100
  # Run algorithms and collect constraints
@@ -169,10 +169,10 @@ def add_output_options(parser: argparse.ArgumentParser) -> None:
169
169
  help="Include original surface points (phi=0) in output",
170
170
  )
171
171
  group.add_argument(
172
- "--surface-point-ratio",
173
- type=float,
174
- default=0.1,
175
- help="Ratio of surface points to include (default: 0.1 = 10%%)",
172
+ "--surface-point-count",
173
+ type=int,
174
+ default=1000,
175
+ help="Number of surface points to include (default: 1000)",
176
176
  )
177
177
 
178
178
 
@@ -463,7 +463,7 @@ def cmd_sample(args: argparse.Namespace) -> int:
463
463
  # Include surface points if requested
464
464
  if args.include_surface_points:
465
465
  samples = _add_surface_points(
466
- samples, xyz, normals, args.surface_point_ratio, args.verbose
466
+ samples, xyz, normals, args.surface_point_count, args.verbose
467
467
  )
468
468
 
469
469
  if args.verbose:
@@ -541,7 +541,7 @@ def cmd_pipeline(args: argparse.Namespace) -> int:
541
541
  # Include surface points if requested
542
542
  if args.include_surface_points:
543
543
  samples = _add_surface_points(
544
- samples, xyz, normals, args.surface_point_ratio, args.verbose
544
+ samples, xyz, normals, args.surface_point_count, args.verbose
545
545
  )
546
546
 
547
547
  if args.verbose:
@@ -557,14 +557,14 @@ def _add_surface_points(
557
557
  samples: list,
558
558
  xyz: np.ndarray,
559
559
  normals: np.ndarray | None,
560
- ratio: float,
560
+ count: int,
561
561
  verbose: bool,
562
562
  ) -> list:
563
563
  """Add surface points to sample list."""
564
564
  from sdf_sampler.models import TrainingSample
565
565
 
566
- n_surface = int(len(xyz) * ratio)
567
- if n_surface == 0:
566
+ n_surface = min(count, len(xyz))
567
+ if n_surface <= 0:
568
568
  return samples
569
569
 
570
570
  # Subsample if needed
@@ -26,6 +26,13 @@ ALL_ALGORITHMS = [
26
26
  AlgorithmType.NORMAL_IDW,
27
27
  ]
28
28
 
29
+ # Default algorithms (excludes normal_idw which is opt-in)
30
+ DEFAULT_ALGORITHMS = [
31
+ AlgorithmType.FLOOD_FILL,
32
+ AlgorithmType.VOXEL_REGIONS,
33
+ AlgorithmType.NORMAL_OFFSET,
34
+ ]
35
+
29
36
 
30
37
  class GeneratedConstraint(BaseModel):
31
38
  """A constraint generated by auto-analysis.
@@ -66,7 +66,7 @@ class SDFSampler:
66
66
  strategy: str | SamplingStrategy = SamplingStrategy.INVERSE_SQUARE,
67
67
  seed: int | None = None,
68
68
  include_surface_points: bool = False,
69
- surface_point_ratio: float = 0.1,
69
+ surface_point_count: int | None = None,
70
70
  ) -> list[TrainingSample]:
71
71
  """Generate training samples from constraints.
72
72
 
@@ -78,7 +78,7 @@ class SDFSampler:
78
78
  strategy: Sampling strategy (CONSTANT, DENSITY, or INVERSE_SQUARE)
79
79
  seed: Random seed for reproducibility
80
80
  include_surface_points: If True, include original surface points with phi=0
81
- surface_point_ratio: Fraction of surface points to include (default 0.1 = 10%)
81
+ surface_point_count: Number of surface points to include (default: 1000, or len(xyz) if smaller)
82
82
 
83
83
  Returns:
84
84
  List of TrainingSample objects
@@ -160,8 +160,10 @@ class SDFSampler:
160
160
 
161
161
  # Add surface points if requested
162
162
  if include_surface_points:
163
+ # Default to 1000 surface points, or all points if smaller
164
+ count = surface_point_count if surface_point_count is not None else min(1000, len(xyz))
163
165
  samples.extend(
164
- self._generate_surface_points(xyz, normals, surface_point_ratio, rng)
166
+ self._generate_surface_points(xyz, normals, count, rng)
165
167
  )
166
168
 
167
169
  return samples
@@ -170,7 +172,7 @@ class SDFSampler:
170
172
  self,
171
173
  xyz: np.ndarray,
172
174
  normals: np.ndarray | None,
173
- ratio: float,
175
+ count: int,
174
176
  rng: np.random.Generator,
175
177
  ) -> list[TrainingSample]:
176
178
  """Generate surface point samples (phi=0) from the input point cloud.
@@ -178,14 +180,14 @@ class SDFSampler:
178
180
  Args:
179
181
  xyz: Point cloud positions (N, 3)
180
182
  normals: Optional point normals (N, 3)
181
- ratio: Fraction of points to include (0.0 to 1.0)
183
+ count: Number of surface points to include
182
184
  rng: Random number generator
183
185
 
184
186
  Returns:
185
187
  List of TrainingSample objects with phi=0
186
188
  """
187
- n_surface = int(len(xyz) * ratio)
188
- if n_surface == 0:
189
+ n_surface = min(count, len(xyz))
190
+ if n_surface <= 0:
189
191
  return []
190
192
 
191
193
  # Subsample if needed
@@ -759,3 +759,120 @@ class TestFullPipelineEquivalence:
759
759
  f"Sample count ratio too high: {ratio:.2f} "
760
760
  f"(standalone={standalone_sample_count}, backend={backend_sample_count})"
761
761
  )
762
+
763
+ @requires_backend
764
+ def test_inverse_square_pipeline_equivalence(self, trench_pointcloud):
765
+ """Test inverse_square sampling produces equivalent results.
766
+
767
+ This is the recommended production workflow: auto-analyze + inverse_square sampling.
768
+ """
769
+ xyz, normals = trench_pointcloud
770
+
771
+ # Shared analysis options
772
+ analysis_options = AutoAnalysisOptions(
773
+ flood_fill_output="samples",
774
+ flood_fill_sample_count=100,
775
+ voxel_regions_output="samples",
776
+ voxel_regions_sample_count=100,
777
+ idw_sample_count=100,
778
+ hull_filter_enabled=False,
779
+ )
780
+
781
+ # Run standalone with inverse_square
782
+ standalone_analyzer = SDFAnalyzer()
783
+ standalone_result = standalone_analyzer.analyze(
784
+ xyz=xyz,
785
+ normals=normals,
786
+ algorithms=["flood_fill", "voxel_regions", "normal_idw"],
787
+ options=analysis_options,
788
+ )
789
+
790
+ standalone_sampler = SDFSampler()
791
+ standalone_samples = standalone_sampler.generate(
792
+ xyz=xyz,
793
+ normals=normals,
794
+ constraints=standalone_result.constraints,
795
+ strategy="inverse_square",
796
+ total_samples=5000,
797
+ seed=42,
798
+ include_surface_points=False, # Test without surface points first
799
+ )
800
+
801
+ # Run backend with inverse_square
802
+ from sdf_labeler_api.config import Settings
803
+ from sdf_labeler_api.services.auto_analysis_service import AutoAnalysisService
804
+ from sdf_labeler_api.services.sampling_service import SamplingService
805
+ from sdf_labeler_api.services.project_service import ProjectService
806
+ from sdf_labeler_api.services.constraint_service import ConstraintService
807
+ from sdf_labeler_api.models.project import ProjectCreate
808
+ from sdf_labeler_api.models.samples import SampleGenerationRequest, SamplingStrategy
809
+
810
+ with tempfile.TemporaryDirectory() as tmpdir:
811
+ data_dir = Path(tmpdir)
812
+
813
+ import sdf_labeler_api.config as backend_config
814
+ original_settings = backend_config.settings
815
+ backend_config.settings = Settings(data_dir=data_dir)
816
+
817
+ try:
818
+ project_service = ProjectService(data_dir)
819
+ project = project_service.create(ProjectCreate(name="test"))
820
+ project_id = project.id
821
+
822
+ setup_backend_project(data_dir, project_id, xyz, normals)
823
+
824
+ # Analyze
825
+ backend_analysis = AutoAnalysisService(backend_config.settings)
826
+ backend_options = get_backend_options(analysis_options)
827
+ backend_result = asyncio.run(backend_analysis.analyze(
828
+ project_id=project_id,
829
+ algorithms=["flood_fill", "voxel_regions", "normal_idw"],
830
+ recompute=True,
831
+ options=backend_options,
832
+ ))
833
+
834
+ # Add constraints to project
835
+ constraint_service = ConstraintService()
836
+ for gc in backend_result.generated_constraints:
837
+ constraint_service.add_from_dict(project_id, gc.constraint)
838
+
839
+ # Sample with inverse_square
840
+ sampling_service = SamplingService()
841
+ request = SampleGenerationRequest(
842
+ total_samples=5000,
843
+ strategy=SamplingStrategy.INVERSE_SQUARE,
844
+ seed=42,
845
+ )
846
+ backend_sample_result = sampling_service.generate(project_id, request)
847
+ backend_samples = backend_sample_result.samples
848
+ finally:
849
+ backend_config.settings = original_settings
850
+
851
+ # Compare results
852
+ print(f"\nInverse square pipeline comparison:")
853
+ print(f" Standalone constraints: {len(standalone_result.constraints)}")
854
+ print(f" Backend constraints: {len(backend_result.generated_constraints)}")
855
+ print(f" Standalone samples: {len(standalone_samples)}")
856
+ print(f" Backend samples: {len(backend_samples)}")
857
+
858
+ # Verify phi distribution is similar (more samples near 0)
859
+ standalone_near_surface = sum(1 for s in standalone_samples if abs(s.phi) < 0.1)
860
+ backend_near_surface = sum(1 for s in backend_samples if abs(s.phi) < 0.1)
861
+
862
+ print(f" Standalone near-surface (|phi|<0.1): {standalone_near_surface}")
863
+ print(f" Backend near-surface (|phi|<0.1): {backend_near_surface}")
864
+
865
+ # Both should have majority of samples near surface (inverse_square characteristic)
866
+ standalone_ratio = standalone_near_surface / len(standalone_samples) if standalone_samples else 0
867
+ backend_ratio = backend_near_surface / len(backend_samples) if backend_samples else 0
868
+
869
+ assert standalone_ratio > 0.3, f"Standalone should have >30% near-surface, got {standalone_ratio:.1%}"
870
+ assert backend_ratio > 0.3, f"Backend should have >30% near-surface, got {backend_ratio:.1%}"
871
+
872
+ # Ratios should be similar
873
+ if standalone_ratio > 0 and backend_ratio > 0:
874
+ ratio_diff = abs(standalone_ratio - backend_ratio)
875
+ assert ratio_diff < 0.2, (
876
+ f"Near-surface ratio difference too high: {ratio_diff:.1%} "
877
+ f"(standalone={standalone_ratio:.1%}, backend={backend_ratio:.1%})"
878
+ )
@@ -855,7 +855,7 @@ wheels = [
855
855
 
856
856
  [[package]]
857
857
  name = "sdf-sampler"
858
- version = "0.2.0"
858
+ version = "0.3.0"
859
859
  source = { editable = "." }
860
860
  dependencies = [
861
861
  { name = "alphashape" },
File without changes
File without changes
File without changes