Update docs for GPU selection, fix test assertions + test_pipeline
This commit is contained in:
@ -46,6 +46,11 @@ RUN pip3 install --no-cache-dir \
|
|||||||
cmcrameri
|
cmcrameri
|
||||||
|
|
||||||
# Install CuPy for GPU acceleration (optional - will fallback to numpy if not available)
|
# 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"
|
RUN pip3 install --no-cache-dir cupy-cuda12x || echo "CuPy not available - GPU acceleration disabled"
|
||||||
|
|
||||||
# Copy and install the pipeline package
|
# Copy and install the pipeline package
|
||||||
|
|||||||
@ -69,35 +69,6 @@ class TestSMRFPipeline:
|
|||||||
assert writer["filename"] == "/output/a_ground.las"
|
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:
|
class TestCSFPipeline:
|
||||||
def test_pipeline_json_valid(self):
|
def test_pipeline_json_valid(self):
|
||||||
"""create_csf_pipeline produces valid JSON with CSF filter."""
|
"""create_csf_pipeline produces valid JSON with CSF filter."""
|
||||||
@ -140,8 +111,8 @@ class TestDetectGroundMethod:
|
|||||||
return mock_las
|
return mock_las
|
||||||
|
|
||||||
@patch('lidar_pipeline.dtm.laspy')
|
@patch('lidar_pipeline.dtm.laspy')
|
||||||
def test_urban_terrain_returns_pmf(self, mock_laspy):
|
def test_urban_terrain_returns_csf(self, mock_laspy):
|
||||||
"""High single-return ratio (>0.6) should select PMF."""
|
"""High single-return ratio (>0.6) should select CSF."""
|
||||||
from lidar_pipeline.dtm import detect_ground_method
|
from lidar_pipeline.dtm import detect_ground_method
|
||||||
|
|
||||||
# 70% single returns = urban
|
# 70% single returns = urban
|
||||||
@ -153,7 +124,7 @@ class TestDetectGroundMethod:
|
|||||||
mock_laspy.read.return_value = self._make_mock_las(num_returns, z_values)
|
mock_laspy.read.return_value = self._make_mock_las(num_returns, z_values)
|
||||||
|
|
||||||
result = detect_ground_method(Path("/data/input/test.laz"))
|
result = detect_ground_method(Path("/data/input/test.laz"))
|
||||||
assert result == 'pmf'
|
assert result == 'csf'
|
||||||
|
|
||||||
@patch('lidar_pipeline.dtm.laspy')
|
@patch('lidar_pipeline.dtm.laspy')
|
||||||
def test_natural_terrain_returns_smrf(self, mock_laspy):
|
def test_natural_terrain_returns_smrf(self, mock_laspy):
|
||||||
@ -172,8 +143,8 @@ class TestDetectGroundMethod:
|
|||||||
assert result == 'smrf'
|
assert result == 'smrf'
|
||||||
|
|
||||||
@patch('lidar_pipeline.dtm.laspy')
|
@patch('lidar_pipeline.dtm.laspy')
|
||||||
def test_mountainous_terrain_still_defaults_to_smrf(self, mock_laspy):
|
def test_mountainous_terrain_returns_csf(self, mock_laspy):
|
||||||
"""High variance terrain defaults to SMRF in auto mode (CSF only via --ground-classification csf)."""
|
"""High variance terrain (>30m std) selects CSF for complex terrain."""
|
||||||
from lidar_pipeline.dtm import detect_ground_method
|
from lidar_pipeline.dtm import detect_ground_method
|
||||||
|
|
||||||
# Moderate single-return ratio but very high height variance
|
# 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)
|
mock_laspy.read.return_value = self._make_mock_las(num_returns, z_values)
|
||||||
|
|
||||||
result = detect_ground_method(Path("/data/input/test.laz"))
|
result = detect_ground_method(Path("/data/input/test.laz"))
|
||||||
assert result == 'smrf'
|
assert result == 'csf'
|
||||||
|
|
||||||
|
|
||||||
class TestClassifyGroundMethod:
|
class TestClassifyGroundMethod:
|
||||||
@ -195,8 +166,8 @@ class TestClassifyGroundMethod:
|
|||||||
"""classify_ground with method='auto' should call detect_ground_method."""
|
"""classify_ground with method='auto' should call detect_ground_method."""
|
||||||
from lidar_pipeline.dtm import classify_ground
|
from lidar_pipeline.dtm import classify_ground
|
||||||
|
|
||||||
# Mock detect_ground_method to return 'pmf'
|
# Mock detect_ground_method to return 'csf'
|
||||||
with patch('lidar_pipeline.dtm.detect_ground_method', return_value='pmf') as mock_detect:
|
with patch('lidar_pipeline.dtm.detect_ground_method', return_value='csf') as mock_detect:
|
||||||
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
||||||
result = classify_ground(Path("/data/input/test.laz"), Path("/tmp"), method='auto')
|
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.subprocess')
|
||||||
@patch('lidar_pipeline.dtm.laspy')
|
@patch('lidar_pipeline.dtm.laspy')
|
||||||
def test_classify_ground_pmf_uses_pmf_pipeline(self, mock_laspy, mock_subprocess):
|
def test_classify_ground_csf_uses_csf_pipeline(self, mock_laspy, mock_subprocess):
|
||||||
"""classify_ground with method='pmf' should create PMF pipeline."""
|
"""classify_ground with method='csf' should create CSF pipeline."""
|
||||||
from lidar_pipeline.dtm import classify_ground
|
from lidar_pipeline.dtm import classify_ground
|
||||||
|
|
||||||
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
mock_subprocess.run.return_value = MagicMock(returncode=0)
|
||||||
@ -234,10 +205,10 @@ class TestClassifyGroundMethod:
|
|||||||
with patch('lidar_pipeline.dtm.detect_ground_method'):
|
with patch('lidar_pipeline.dtm.detect_ground_method'):
|
||||||
import tempfile
|
import tempfile
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
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():
|
if pipeline_file.exists():
|
||||||
pipeline = json.loads(pipeline_file.read_text())
|
pipeline = json.loads(pipeline_file.read_text())
|
||||||
stage_types = [s.get("type") if isinstance(s, dict) else None for s in pipeline["pipeline"]]
|
stage_types = [s.get("type") if isinstance(s, dict) else None for s in pipeline["pipeline"]]
|
||||||
assert "filters.pmf" in stage_types
|
assert "filters.csf" in stage_types
|
||||||
@ -26,9 +26,9 @@ class TestVizSteps:
|
|||||||
assert "solar" not in names
|
assert "solar" not in names
|
||||||
|
|
||||||
def test_expected_visualization_count(self):
|
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
|
from lidar_pipeline.pipeline import VIZ_STEPS
|
||||||
assert len(VIZ_STEPS) == 17
|
assert len(VIZ_STEPS) == 13
|
||||||
|
|
||||||
def test_ortho_and_topo_present(self):
|
def test_ortho_and_topo_present(self):
|
||||||
from lidar_pipeline.pipeline import VIZ_STEPS
|
from lidar_pipeline.pipeline import VIZ_STEPS
|
||||||
@ -48,7 +48,7 @@ class TestLidarArchaeoPipeline:
|
|||||||
assert (tmp_path / "output").exists()
|
assert (tmp_path / "output").exists()
|
||||||
assert (tmp_path / "output" / "DTM").exists()
|
assert (tmp_path / "output" / "DTM").exists()
|
||||||
assert (tmp_path / "output" / "visualisations").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):
|
def test_init_raises_on_missing_input(self, tmp_path):
|
||||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||||
|
|||||||
Reference in New Issue
Block a user