From 5569bb6750d122bdf25280ff4a9a28d73d739e35 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 7 Oct 2026 18:46:44 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EA=B7=A0=ED=98=95=20=EC=A2=85?= =?UTF-8?q?=EB=8B=A8=20=EB=B0=98=EC=98=81=EB=8F=84=201=EC=B0=A8=20?= =?UTF-8?q?=EC=A2=85=EB=8B=A8=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=E2=80=94=20=EC=A2=85=EB=8B=A8=EA=B3=A1=EC=84=A0?= =?UTF-8?q?=EC=9D=B4=20=EB=93=A4=EC=96=B4=EA=B0=80=EB=8F=84=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=EB=A7=88=EB=8B=A4=20=EA=B7=A0=ED=98=95=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=EA=B3=A0=EB=A5=BC=20=EC=A7=80=EB=82=A8(74-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 꼭짓점 반복 보정을 pass_through 로 떼어 1차 종단(목표 = 지반고) · 균형 종단 반영(목표 = 균형 종단)이 함께 씀 - 측점 계획고 대 균형 종단 차 최대 0.092 m 였던 것을 없앰 - 「종단 반영」 도움말 — 켬으로 [적용]한 노선 그대로 [확인]하면 균형 종단이 첫 계획선 - 토글 단추 — 두 줄 알약 오른쪽 가운데 · 켬 글자색 토큰 바로잡음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7 --- .../B05_Profile_Engine_Grade_Profile.py | 33 ++++++++++++------- .../B05_Profile_Router_RouteInitial.py | 14 ++++++-- ...B05_Profile_UI_RouteEdit_Recommend_Help.ts | 2 +- .../B05_Profile_UI_Style_RouteRecommend.css | 4 +-- .../tester/test_74_route_confirm_initial.py | 27 +++++++++++++-- 5 files changed, 59 insertions(+), 21 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_Grade_Profile.py b/B05_Profile/B05_Profile_Engine_Grade_Profile.py index b5991a448..9a904d09f 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Profile.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Profile.py @@ -145,6 +145,26 @@ def _clearance_at( return best +def pass_through(base_s: np.ndarray, target: np.ndarray, policy: AlignmentPolicy) -> np.ndarray: + """종단곡선을 넣어도 계획선이 측점마다 `target` 을 지나게 하는 꼭짓점 표고. + + 대칭 종단곡선은 꼭짓점을 지나지 않는다(중앙종거 |A|·L/8) — 보정 없이 곡선을 넣으면 + 계획고가 목표에서 뜬다. 호가 측점 세로선과 만나는 점이 목표가 되도록 꼭짓점을 + 밀어내며, 표고가 움직이면 대수차도 변하므로 수렴할 때까지 되풀이한다. 1차 종단(목표 = + 지반고)과 노선 추천 균형 종단 반영(목표 = 균형 종단, PLAN 74-4)이 함께 쓴다. + """ + base_z = np.asarray(target, dtype=np.float64).copy() + for _ in range(12): + curves, _curve_warnings = build_curves(base_s, base_z, policy) + error = target - evaluate(base_s, base_z, curves, base_s) + error[0] = 0.0 + error[-1] = 0.0 + if float(np.max(np.abs(error))) < 1e-4: + break + base_z = base_z + error + return base_z + + def design_ground_following_profile( longitudinal: dict[str, Any], options: GradeDesignOptions, @@ -209,18 +229,7 @@ def design_ground_following_profile( if note: warnings.append(note) - # 대칭 종단곡선은 꼭짓점을 지나지 않는다(중앙종거 |A|·L/8) — 보정 없이 곡선을 넣으면 - # 계획고가 지반고에서 뜬다. 호가 측점 세로선과 만나는 점이 지반고가 되도록 꼭짓점을 - # 밀어내며, 표고가 움직이면 대수차도 변하므로 수렴할 때까지 되풀이한다. - target = base_z.copy() - for _ in range(12): - curves, _curve_warnings = build_curves(base_s, base_z, policy) - error = target - evaluate(base_s, base_z, curves, base_s) - error[0] = 0.0 - error[-1] = 0.0 - if float(np.max(np.abs(error))) < 1e-4: - break - base_z = base_z + error + base_z = pass_through(base_s, base_z, policy) alignment = build_alignment( base_s=base_s, diff --git a/B05_Profile/B05_Profile_Router_RouteInitial.py b/B05_Profile/B05_Profile_Router_RouteInitial.py index c94f9e657..6dd9af48c 100644 --- a/B05_Profile/B05_Profile_Router_RouteInitial.py +++ b/B05_Profile/B05_Profile_Router_RouteInitial.py @@ -127,12 +127,20 @@ def _set_base_pvi(path: Path, stations: list[float], plan: list[float]) -> int: import numpy as np + from B05_Profile.B05_Profile_Engine_Grade_Alignment import AlignmentPolicy + from B05_Profile.B05_Profile_Engine_Grade_Profile import pass_through from common_util.common_util_json import atomic_write_json longitudinal = json.loads(path.read_text(encoding="utf-8")) - base = longitudinal["profile_alignment"]["base_pvi"] - for item in base: - item["elevation_m"] = round(float(np.interp(float(item["chainage_m"]), stations, plan)), 4) + alignment = longitudinal["profile_alignment"] + base = alignment["base_pvi"] + base_s = np.array([float(item["chainage_m"]) for item in base]) + # 1차 종단과 같은 맞춤 — 종단곡선을 넣어도 측점마다 균형 종단 계획고를 지나게. + base_z = pass_through( + base_s, np.interp(base_s, stations, plan), AlignmentPolicy.from_dict(alignment["policy"]) + ) + for item, value in zip(base, base_z, strict=True): + item["elevation_m"] = round(float(value), 4) atomic_write_json(path, longitudinal) return len(base) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts index 319e00253..473313265 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts @@ -113,7 +113,7 @@ const LOGIC_HELP: Record = { const CRITERIA_HELP: Array<{ name: string; text: string }> = [ { name: "종단 반영", - text: "켬(기본) = 후보마다 종단을 세워 토공 점수에 넣음 · 끔 = 원지반만 보고 점수를 냄(종단 그림 · 덮개 줄은 안 나옴). 이 창 안에서만 적용되고 [적용]한 노선 · 저장값에는 남지 않음", + text: "켬(기본) = 후보마다 종단을 세워 토공 점수에 넣음 · 끔 = 원지반만 보고 점수를 냄(종단 그림 · 덮개 줄은 안 나옴). 켬으로 [적용]한 노선을 고치지 않고 [확인]하면 그 균형 종단이 첫 계획선이 됨 · 끔 · 손으로 고친 노선은 계획고 = 지반고", }, { name: "종단기울기 상한(%)", diff --git a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css index 3dec10029..2e9a9dba2 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteRecommend.css @@ -216,7 +216,7 @@ .b05-routerec__pillrow { display: flex; gap: var(--spacing-8); - align-items: stretch; + align-items: center; } .b05-routerec__pillrow > .b05-routerec__pills { @@ -232,7 +232,7 @@ .b05-routerec__profile-toggle[aria-pressed="true"] { border-color: var(--color-primary); box-shadow: inset 0 0 0 1px var(--color-primary); - color: var(--color-primary-text, var(--color-primary)); + color: var(--color-primary); font-weight: 600; } diff --git a/resources/tester/test_74_route_confirm_initial.py b/resources/tester/test_74_route_confirm_initial.py index 2ecf3ca36..0211134d8 100644 --- a/resources/tester/test_74_route_confirm_initial.py +++ b/resources/tester/test_74_route_confirm_initial.py @@ -39,15 +39,36 @@ def test_체인이_옛_설계를_통째로_옮기지_않는다() -> None: def test_변화점_계획고를_균형_종단으로(tmp_path: Path) -> None: path = tmp_path / "longitudinal.json" base = [{"chainage_m": s, "elevation_m": 100.0} for s in (0.0, 20.0, 30.0, 40.0)] - path.write_text(json.dumps({"profile_alignment": {"base_pvi": base}}), encoding="utf-8") - assert _set_base_pvi(path, [0.0, 20.0, 40.0], [10.0, 12.0, 16.0]) == 4 + alignment = {"base_pvi": base, "policy": {}} + path.write_text(json.dumps({"profile_alignment": alignment}), encoding="utf-8") + assert _set_base_pvi(path, [0.0, 20.0, 40.0], [10.0, 12.0, 14.0]) == 4 got = [ p["elevation_m"] for p in json.loads(path.read_text("utf-8"))["profile_alignment"]["base_pvi"] ] - assert got == [10.0, 12.0, 14.0, 16.0] # 구조물 측점(30 m)은 직선 보간 + assert got == [10.0, 12.0, 13.0, 14.0] # 구조물 측점(30 m)은 직선 보간 def test_확인_요청_기본은_끔() -> None: assert RouteReplanRequest().balanced_profile is False assert RouteReplanRequest(balanced_profile=True).balanced_profile is True + + +def test_종단곡선이_있어도_측점마다_목표를_지난다() -> None: + import numpy as np + + from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( + AlignmentPolicy, + build_curves, + evaluate, + ) + from B05_Profile.B05_Profile_Engine_Grade_Profile import pass_through + + base_s = np.array([0.0, 20.0, 40.0, 60.0, 80.0]) + target = np.array([100.0, 102.4, 102.0, 104.6, 103.0]) # 대수차가 커 곡선이 들어감 + policy = AlignmentPolicy.from_dict({}) + curves, _ = build_curves(base_s, target, policy) + assert curves # 맞춤이 없으면 측점에서 뜸 + base_z = pass_through(base_s, target, policy) + got = evaluate(base_s, base_z, build_curves(base_s, base_z, policy)[0], base_s) + assert np.max(np.abs(got - target)) < 1e-3