Files
Aislo/resources/tester/test_511_b3_profile_node.py
T

208 lines
8.5 KiB
Python

# -*- coding: utf-8 -*-
"""511-B B-3 종단 짝 모으기 — 종단 다시 셈 · 첫 계획선 · 계획고 옮김 = 서버 Node(TS 한 벌).
옛 파이썬 길(`build_alignment` · `straight_pass_through` — 2~4 순위 폴백 몫으로 남음)로 지은
정본과 새 Node 길(`rebuild_alignment_profile` · `design_ground_following_profile` 겉 →
`profile.alignment` · `profile.base` → `alignment_record`)이 낸 정본의 **JSON 글이 같음**
(칸 · 차례 · 실수형 · −0.0 · 반올림).
사용자 R(측점 밖 곡선 시종점) · 측점 0.006 m 안 편집 키 · 법정 R 미달 경고 · 시종점 오프셋 포함.
./venv/Scripts/python.exe -m pytest resources/tester/test_511_b3_profile_node.py -q
"""
from __future__ import annotations
import copy
import json
import math
import sys
from dataclasses import replace
from pathlib import Path
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B05_Profile.B05_Profile_Engine_Grade import ( # noqa: E402
ground_profile,
legal_plan_radius_min_m, # noqa: E402
resolve_grade_options,
)
from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402
AlignmentPolicy,
build_alignment,
)
from B05_Profile.B05_Profile_Engine_Grade_Carry import carry_edits # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade_Node import carry_seat # noqa: E402
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
design_ground_following_profile,
infer_station_interval,
rebuild_alignment_profile,
straight_pass_through,
)
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
reason="프론트엔드 의존성 미설치",
)
def _ground(s: float, phase: float) -> float:
return 300.0 + 0.06 * s + 1.4 * math.sin(s / 37.0 + phase) + 0.2 * math.sin(s / 7.3)
def _longitudinal(phase: float, length: float = 400.0) -> dict:
samples = [
{"chainage_m": float(s), "elevation_m": _ground(float(s), phase), "valid": True}
for s in np.arange(0.0, length + 1e-9, 1.0)
]
marks = sorted({*np.arange(0.0, length + 1e-9, 20.0).tolist(), 133.37, 251.2, 318.06})
stations = [
{"station_id": f"s{index}", "chainage_m": float(value)} for index, value in enumerate(marks)
]
return {"samples": samples, "stations": stations, "length_m": length, "design_profiles": []}
def _options():
return resolve_grade_options("trunk", terrain_type="special", design_speed_kph=30)
def _old_ground_following(longitudinal: dict, options, edits: dict | None) -> dict:
"""옛 1순위 몸통(B-3 전 `design_ground_following_profile`) — 파이썬 꼭짓점 · 파이썬 선형."""
chainage, ground = ground_profile(longitudinal)
total = float(chainage[-1])
stations = list(longitudinal["stations"])
policy = AlignmentPolicy.from_config(
station_interval_m=infer_station_interval(stations),
max_grade_pct=options.max_grade_pct,
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
paved=options.paved,
min_plan_radius_m=legal_plan_radius_min_m(options.design_speed_kph, options.terrain_type),
min_vertical_radius_m=options.min_vertical_radius_m,
min_curve_length_m=options.min_curve_length_m,
)
node_s = sorted(
{round(float(row["chainage_m"]), 3) for row in stations if 0 <= row["chainage_m"] <= total}
)
if node_s[-1] < total:
node_s.append(round(total, 3))
base_s = np.array(node_s, dtype=np.float64)
base_z = np.interp(base_s, chainage, ground)
base_z[0] += options.start_elevation_offset_m
base_z[-1] += options.end_elevation_offset_m
base_z = straight_pass_through(base_s, base_z, policy)
return build_alignment(
base_s=base_s,
base_z=base_z,
chainage=chainage,
ground=ground,
stations=stations,
policy=policy,
edits=dict(edits or {}),
)
def _old_rebuild(longitudinal: dict, edits: dict | None) -> dict:
"""옛 `rebuild_alignment_profile` 몸통 — 저장 꼭짓점 · 파이썬 선형."""
stored = longitudinal["profile_alignment"]
chainage, ground = ground_profile(longitudinal)
base = stored["base_pvi"]
criteria = (longitudinal.get("design_profiles") or [{}])[0].get("criteria") or {}
policy = AlignmentPolicy.from_dict(stored.get("policy") or {})
policy = replace(
policy,
min_vertical_radius_m=policy.min_vertical_radius_m
or float(criteria.get("min_vertical_radius_m") or 0.0),
min_curve_length_m=policy.min_curve_length_m
or float(criteria.get("min_curve_length_m") or 0.0),
)
return build_alignment(
base_s=np.array([float(n["chainage_m"]) for n in base], dtype=np.float64),
base_z=np.array([float(n["elevation_m"]) for n in base], dtype=np.float64),
chainage=chainage,
ground=ground,
stations=list(longitudinal["stations"]),
policy=policy,
edits=edits,
)
def _text(value) -> str:
return json.dumps(value, ensure_ascii=False)
EDITS = {
"station_offsets": {"60.000": 0.8, "133.370": -0.45, "251.204": 0.3, "300.000": None},
"curve_radii": {"100.000": 350.0, "251.200": 77.7, "60.000": 1234.5},
}
@pytest.mark.parametrize("phase", [0.0, 0.7, 2.1])
def test_첫_계획선_옛_파이썬과_정본_글_같음(phase: float) -> None:
longitudinal = _longitudinal(phase)
options = _options()
old = _old_ground_following(longitudinal, options, None)
new, profile = design_ground_following_profile(longitudinal, options)
assert _text(new) == _text(old)
assert profile["samples"] == new["samples"]
@pytest.mark.parametrize("phase", [0.0, 0.7, 2.1])
def test_다시_셈_옛_파이썬과_정본_글_같음(phase: float) -> None:
longitudinal = _longitudinal(phase)
alignment, profile = design_ground_following_profile(longitudinal, _options())
longitudinal["profile_alignment"] = alignment
longitudinal["design_profiles"] = [profile]
for edits in (None, {}, copy.deepcopy(EDITS)):
old = _old_rebuild(longitudinal, copy.deepcopy(edits))
new, entry = rebuild_alignment_profile(longitudinal, copy.deepcopy(edits))
assert _text(new) == _text(old)
assert entry["summary"]["warnings"] == old["warnings"]
# 사용자 R 77.7 m = 측점 밖 곡선 시종점 · 법정 R 미달 경고(432)
assert any(abs(s["chainage_m"] * 1000 % 1) > 1e-6 for s in new["samples"])
assert any("종단곡선" in text for text in new["warnings"])
def test_시종점_오프셋_첫_계획선_같음() -> None:
longitudinal = _longitudinal(1.3)
options = replace(_options(), start_elevation_offset_m=0.6, end_elevation_offset_m=-0.4)
old = _old_ground_following(longitudinal, options, EDITS)
new, _profile = design_ground_following_profile(longitudinal, options, edits=EDITS)
assert _text(new) == _text(old)
def test_저장_선형_없으면_ValueError() -> None:
with pytest.raises(ValueError):
rebuild_alignment_profile(_longitudinal(0.0), {})
def test_계획고_옮김_꼭짓점_세움_한_부름() -> None:
"""노선 [저장] 몫 — 편집 옮김(`carryEdits`)과 꼭짓점 세움(`seatRouteMoved`)이 Node 한 부름 ·
편집만 부르는 겉(`carry_edits`)과 같은 편집."""
old_xy = [[0.0, 0.0], [200.0, 0.0], [400.0, 0.0]]
new_xy = [[0.0, 0.0], [200.0, 0.0], [300.0, 60.0], [420.0, 60.0]]
longitudinal = _longitudinal(0.4)
alignment, _profile = design_ground_following_profile(longitudinal, _options())
base = alignment["base_pvi"]
edits = {"station_offsets": {"60.000": 0.5, "120.000": -0.3}, "curve_radii": {"120.000": 400}}
stations = [row["chainage_m"] for row in longitudinal["stations"]]
seat = {
"ground_s": [row["chainage_m"] for row in alignment["samples"]],
"ground_z": [row["ground_elevation_m"] for row in alignment["samples"]],
"policy": AlignmentPolicy.from_dict(alignment["policy"]).as_dict(),
"base_pvi": base,
"born": [251.2],
}
moved, seated = carry_seat(old_xy, new_xy, base, edits, stations, 10.0, seat)
assert moved == carry_edits(old_xy, new_xy, base, edits, stations, 10.0)
assert set(moved["station_offsets"]) == {"60.000", "120.000"}
assert moved["curve_radii"] == {"120.000": 400.0}
assert all(isinstance(v, float) for v in moved["station_offsets"].values())
assert any(abs(node["chainage_m"] - 251.2) < 1e-6 for node in seated)
assert carry_edits(old_xy, new_xy, [], edits, stations, 10.0) == {
"station_offsets": {},
"curve_radii": {},
}