KSvend Claude Happy commited on
Commit ·
cb89291
1
Parent(s): c6c1ec2
fix: NDVI harvest baseline_path never assigned after download
Browse filesGenerated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
- app/indicators/ndvi.py +1 -0
- tests/test_indicator_ndvi.py +64 -0
app/indicators/ndvi.py
CHANGED
|
@@ -109,6 +109,7 @@ class NdviIndicator(BaseIndicator):
|
|
| 109 |
baseline_dir = os.path.join(results_dir, "baseline")
|
| 110 |
os.makedirs(baseline_dir, exist_ok=True)
|
| 111 |
paths = baseline_job.download_results(baseline_dir)
|
|
|
|
| 112 |
except Exception as exc:
|
| 113 |
logger.warning("NDVI baseline batch download failed, degrading: %s", exc)
|
| 114 |
|
|
|
|
| 109 |
baseline_dir = os.path.join(results_dir, "baseline")
|
| 110 |
os.makedirs(baseline_dir, exist_ok=True)
|
| 111 |
paths = baseline_job.download_results(baseline_dir)
|
| 112 |
+
baseline_path = self._find_tif(paths, baseline_dir)
|
| 113 |
except Exception as exc:
|
| 114 |
logger.warning("NDVI baseline batch download failed, degrading: %s", exc)
|
| 115 |
|
tests/test_indicator_ndvi.py
CHANGED
|
@@ -263,3 +263,67 @@ async def test_ndvi_harvest_falls_back_when_current_fails(test_aoi, test_time_ra
|
|
| 263 |
)
|
| 264 |
|
| 265 |
assert result.data_source == "placeholder"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
)
|
| 264 |
|
| 265 |
assert result.data_source == "placeholder"
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
@pytest.mark.asyncio
|
| 269 |
+
async def test_ndvi_harvest_uses_baseline_when_available(test_aoi, test_time_range):
|
| 270 |
+
"""harvest() computes change vs baseline when baseline download succeeds."""
|
| 271 |
+
from app.indicators.ndvi import NdviIndicator
|
| 272 |
+
|
| 273 |
+
indicator = NdviIndicator()
|
| 274 |
+
|
| 275 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 276 |
+
# Current: higher NDVI
|
| 277 |
+
current_path = os.path.join(tmpdir, "current.tif")
|
| 278 |
+
_mock_ndvi_tif(current_path, n_months=12)
|
| 279 |
+
|
| 280 |
+
# Baseline: lower NDVI (shift down by 0.1)
|
| 281 |
+
baseline_path = os.path.join(tmpdir, "baseline.tif")
|
| 282 |
+
import rasterio
|
| 283 |
+
from rasterio.transform import from_bounds
|
| 284 |
+
rng = np.random.default_rng(99)
|
| 285 |
+
data = np.zeros((12, 10, 10), dtype=np.float32)
|
| 286 |
+
for m in range(12):
|
| 287 |
+
data[m] = 0.2 + 0.1 * np.sin(np.pi * (m - 3) / 6) + rng.normal(0, 0.02, (10, 10))
|
| 288 |
+
with rasterio.open(
|
| 289 |
+
baseline_path, "w", driver="GTiff", height=10, width=10, count=12,
|
| 290 |
+
dtype="float32", crs="EPSG:4326",
|
| 291 |
+
transform=from_bounds(32.45, 15.65, 32.65, 15.8, 10, 10),
|
| 292 |
+
nodata=-9999.0,
|
| 293 |
+
) as dst:
|
| 294 |
+
for i in range(12):
|
| 295 |
+
dst.write(data[i], i + 1)
|
| 296 |
+
|
| 297 |
+
rgb_path = os.path.join(tmpdir, "rgb.tif")
|
| 298 |
+
_mock_true_color_tif(rgb_path)
|
| 299 |
+
|
| 300 |
+
def make_mock_job(src_path):
|
| 301 |
+
job = MagicMock()
|
| 302 |
+
job.job_id = "j-test"
|
| 303 |
+
def fake_download_results(target):
|
| 304 |
+
import shutil
|
| 305 |
+
os.makedirs(target, exist_ok=True)
|
| 306 |
+
dest = os.path.join(target, "result.tif")
|
| 307 |
+
shutil.copy(src_path, dest)
|
| 308 |
+
from pathlib import Path
|
| 309 |
+
return {Path(dest): {"type": "image/tiff"}}
|
| 310 |
+
job.download_results.side_effect = fake_download_results
|
| 311 |
+
job.status.return_value = "finished"
|
| 312 |
+
return job
|
| 313 |
+
|
| 314 |
+
current_job = make_mock_job(current_path)
|
| 315 |
+
baseline_job = make_mock_job(baseline_path)
|
| 316 |
+
true_color_job = make_mock_job(rgb_path)
|
| 317 |
+
|
| 318 |
+
result = await indicator.harvest(
|
| 319 |
+
test_aoi, test_time_range,
|
| 320 |
+
batch_jobs=[current_job, baseline_job, true_color_job],
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
assert result.data_source == "satellite"
|
| 324 |
+
# With distinct current vs baseline, confidence should NOT be LOW
|
| 325 |
+
# (it should be HIGH with 12 valid months)
|
| 326 |
+
assert result.confidence == ConfidenceLevel.HIGH
|
| 327 |
+
# Baseline band data should be present in chart
|
| 328 |
+
assert "baseline_mean" in result.chart_data
|
| 329 |
+
assert len(result.chart_data["baseline_mean"]) > 0
|