From 867c4650c756ab528b4426576eb2fe968f6c6e70 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 29 Sep 2026 10:12:59 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EA=B4=80=20=C2=B7=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=9E=90=EC=B2=B4=20=EC=B8=A1=EC=A0=90?= =?UTF-8?q?=EC=9D=B4=20=EC=A2=85=EB=8B=A8=20=EC=B8=A1=EC=A0=90=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=95=EC=88=98=20m=20=EA=B2=A9=EC=9E=90?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=99=EB=8D=98=20=EA=B2=83=20=E2=80=94=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84=EC=97=90=EB=A7=8C=20?= =?UTF-8?q?=EC=A0=95=EC=88=98=20m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 19장 남은 차이(sub8 왕복 시험): 관 440.24 · 619.71 · 720.44 · 899.91 이 저장 뒤 종단 측점에서 440 · 620 · 720 · 900 으로 붙음(generate_sections 의 정수 m 격자 붙이기). 판정 (다) 유지 — 횡단 파일 이름(cross_{정수m}m)은 그대로 한 정수 m 에 한 장, 측점 값은 관 · 구조물 자체 측점 그대로: - 같은 정수 m 에 격자 측점이 있으면 격자 대신 관 · 구조물 측점을 씀(kind irregular · 라벨 그대로) - 시 · 종점 격자는 그대로 두고 라벨만 얹음(노선 끝 측점이 사라지지 않게) - 비정규끼리 같은 정수 m 면 앞선 것만(종전과 같음 · 파일 한 장) 시험 — test_b05_b06_save_roundtrip[B05] 통과(관 측점 차이 0 · 사본 storage · 읽기 전용 DB) · test_b05_extra_stations_supplier(옛 「격자 우선」 기대를 새 규칙으로 · 시 · 종점 하나 더함) · test_b05_station_roundtrip · test_regeneration_extra_stations 통과. [B06] 경우의 「횡단 patch 263.47 — 받을 행이 없어 DB 가 버림」은 고치기 전 코드에서도 같게 실패(이번 변경과 무관 · 보고함). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg --- .../B05_Profile_Engine_Sections_Core.py | 31 ++++++++++++++----- .../test_b05_extra_stations_supplier.py | 26 ++++++++++++---- .../tester/test_b05_station_roundtrip.py | 10 +++--- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_Sections_Core.py b/B05_Profile/B05_Profile_Engine_Sections_Core.py index 707c6694..a76490ee 100644 --- a/B05_Profile/B05_Profile_Engine_Sections_Core.py +++ b/B05_Profile/B05_Profile_Engine_Sections_Core.py @@ -341,17 +341,34 @@ def generate_sections( if 0.0 <= float(chainage) <= total } if extras: - # 횡단 파일명이 정수 미터 반올림이라(cross_filename) 1m 안의 두 측점은 같은 파일을 - # 덮어써 측점 수와 횡단 파일 수가 어긋난다. 같은 정수 미터면 격자 측점으로 스냅해 - # 라벨만 얹고(아래 station["structure"] 부착), 비정규끼리 겹치면 앞선 것만 남긴다. - # ponytail: 파일명이 정수 미터인 동안의 가드. 소수 1자리 규칙으로 바꾸면 지운다. - buckets = {int(round(float(value))): round(float(value), 6) for value in station_chainage} + # 횡단 파일명이 정수 미터 반올림이라(cross_filename) 한 정수 미터에 측점은 하나만 둔다. + # 관 · 구조물 자체 측점은 소수 그대로 — 격자 측점과 같은 정수 미터면 격자 대신 그 측점을 + # 쓴다(시 · 종점 격자만 그대로 두고 라벨을 얹음). 비정규끼리 겹치면 앞선 것만 남긴다. + # ponytail: 파일명이 정수 미터인 동안의 가드. 소수 이름 규칙으로 바꾸면 지운다. + grid = [round(float(value), 6) for value in station_chainage] + ends = {grid[0], grid[-1]} + buckets = {int(round(value)): value for value in grid} + taken: set[int] = set() + dropped: set[float] = set() snapped: dict[float, str] = {} for chainage in sorted(extras): - snapped.setdefault(buckets.setdefault(int(round(chainage)), chainage), extras[chainage]) + bucket = int(round(chainage)) + if bucket in taken: + continue + taken.add(bucket) + regular = buckets.get(bucket) + if regular in ends: + snapped[regular] = extras[chainage] + continue + if regular is not None: + dropped.add(regular) + snapped[chainage] = extras[chainage] extras = snapped station_chainage = np.unique( - np.r_[station_chainage, np.array(sorted(extras), dtype=np.float64)] + np.r_[ + np.array([value for value in grid if value not in dropped], dtype=np.float64), + np.array(sorted(extras), dtype=np.float64), + ] ) station_xy = _interpolate_xy(points, route_chainage, station_chainage) tangents = np.vstack( diff --git a/resources/tester/test_b05_extra_stations_supplier.py b/resources/tester/test_b05_extra_stations_supplier.py index 0594c692..689ea264 100644 --- a/resources/tester/test_b05_extra_stations_supplier.py +++ b/resources/tester/test_b05_extra_stations_supplier.py @@ -192,7 +192,7 @@ def test_matching_signature_loads_points(tmp_path): ] -# ── 횡단 파일명 충돌 스냅(격자 우선 불변식) ──────────────────────────────── +# ── 횡단 파일명 충돌 — 한 정수 미터에 측점 하나 · 관 · 구조물 자체 측점 우선(PLAN 19장) ── def _stations(extra_stations): @@ -204,17 +204,31 @@ def _stations(extra_stations): return result["longitudinal"]["stations"] -def test_extra_station_within_one_meter_snaps_to_grid(): +def test_extra_station_in_grid_meter_replaces_grid_station(): stations = _stations(((100.4, "X"),)) - assert 100.4 not in [station["chainage_m"] for station in stations] + chainages = [station["chainage_m"] for station in stations] + # 관 · 구조물 자체 측점 그대로 — 같은 정수 미터 격자 100 은 빠져 파일(cross_00100m)은 한 장 + assert 100.4 in chainages and 100.0 not in chainages labelled = [station for station in stations if station.get("structure") == "X"] - assert [station["chainage_m"] for station in labelled] == [100.0] - assert labelled[0]["kind"] == "regular" # 격자 측점 우선 — 기존 불변식 + assert [(station["chainage_m"], station["kind"]) for station in labelled] == [ + (100.4, "irregular") + ] + assert len({cross_filename(c) for c in chainages}) == len(chainages) + + +def test_extra_station_at_route_end_keeps_end_station(): + stations = _stations(((0.3, "S"), (349.8, "E"))) + chainages = [station["chainage_m"] for station in stations] + # 시 · 종점은 격자 그대로 두고 라벨만 얹음 + assert chainages[0] == 0.0 and chainages[-1] == 350.0 + assert 0.3 not in chainages and 349.8 not in chainages + assert stations[0].get("structure") == "S" and stations[-1].get("structure") == "E" def test_two_extras_in_same_meter_bucket_keep_one(): stations = _stations(((80.2, "A"), (80.4, "B"))) - assert [station["chainage_m"] for station in stations if station.get("structure")] == [80.0] + assert [station["chainage_m"] for station in stations if station.get("structure")] == [80.2] + assert 80.0 not in [station["chainage_m"] for station in stations] def test_live_extras_stay_irregular_and_filenames_stay_unique(): diff --git a/resources/tester/test_b05_station_roundtrip.py b/resources/tester/test_b05_station_roundtrip.py index e0f84139..68e956c5 100644 --- a/resources/tester/test_b05_station_roundtrip.py +++ b/resources/tester/test_b05_station_roundtrip.py @@ -52,9 +52,9 @@ def test_계획고_편집은_가까운_새_측점으로_옮김(results): assert "200.000" not in results["deleted"]["station_offsets"] -# ── 서버 — 사용자가 놓은 구조물 측점은 소수 그대로(격자 붙이기는 횡단 측점에만) ───────── -# 브레인 판정(다): 횡단 측점 · 파일 이름의 정수 m 격자 붙이기는 그대로 두고, 구조물 정본 측점만 -# 사용자가 놓은 소수 그대로 저장 · 다시 읽기. 임시 storage 에서만 씀(공유 storage 안 건드림). +# ── 서버 — 관 · 구조물 자체 측점은 소수 그대로(정수 m 는 횡단 파일 이름에만) ───────── +# 브레인 판정(다) · 19장 남은 차이: 파일 이름은 한 정수 m 에 한 장 그대로, 측점 값은 사용자가 놓은 +# 소수 그대로 저장 · 다시 읽기 · 종단 측점. 임시 storage 에서만 씀(공유 storage 안 건드림). def _flat_sampler(): @@ -96,8 +96,8 @@ def test_사용자_구조물_119_8_저장_다시_읽기_그대로(tmp_path): SectionGenerationOptions(extra_stations=extras), ) chainages = [round(s["chainage_m"], 3) for s in sections["longitudinal"]["stations"]] - # 횡단 측점은 격자 120 으로 붙음(파일 이름 cross_00120m 한 장) — 정본 측점은 119.8 그대로 - assert 120.0 in chainages and 119.8 not in chainages + # 횡단 측점도 자체 측점 119.8(같은 정수 미터 격자 120 대신 · 파일 이름 cross_00120m 한 장) + assert 119.8 in chainages and 120.0 not in chainages assert cross_filename(119.8) == cross_filename(120.0) _, again = load_structures(str(tmp_path)) assert [s.chainage_m for s in again] == [119.8] and again[0].memo == "메모만"