Files
lidar_rendu/lidar_pipeline/tests/test_quality.py
Antoine fb892ea9f2 Translate the whole project to English and fix outdated comments and help
Comments, docstrings, logs, CLI help, map UI, legends, PDF sheet, scripts,
compose files and AGENTS.md are now English. Data keys stay unchanged
(relief_oriente, densite_sol, visualisations/, API JSON keys, link params).
Wrong comments and help defaults found along the way are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:16:45 +02:00

178 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests for tile quality measurement (ground density, acquisition date)."""
BOUNDS = (652000.0, 6861000.0, 653000.0, 6862000.0)
def _gps_seconds(year, month, day):
"""Standard adjusted GPS time (GPS seconds − 1e9) of a UTC noon."""
from datetime import datetime, timezone
epoch = datetime(1980, 1, 6, tzinfo=timezone.utc)
return (datetime(year, month, day, 12, tzinfo=timezone.utc) - epoch).total_seconds() - 1e9
def test_gps_adjusted_to_date():
from lidar_pipeline.quality import gps_adjusted_to_date
assert gps_adjusted_to_date(_gps_seconds(2023, 3, 15)) == "2023-03-15"
def test_compute_quality_density_grid_and_dates():
import numpy as np
from lidar_pipeline.quality import compute_quality
# 1 point per m² on the northern half, nothing in the south; 2 flight dates.
xs, ys = np.meshgrid(np.arange(652000.5, 653000.0, 1.0),
np.arange(6861500.5, 6862000.0, 1.0))
x, y = xs.ravel(), ys.ravel()
t = np.where(x < 652500, _gps_seconds(2023, 3, 15), _gps_seconds(2023, 3, 17))
q = compute_quality(x, y, t, BOUNDS)
assert q["version"] == 1
assert abs(q["ground_density"] - 0.5) < 1e-6
grid = q["density_grid"]
assert len(grid) == 20 and all(len(r) == 20 for r in grid)
assert grid[0][0] == 1.0 # row 0 = north: covered
assert grid[19][0] == 0.0 # south: empty
assert q["acq_start"] == "2023-03-15" and q["acq_end"] == "2023-03-17"
assert q["acq_source"] == "gps"
# 1 point per m²: 1 in 25 0.2 m pixels occupied in the north, none in the south.
assert abs(q["empty_fraction"] - (1 - 0.5 / 25)) < 1e-3
def test_compute_quality_ignores_points_outside_bounds():
import numpy as np
from lidar_pipeline.quality import compute_quality
x = np.array([652500.0, 660000.0]); y = np.array([6861500.0, 6861500.0])
q = compute_quality(x, y, None, BOUNDS, header_date="2024-05-03")
assert abs(q["ground_density"] - 1e-6) < 1e-9
assert q["acq_start"] == q["acq_end"] == "2024-05-03"
assert q["acq_source"] == "header"
def test_compute_quality_week_time_falls_back_to_header():
import numpy as np
from lidar_pipeline.quality import compute_quality
x = np.array([652500.0]); y = np.array([6861500.0])
q = compute_quality(x, y, np.array([345600.0]), BOUNDS, gps_adjusted=False,
header_date="2024-05-03")
assert q["acq_source"] == "header" and q["acq_start"] == "2024-05-03"
def test_compute_quality_empty_tile():
import numpy as np
from lidar_pipeline.quality import compute_quality
q = compute_quality(np.array([]), np.array([]), np.array([]), BOUNDS)
assert q["ground_density"] == 0.0 and q["empty_fraction"] == 1.0
assert q["acq_start"] is None and q["acq_end"] is None and q["acq_source"] is None
def test_sidecar_roundtrip_and_table(tmp_path):
from lidar_pipeline.quality import (load_quality_table, quality_path,
read_quality, write_quality)
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
data = {"version": 1, "ground_density": 4.2}
assert write_quality(tmp_path, base, data) is True
assert write_quality(tmp_path, base, data) is False # identical content: not rewritten
assert quality_path(tmp_path, base).is_file()
assert read_quality(tmp_path, base) == data
assert load_quality_table(tmp_path) == {base: data}
def test_read_quality_rejects_other_version(tmp_path):
from lidar_pipeline.quality import read_quality, write_quality
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
write_quality(tmp_path, base, {"version": 0})
assert read_quality(tmp_path, base) is None
def test_quality_module_imports_without_numpy():
"""The lightweight image has no numpy: the module must not import it at load time."""
import subprocess, sys
code = ("import sys; sys.modules['numpy'] = None; "
"import lidar_pipeline.quality as q; print(q.QUALITY_VERSION)")
r = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True)
assert r.returncode == 0, r.stderr
def _write_las(path, x, y, cls, t, adjusted=True, creation=None):
"""Minimal LAS (format 6: gps_time) for the tests."""
import laspy
import numpy as np
header = laspy.LasHeader(point_format=6, version="1.4")
header.scales = [0.01, 0.01, 0.01]
header.offsets = [652000.0, 6861000.0, 0.0]
if adjusted:
header.global_encoding.gps_time_type = laspy.header.GpsTimeType.STANDARD
if creation is not None:
header.creation_date = creation
las = laspy.LasData(header)
las.x = np.asarray(x, float); las.y = np.asarray(y, float)
las.z = np.zeros(len(x))
las.classification = np.asarray(cls, np.uint8)
las.return_number = np.ones(len(x), np.uint8)
las.number_of_returns = np.ones(len(x), np.uint8)
las.gps_time = np.asarray(t, float)
las.write(str(path))
def test_quality_from_las_filters_classes(tmp_path):
from lidar_pipeline.quality import quality_from_las
p = tmp_path / "LHD_FXX_0652_6862_PTS_LAMB93_IGN69.laz"
t = _gps_seconds(2022, 6, 1)
_write_las(p, [652100, 652200, 652300], [6861100, 6861200, 6861300],
[2, 2, 5], [t, t, t])
q = quality_from_las(p, BOUNDS, codes=(2,))
assert abs(q["ground_density"] - 2e-6) < 1e-9
assert q["acq_start"] == "2022-06-01" and q["acq_source"] == "gps"
def test_quality_from_las_week_time_uses_header_date(tmp_path):
from datetime import date
from lidar_pipeline.quality import quality_from_las
p = tmp_path / "x.las"
_write_las(p, [652100], [6861100], [2], [1000.0], adjusted=False,
creation=date(2021, 9, 2))
q = quality_from_las(p, BOUNDS)
assert q["acq_source"] == "header" and q["acq_start"] == "2021-09-02"
def test_quality_from_las_unreadable_returns_none(tmp_path):
from lidar_pipeline.quality import quality_from_las
p = tmp_path / "bad.laz"; p.write_bytes(b"not a LAS")
assert quality_from_las(p, BOUNDS) is None
def test_ensure_quality_skips_existing_and_unparsable(tmp_path):
from lidar_pipeline.quality import ensure_quality, read_quality, write_quality
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
p = tmp_path / f"{base}_ground.las"
_write_las(p, [652100], [6861100], [2], [_gps_seconds(2022, 6, 1)])
assert ensure_quality(p, base, tmp_path) is True
assert read_quality(tmp_path, base)["acq_start"] == "2022-06-01"
write_quality(tmp_path, base, dict(read_quality(tmp_path, base), ground_density=99.0))
assert ensure_quality(p, base, tmp_path) is True
assert read_quality(tmp_path, base)["ground_density"] == 99.0 # not recomputed
assert ensure_quality(p, "nom_libre", tmp_path) is False
def test_backfill_quality_reads_input_laz(tmp_path):
from lidar_pipeline.quality import backfill_quality, read_quality
inp = tmp_path / "input"; inp.mkdir()
out = tmp_path / "output"
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
_write_las(inp / f"{base}.copc.laz", [652100, 652200], [6861100, 6861200],
[2, 6], [_gps_seconds(2022, 6, 1)] * 2)
assert backfill_quality(inp, out) == 1
assert abs(read_quality(out, base)["ground_density"] - 1e-6) < 1e-9
assert backfill_quality(inp, out) == 0 # already up to date
def test_cli_quality_backfill(tmp_path):
import subprocess, sys
inp = tmp_path / "input"; inp.mkdir()
out = tmp_path / "output"
base = "LHD_FXX_0652_6862_PTS_LAMB93_IGN69"
_write_las(inp / f"{base}.laz", [652100], [6861100], [2], [_gps_seconds(2022, 6, 1)])
r = subprocess.run([sys.executable, "-m", "lidar_pipeline", str(inp), "-o", str(out),
"--quality-backfill"], capture_output=True, text=True, timeout=180)
assert r.returncode == 0, r.stderr
assert (out / "quality" / f"{base}.json").is_file()
assert not (out / "DTM").exists()