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:
Antoine
2026-09-27 23:16:45 +02:00
parent cc1c22d2b8
commit fb892ea9f2
52 changed files with 4356 additions and 4323 deletions

View File

@ -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",)]