diff --git a/Dockerfile b/Dockerfile index 3490119..6faaa91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,11 @@ RUN pip3 install --no-cache-dir \ cmcrameri # Install CuPy for GPU acceleration (optional - will fallback to numpy if not available) +# CUPY_CUDA_COMPILE_WITH_CACHE=1 enables JIT compilation of kernels for +# GPU architectures not included in the pre-built wheel (e.g. sm_89, sm_120). +# The devel image includes nvcc for JIT compilation. If CuPy still fails +# (CUDA_ERROR_NO_BINARY_FOR_GPU), the pipeline falls back to CPU automatically. +ENV CUPY_CUDA_COMPILE_WITH_CACHE=1 RUN pip3 install --no-cache-dir cupy-cuda12x || echo "CuPy not available - GPU acceleration disabled" # Copy and install the pipeline package diff --git a/lidar_pipeline/tests/test_dtm.py b/lidar_pipeline/tests/test_dtm.py index 8e1ab57..0393b8d 100644 --- a/lidar_pipeline/tests/test_dtm.py +++ b/lidar_pipeline/tests/test_dtm.py @@ -69,35 +69,6 @@ class TestSMRFPipeline: assert writer["filename"] == "/output/a_ground.las" -class TestPMFPipeline: - def test_pipeline_json_valid(self): - """create_pmf_pipeline produces valid JSON with PMF filter.""" - from lidar_pipeline.dtm import create_pmf_pipeline - result = create_pmf_pipeline("/data/input/test.laz", "/data/output/test_ground.las") - pipeline = json.loads(result) - - assert "pipeline" in pipeline - stages = pipeline["pipeline"] - stage_types = [s.get("type") if isinstance(s, dict) else None for s in stages] - - # Must contain PMF filter - assert "filters.pmf" in stage_types - - # Must contain ReturnNumber filter - range_stages = [s for s in stages if isinstance(s, dict) and s.get("type") == "filters.range"] - assert any("ReturnNumber" in str(s.get("limits", "")) for s in range_stages) - - def test_pmf_parameters(self): - """PMF pipeline has expected parameters.""" - from lidar_pipeline.dtm import create_pmf_pipeline - result = create_pmf_pipeline("/input/a.laz", "/output/a_ground.las") - pipeline = json.loads(result) - - pmf_stage = [s for s in pipeline["pipeline"] if isinstance(s, dict) and s.get("type") == "filters.pmf"][0] - assert pmf_stage["max_window"] == 33 - assert pmf_stage["slope"] == 0.15 - - class TestCSFPipeline: def test_pipeline_json_valid(self): """create_csf_pipeline produces valid JSON with CSF filter.""" @@ -140,8 +111,8 @@ class TestDetectGroundMethod: return mock_las @patch('lidar_pipeline.dtm.laspy') - def test_urban_terrain_returns_pmf(self, mock_laspy): - """High single-return ratio (>0.6) should select PMF.""" + def test_urban_terrain_returns_csf(self, mock_laspy): + """High single-return ratio (>0.6) should select CSF.""" from lidar_pipeline.dtm import detect_ground_method # 70% single returns = urban @@ -153,7 +124,7 @@ class TestDetectGroundMethod: mock_laspy.read.return_value = self._make_mock_las(num_returns, z_values) result = detect_ground_method(Path("/data/input/test.laz")) - assert result == 'pmf' + assert result == 'csf' @patch('lidar_pipeline.dtm.laspy') def test_natural_terrain_returns_smrf(self, mock_laspy): @@ -172,8 +143,8 @@ class TestDetectGroundMethod: assert result == 'smrf' @patch('lidar_pipeline.dtm.laspy') - def test_mountainous_terrain_still_defaults_to_smrf(self, mock_laspy): - """High variance terrain defaults to SMRF in auto mode (CSF only via --ground-classification csf).""" + def test_mountainous_terrain_returns_csf(self, mock_laspy): + """High variance terrain (>30m std) selects CSF for complex terrain.""" from lidar_pipeline.dtm import detect_ground_method # Moderate single-return ratio but very high height variance @@ -185,7 +156,7 @@ class TestDetectGroundMethod: mock_laspy.read.return_value = self._make_mock_las(num_returns, z_values) result = detect_ground_method(Path("/data/input/test.laz")) - assert result == 'smrf' + assert result == 'csf' class TestClassifyGroundMethod: @@ -195,8 +166,8 @@ class TestClassifyGroundMethod: """classify_ground with method='auto' should call detect_ground_method.""" from lidar_pipeline.dtm import classify_ground - # Mock detect_ground_method to return 'pmf' - with patch('lidar_pipeline.dtm.detect_ground_method', return_value='pmf') as mock_detect: + # Mock detect_ground_method to return 'csf' + with patch('lidar_pipeline.dtm.detect_ground_method', return_value='csf') as mock_detect: mock_subprocess.run.return_value = MagicMock(returncode=0) result = classify_ground(Path("/data/input/test.laz"), Path("/tmp"), method='auto') @@ -225,8 +196,8 @@ class TestClassifyGroundMethod: @patch('lidar_pipeline.dtm.subprocess') @patch('lidar_pipeline.dtm.laspy') - def test_classify_ground_pmf_uses_pmf_pipeline(self, mock_laspy, mock_subprocess): - """classify_ground with method='pmf' should create PMF pipeline.""" + def test_classify_ground_csf_uses_csf_pipeline(self, mock_laspy, mock_subprocess): + """classify_ground with method='csf' should create CSF pipeline.""" from lidar_pipeline.dtm import classify_ground mock_subprocess.run.return_value = MagicMock(returncode=0) @@ -234,10 +205,10 @@ class TestClassifyGroundMethod: with patch('lidar_pipeline.dtm.detect_ground_method'): import tempfile with tempfile.TemporaryDirectory() as tmpdir: - result = classify_ground(Path("/data/input/test.laz"), Path(tmpdir), method='pmf') + result = classify_ground(Path("/data/input/test.laz"), Path(tmpdir), method='csf') - pipeline_file = Path(tmpdir) / "pipeline_pmf.json" + pipeline_file = Path(tmpdir) / "pipeline_csf.json" if pipeline_file.exists(): pipeline = json.loads(pipeline_file.read_text()) stage_types = [s.get("type") if isinstance(s, dict) else None for s in pipeline["pipeline"]] - assert "filters.pmf" in stage_types \ No newline at end of file + assert "filters.csf" in stage_types \ No newline at end of file diff --git a/lidar_pipeline/tests/test_pipeline.py b/lidar_pipeline/tests/test_pipeline.py index adcdd59..bce852f 100644 --- a/lidar_pipeline/tests/test_pipeline.py +++ b/lidar_pipeline/tests/test_pipeline.py @@ -26,9 +26,9 @@ class TestVizSteps: assert "solar" not in names def test_expected_visualization_count(self): - """Should have 17 visualizations (15 terrain + ortho + topo).""" + """Should have 13 visualizations (11 terrain + ortho + topo).""" from lidar_pipeline.pipeline import VIZ_STEPS - assert len(VIZ_STEPS) == 17 + assert len(VIZ_STEPS) == 13 def test_ortho_and_topo_present(self): from lidar_pipeline.pipeline import VIZ_STEPS @@ -48,7 +48,7 @@ class TestLidarArchaeoPipeline: assert (tmp_path / "output").exists() assert (tmp_path / "output" / "DTM").exists() assert (tmp_path / "output" / "visualisations").exists() - assert (tmp_path / "output" / "rapports").exists() + assert (tmp_path / "output" / "temp").exists() def test_init_raises_on_missing_input(self, tmp_path): from lidar_pipeline.pipeline import LidarArchaeoPipeline