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>
This commit is contained in:
@ -20,13 +20,13 @@ class TestVizSteps:
|
||||
assert len(names) == len(set(names)), "VIZ_STEPS has duplicate names"
|
||||
|
||||
def test_expected_visualization_count(self):
|
||||
"""17 visualisations : 14 terrain + densité de points + ortho + topo."""
|
||||
"""17 visualizations: 14 terrain products + point density + ortho + topo."""
|
||||
from lidar_pipeline.pipeline import VIZ_STEPS
|
||||
assert len(VIZ_STEPS) == 17
|
||||
|
||||
def test_default_run_produces_only_panel_layers(self, tmp_path):
|
||||
"""Sans --only : seules les couches affichées (relief orienté, densité
|
||||
de points) sont produites ; --only reste libre pour les autres."""
|
||||
"""Without --only: only the displayed layers (oriented relief, point
|
||||
density) are produced; --only still allows the others."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
p = LidarArchaeoPipeline(tmp_path, tmp_path / "out")
|
||||
assert [n for n, _ in p.viz_steps] == ["relief_oriente", "densite_sol"]
|
||||
@ -34,24 +34,24 @@ class TestVizSteps:
|
||||
assert [n for n, _ in p.viz_steps] == ["slope"]
|
||||
|
||||
def test_incremental_index_on_by_default(self, tmp_path):
|
||||
"""La carte suit le rendu en cours quel que soit le lanceur."""
|
||||
"""The map follows the ongoing render whatever the launcher."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
assert LidarArchaeoPipeline(tmp_path, tmp_path / "o").incremental_index
|
||||
assert not LidarArchaeoPipeline(tmp_path, tmp_path / "o2", no_index=True).incremental_index
|
||||
|
||||
def test_debounced_tile_is_indexed_later(self, tmp_path, monkeypatch):
|
||||
"""Une dalle terminée pendant l'anti-rebond est reprise par une passe
|
||||
différée, sans attendre la dalle suivante."""
|
||||
"""A tile finished during the debounce interval is picked up by a
|
||||
deferred pass, without waiting for the next tile."""
|
||||
import time
|
||||
import lidar_pipeline.index as index
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
calls = []
|
||||
monkeypatch.setattr(index, "build_index", lambda *a, **k: calls.append(time.time()))
|
||||
p = LidarArchaeoPipeline(tmp_path, tmp_path / "o")
|
||||
p._rebuild_index_incremental() # passe immédiate
|
||||
p._last_index_rebuild = time.time() - 2.8 # anti-rebond presque écoulé
|
||||
p._rebuild_index_incremental() # différée (~0,2 s)
|
||||
p._rebuild_index_incremental() # déjà programmée : pas de doublon
|
||||
p._rebuild_index_incremental() # immediate pass
|
||||
p._last_index_rebuild = time.time() - 2.8 # debounce almost elapsed
|
||||
p._rebuild_index_incremental() # deferred (~0.2 s)
|
||||
p._rebuild_index_incremental() # already scheduled: no duplicate
|
||||
time.sleep(0.6)
|
||||
assert len(calls) == 2
|
||||
|
||||
@ -63,17 +63,17 @@ class TestVizSteps:
|
||||
|
||||
|
||||
class TestFetchEdgeNeighbors:
|
||||
"""Raccord des bords : pré-téléchargement des voisines manquantes."""
|
||||
"""Edge stitching: pre-download of missing neighbors."""
|
||||
|
||||
def test_downloads_missing_ring_dedup(self, tmp_path, monkeypatch):
|
||||
"""Les 8 voisines manquantes sont demandées une seule fois, présente exclue."""
|
||||
"""Missing neighbors are requested once each, the present one excluded."""
|
||||
import lidar_pipeline.fetch_ign as fetch_ign
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
input_dir = tmp_path / "input"
|
||||
input_dir.mkdir()
|
||||
lazh = input_dir / "LHD_FXX_0999_6778_PTS_LAMB93_IGN69.copc.laz"
|
||||
lazh.touch()
|
||||
# Une voisine déjà présente ne doit pas être retéléchargée.
|
||||
# A neighbor already present must not be downloaded again.
|
||||
(input_dir / "LHD_FXX_1000_6779_PTS_LAMB93_IGN69.copc.laz").touch()
|
||||
calls = []
|
||||
|
||||
@ -85,12 +85,12 @@ class TestFetchEdgeNeighbors:
|
||||
pipeline = LidarArchaeoPipeline(input_dir, tmp_path / "output",
|
||||
edge_buffer=100.0)
|
||||
pipeline._fetch_edge_neighbors([lazh])
|
||||
assert len(calls) == 7 # 8 voisines - 1 déjà présente
|
||||
assert len(calls) == 7 # 8 neighbors - 1 already present
|
||||
assert (1000, 6779) not in calls
|
||||
assert sorted(set(calls)) == sorted(calls) # dédupliqué
|
||||
assert sorted(set(calls)) == sorted(calls) # deduplicated
|
||||
|
||||
def test_no_download_without_edge_buffer(self, tmp_path, monkeypatch):
|
||||
"""Raccord désactivé : aucun téléchargement de voisines."""
|
||||
"""Edge stitching disabled: no neighbor download."""
|
||||
import lidar_pipeline.fetch_ign as fetch_ign
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
input_dir = tmp_path / "input"
|
||||
@ -99,7 +99,7 @@ class TestFetchEdgeNeighbors:
|
||||
lazh.touch()
|
||||
|
||||
def boom(*args, **kwargs):
|
||||
raise AssertionError("fetch_tiles ne doit pas être appelé")
|
||||
raise AssertionError("fetch_tiles must not be called")
|
||||
|
||||
monkeypatch.setattr(fetch_ign, "fetch_tiles", boom)
|
||||
pipeline = LidarArchaeoPipeline(input_dir, tmp_path / "output",
|
||||
@ -108,7 +108,7 @@ class TestFetchEdgeNeighbors:
|
||||
|
||||
|
||||
class TestCleanupEdgeNeighborDuplicates:
|
||||
"""Nettoyage des doublons entre input/ et input/edge_neighbors/."""
|
||||
"""Cleanup of duplicates between input/ and input/edge_neighbors/."""
|
||||
|
||||
def test_removes_true_duplicates_keeps_unique_and_part(self, tmp_path):
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
@ -125,8 +125,8 @@ class TestCleanupEdgeNeighborDuplicates:
|
||||
|
||||
(input_dir / dup_name).write_bytes(b"authoritative")
|
||||
(edge_dir / dup_name).write_bytes(b"authoritative")
|
||||
# input/ tronqué (taille différente) : la voisine peut être la seule
|
||||
# copie saine, elle doit rester.
|
||||
# Truncated input/ copy (different size): the neighbor may be the only
|
||||
# sound copy, it must stay.
|
||||
(input_dir / mismatch_name).write_bytes(b"")
|
||||
(edge_dir / mismatch_name).write_bytes(b"complete")
|
||||
(edge_dir / unique_name).write_bytes(b"voisine-unique")
|
||||
@ -149,7 +149,7 @@ class TestCleanupEdgeNeighborDuplicates:
|
||||
input_dir.mkdir()
|
||||
pipeline = LidarArchaeoPipeline(input_dir, tmp_path / "output",
|
||||
edge_buffer=100.0)
|
||||
# Ne doit pas lever si edge_neighbors/ n'existe pas encore.
|
||||
# Must not raise if edge_neighbors/ does not exist yet.
|
||||
pipeline._cleanup_edge_neighbor_duplicates(input_dir / "edge_neighbors")
|
||||
|
||||
|
||||
@ -168,11 +168,11 @@ class TestLidarArchaeoPipeline:
|
||||
|
||||
def test_init_raises_on_missing_input(self, tmp_path):
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
with pytest.raises(ValueError, match="introuvable"):
|
||||
with pytest.raises(ValueError, match="not found"):
|
||||
LidarArchaeoPipeline("/nonexistent/path", str(tmp_path / "output"))
|
||||
|
||||
def test_incremental_index_rebuild(self, tmp_path, monkeypatch):
|
||||
"""Mode incrémental : l'index est régénéré après une tuile, avec anti-rebond."""
|
||||
"""Incremental mode: the index is rebuilt after a tile, with debounce."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
import lidar_pipeline.index as index_mod
|
||||
|
||||
@ -185,10 +185,10 @@ class TestLidarArchaeoPipeline:
|
||||
pipeline = LidarArchaeoPipeline(str(input_dir), str(tmp_path / "output"),
|
||||
incremental_index=True)
|
||||
pipeline._rebuild_index_incremental()
|
||||
pipeline._rebuild_index_incremental() # < 3 s : anti-rebond, ignoré
|
||||
pipeline._rebuild_index_incremental() # < 3 s: debounced, deferred (not run immediately)
|
||||
assert len(calls) == 1
|
||||
|
||||
# --no-index : jamais de rebuild incrémental
|
||||
# --no-index: never an incremental rebuild
|
||||
pipeline._last_index_rebuild = 0.0
|
||||
pipeline.no_index = True
|
||||
pipeline._rebuild_index_incremental()
|
||||
@ -218,35 +218,35 @@ class TestLidarArchaeoPipeline:
|
||||
assert "readme.txt" not in names
|
||||
|
||||
def test_find_laz_files_sorted_north_to_south(self, tmp_path):
|
||||
"""Lignes LHD triées du nord au sud (row décroissante, col croissante)."""
|
||||
"""LHD rows sorted north to south (decreasing row, increasing col)."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
input_dir = tmp_path / "input"
|
||||
input_dir.mkdir()
|
||||
for name in ("LHD_FXX_1054_6880_PTS_LAMB93_IGN69.copc.laz",
|
||||
"LHD_FXX_1056_6882_PTS_LAMB93_IGN69.copc.laz",
|
||||
"LHD_FXX_1053_6882_PTS_LAMB93_IGN69.copc.laz",
|
||||
"zz_autre.laz"):
|
||||
"zz_other.laz"):
|
||||
(input_dir / name).touch()
|
||||
|
||||
pipeline = LidarArchaeoPipeline(str(input_dir), str(tmp_path / "output"))
|
||||
names = [f.name for f in pipeline.find_laz_files()]
|
||||
assert names == [
|
||||
"LHD_FXX_1053_6882_PTS_LAMB93_IGN69.copc.laz", # ligne nord, col mini
|
||||
"LHD_FXX_1056_6882_PTS_LAMB93_IGN69.copc.laz", # ligne nord, col maxi
|
||||
"LHD_FXX_1054_6880_PTS_LAMB93_IGN69.copc.laz", # ligne sud
|
||||
"zz_autre.laz", # hors pattern : en fin
|
||||
"LHD_FXX_1053_6882_PTS_LAMB93_IGN69.copc.laz", # north row, lowest col
|
||||
"LHD_FXX_1056_6882_PTS_LAMB93_IGN69.copc.laz", # north row, highest col
|
||||
"LHD_FXX_1054_6880_PTS_LAMB93_IGN69.copc.laz", # south row
|
||||
"zz_other.laz", # non-LHD pattern: last
|
||||
]
|
||||
|
||||
|
||||
class TestDtmMethodSidecar:
|
||||
"""Méthode de classification enregistrée à côté du DTM (invalidation du cache)."""
|
||||
"""Classification method recorded next to the DTM (cache invalidation)."""
|
||||
|
||||
def test_missing_sidecar_matches(self, tmp_path):
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
input_dir = tmp_path / "input"
|
||||
input_dir.mkdir()
|
||||
pipeline = LidarArchaeoPipeline(str(input_dir), str(tmp_path / "output"), ground_method='csf')
|
||||
# Aucun sidecar écrit → cache conservé (considéré compatible).
|
||||
# No sidecar written → cache kept (considered compatible).
|
||||
assert pipeline._dtm_method_matches("tileA", "") is True
|
||||
|
||||
def test_matching_method(self, tmp_path):
|
||||
@ -276,7 +276,7 @@ class TestDtmMethodSidecar:
|
||||
assert sidecar.exists()
|
||||
assert sidecar.read_text(encoding="utf-8").strip() == "smrf"
|
||||
assert pipeline._dtm_method_name("tileA", "_r0p2") == "smrf"
|
||||
# Le sidecar est un fichier .txt : il ne gêne pas la recherche des DTM .tif.
|
||||
# The sidecar is a .txt file: it does not interfere with the .tif DTM lookup.
|
||||
dtm = tmp_path / "output" / "DTM" / "tileA_dtm_r0p2.tif"
|
||||
dtm.touch()
|
||||
assert [p.name for p in (tmp_path / "output" / "DTM").glob("*.tif")] == ["tileA_dtm_r0p2.tif"]
|
||||
@ -298,18 +298,18 @@ class TestDtmMethodSidecar:
|
||||
(vis_dir / "tileA_ortho.avif").touch()
|
||||
dtm = tmp_path / "dtm.tif"
|
||||
|
||||
# Image existante, pas de force → ignorée (pas de régénération).
|
||||
# Existing image, no force → skipped (no regeneration).
|
||||
pipeline.generate_all_visualizations(dtm, "tileA", resolution=0.5, vis_dir=vis_dir, force_images=False)
|
||||
assert calls == []
|
||||
|
||||
# Image existante, force_images=True → régénérée.
|
||||
# Existing image, force_images=True → regenerated.
|
||||
calls.clear()
|
||||
pipeline.generate_all_visualizations(dtm, "tileA", resolution=0.5, vis_dir=vis_dir, force_images=True)
|
||||
assert calls == ["tileA"]
|
||||
|
||||
class TestEffectiveGroundMethod:
|
||||
def test_ign_label_encodes_classes(self):
|
||||
"""Les classes IGN sont encodées dans l'étiquette de cache (reclassification)."""
|
||||
"""IGN classes are encoded in the cache label (reclassification)."""
|
||||
from lidar_pipeline.pipeline import LidarArchaeoPipeline
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@ -334,8 +334,8 @@ class TestEffectiveGroundMethod:
|
||||
|
||||
|
||||
class TestQualityCacheHit:
|
||||
"""Sidecar qualité écrit même quand le DTM primaire est réutilisé depuis
|
||||
le cache (classification sol sautée, pas de LAS sol disponible)."""
|
||||
"""Quality sidecar written even when the primary DTM is reused from the
|
||||
cache (ground classification skipped, no ground LAS available)."""
|
||||
|
||||
@staticmethod
|
||||
def _write_las(path, x, y, cls, t):
|
||||
@ -376,11 +376,11 @@ class TestQualityCacheHit:
|
||||
pipeline = LidarArchaeoPipeline(
|
||||
str(input_dir), str(tmp_path / "output"), ground_method='ign',
|
||||
ign_classes="sol", strip_align=False, edge_buffer=0.0)
|
||||
pipeline.viz_steps = [] # aucune visualisation à calculer (hors périmètre)
|
||||
pipeline.viz_steps = [] # no visualization to compute (out of scope)
|
||||
|
||||
# DTM déjà en cache, compatible avec la config du run (pas de calage,
|
||||
# pas de raccord, comblement à la version courante) : le run doit
|
||||
# emprunter la branche « DTM existant » sans reclassifier ni régénérer.
|
||||
# DTM already cached, compatible with the run config (no alignment,
|
||||
# no edge buffer, gap filling at the current version): the run must
|
||||
# take the "existing DTM" branch without reclassifying or regenerating.
|
||||
dtm_path = pipeline.dtm_dir / f"{base}_dtm.tif"
|
||||
with rasterio.open(
|
||||
dtm_path, "w", driver="GTiff", height=10, width=10, count=1,
|
||||
@ -391,7 +391,7 @@ class TestQualityCacheHit:
|
||||
dst.update_tags(**{GAP_FILL_TAG: GAP_FILL_VERSION})
|
||||
|
||||
def boom(*a, **k):
|
||||
raise AssertionError("cache hit attendu : ne doit pas reclassifier/régénérer le DTM")
|
||||
raise AssertionError("cache hit expected: must not reclassify/regenerate the DTM")
|
||||
|
||||
monkeypatch.setattr(pipeline_mod, "classify_ground", boom)
|
||||
monkeypatch.setattr(pipeline_mod, "create_dtm_fast", boom)
|
||||
@ -400,7 +400,7 @@ class TestQualityCacheHit:
|
||||
|
||||
data = read_quality(pipeline.output_dir, base)
|
||||
assert data is not None
|
||||
assert abs(data["ground_density"] - 2e-6) < 1e-9 # 2 points de classe 2 sur 1 km²
|
||||
assert abs(data["ground_density"] - 2e-6) < 1e-9 # 2 class-2 points over 1 km²
|
||||
assert data["acq_start"] == "2022-06-01" and data["acq_source"] == "gps"
|
||||
|
||||
|
||||
@ -413,7 +413,7 @@ class TestResolveWorkers:
|
||||
|
||||
def test_auto_bounded(self):
|
||||
from lidar_pipeline.pipeline import resolve_workers
|
||||
# Bornes : jamais sous 2, jamais au-dessus de 16
|
||||
# Bounds: never below 2, never above 16
|
||||
assert 2 <= resolve_workers('auto') <= 16
|
||||
|
||||
def test_explicit_int(self):
|
||||
@ -427,8 +427,8 @@ class TestResolveWorkers:
|
||||
assert resolve_workers(None) == 1
|
||||
|
||||
def test_worker_slot_initializer(self, monkeypatch):
|
||||
"""Chaque processus du pool prend UNE place à sa création : GPU
|
||||
(set_active_gpu) ou CPU forcé (-1) ; None ou file vide = libre."""
|
||||
"""Each pool process takes ONE slot when it starts: GPU
|
||||
(set_active_gpu) or forced CPU (-1); None or empty queue = free."""
|
||||
import queue
|
||||
from lidar_pipeline import gpu, pipeline
|
||||
calls = []
|
||||
@ -437,6 +437,6 @@ class TestResolveWorkers:
|
||||
q = queue.Queue()
|
||||
for slot in (1, -1, None):
|
||||
q.put(slot)
|
||||
for _ in range(4): # 4e appel : file vide
|
||||
for _ in range(4): # 4th call: empty queue
|
||||
pipeline._init_worker_slot(q)
|
||||
assert calls == [("gpu", 1), ("cpu",)]
|
||||
|
||||
Reference in New Issue
Block a user