"""Tests for visualization functions. Each test creates a small synthetic DEM and runs a visualization function, checking that it produces a valid output file. """ import numpy as np import pytest from pathlib import Path # --- Core terrain visualizations (no GPU required) --- class TestHillshade: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_hillshade result = generate_hillshade(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() assert result.suffix == ".tif" def test_output_values_valid(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_hillshade result = generate_hillshade(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) assert data.shape[0] > 0 assert np.nanmin(data) >= 0 assert np.nanmax(data) <= 1 class TestSlope: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_slope result = generate_slope(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_slope_values_degrees(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_slope result = generate_slope(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) assert np.nanmin(data) >= 0 assert np.nanmax(data) <= 90 class TestAspect: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_aspect result = generate_aspect(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_aspect_values_0_360(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_aspect result = generate_aspect(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) valid = data[~np.isnan(data)] assert np.nanmin(valid) >= 0 assert np.nanmax(valid) <= 360 class TestCurvature: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_curvature result = generate_curvature(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() # --- GPU-accelerated visualizations --- class TestLRM: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_lrm result = generate_lrm(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_lrm_has_positive_negative(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_lrm result = generate_lrm(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) # LRM should have both positive and negative values assert np.nanmax(data) > 0 assert np.nanmin(data) < 0 class TestSVF: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_svf result = generate_svf(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_svf_values_0_1(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_svf result = generate_svf(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) valid = data[~np.isnan(data)] assert np.nanmin(valid) >= 0 assert np.nanmax(valid) <= 1 class TestOpenness: def test_positive_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_openness result = generate_openness(synthetic_dem, "test", tmp_output_dir, 5.0, positive=True) assert result is not None assert result.exists() def test_negative_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_openness result = generate_openness(synthetic_dem, "test", tmp_output_dir, 5.0, positive=False) assert result is not None assert result.exists() class TestMSLRM: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_mslrm result = generate_mslrm(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() class TestTPI: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_tpi result = generate_tpi(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() class TestSAILORE: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_sailore result = generate_sailore(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() class TestRoughness: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_roughness result = generate_roughness(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_roughness_non_negative(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_roughness result = generate_roughness(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) # Standard deviation is always >= 0 assert np.nanmin(data) >= 0 class TestAnomalies: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_anomalies result = generate_anomalies(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() class TestWavelet: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_wavelet result = generate_wavelet(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() class TestFlow: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_flow result = generate_flow(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() def test_flow_log_values(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_flow result = generate_flow(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) # log1p(x) >= 0 for x >= 0 valid = data[~np.isnan(data)] assert np.nanmin(valid) >= 0 class TestRRIM: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_rrim result = generate_rrim(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() assert result.suffix == ".tif" def test_rrim_is_rgb_3band(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_rrim result = generate_rrim(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: assert src.count == 3, f"Expected 3 bands, got {src.count}" assert src.dtypes[0] == 'uint8' def test_rrim_values_0_255(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_rrim result = generate_rrim(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: for band in range(1, 4): data = src.read(band) assert data.min() >= 0 assert data.max() <= 255 def test_rrim_no_nan(self, synthetic_dem, tmp_output_dir): """RRIM is uint8 RGB — NaN zones are set to 0 (black).""" import rasterio from lidar_pipeline.visualizations import generate_rrim result = generate_rrim(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: # uint8 bands should not have NaN for band in range(1, 4): data = src.read(band) assert not np.isnan(data).any(), f"Band {band} has NaN values" class TestMultiHillshade: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_multi_hillshade result = generate_multi_hillshade(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() assert result.suffix == ".tif" def test_multi_hillshade_is_rgb_3band(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_multi_hillshade result = generate_multi_hillshade(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: assert src.count == 3, f"Expected 3 bands, got {src.count}" assert src.dtypes[0] == 'uint8' def test_multi_hillshade_values_0_255(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_multi_hillshade result = generate_multi_hillshade(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: for band in range(1, 4): data = src.read(band) assert data.min() >= 0 assert data.max() <= 255 class TestLocalDominance: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_local_dominance result = generate_local_dominance(synthetic_dem, "test", tmp_output_dir, 5.0) assert result is not None assert result.exists() assert result.suffix == ".tif" def test_dominance_values_0_1(self, synthetic_dem, tmp_output_dir): import rasterio from lidar_pipeline.visualizations import generate_local_dominance result = generate_local_dominance(synthetic_dem, "test", tmp_output_dir, 5.0) with rasterio.open(result) as src: data = src.read(1) valid = data[~np.isnan(data)] assert np.nanmin(valid) >= 0, "Local dominance should be >= 0" assert np.nanmax(valid) <= 1, "Local dominance should be <= 1" def test_dominance_nan_mask_preserved(self, synthetic_dem, tmp_output_dir): """Check that NaN zones from original DEM are preserved.""" import rasterio from lidar_pipeline.visualizations import generate_local_dominance result = generate_local_dominance(synthetic_dem, "test", tmp_output_dir, 5.0) # The synthetic DEM has no NaN, so this just verifies the output is valid with rasterio.open(result) as src: data = src.read(1) # Shape should match input assert data.shape[0] > 0 assert data.shape[1] > 0