걷은 함수(운영 부름 0 · 전수 확인 = 화면 · 서버 · 체인 · 추천 엔진 · B07 · 시험) - common_util/common_util_excavation.py 통째(126 줄 · pipe_trench_width_m · wall_trench_area_m2 · wall_trench_width_m · 서버 = Node excavation.trench) - Engine_Grade_Carry.carry_edits(Node carry_seat 겉) · Route_Save_Parts.seat_route_moved(시험 헬퍼 helper_475_1_moved 로 옮김) - Engine_Tilt.attach_underdrains · Engine_Structures_Wall.attach_wall_structures · load_wall_structures - Engine_Grade_Alignment.tangent_elevation(부름 0) · station_key.dedupe_station_keys · loose_chainage 줄 수 전 → 후: excavation.py 126 → 0 · Grade_Carry 178 → 157 · Save_Parts 289 → 269 · Wall 221 → 197 · Tilt 115 → 112 · station_key 141 → 122 · Alignment 657 → 654 남는 짝(머리 「남는 까닭」 한 줄) — Grade · Grade_Profile · Alignment · CurveLaw(2~4 순위 폴백 · 추천) · NodeEdits · SampleExtend · Router_Design · Ranges · Engine_Design · Areas · cross_berm(DTM 아닌 지표면 · Node 못 부를 때 · B07 · 추천) · Tilt · Wall · Culvert(B04 B07 B08) · route_follow · structure_station · station_key · route_polyline(745 줄 · 까닭 = 등록부 R1 줄) 등록부 — until 칸 걷음(G1 = py 표본기 남김 · 까닭 note) · C6 pair = attach_underdrain_specs · F8 pair null 알려진 차 — golden KNOWN 셋 그대로 나타남(R7 표본 출처 · W0 저장 때 경고 · X2 입력 선 자릿수 · 까닭 고침) · underdrain 알려진 차 까닭 · 511_b5 KNOWN_ARC 그대로 계산 자리 창 — 45 줄 · 서버만 아닌 41(바꿀 수 있음 40 + F9) · 서버만 넷(G3 D1 D2 N1 줄마다 까닭) · 옛 까닭 「짝 모으는 중 · B-4」 글 걷음 test_322_3_flow 빨강 — 헬퍼가 @config/config_frontend 흉내(511-A design_calc_server import) 시험 기준 — 걷은 짝 거울 = 걷기 전 파이썬 값 고정(excavation · station_key) · Node carry_seat · attach_underdrain_specs · wall_specs_from 검증: 고친 대상 시험 97 파일 562 통과 · 21 건너뜀 · typecheck 0 · 700 줄 넘는 새 파일 없음 — 요구 320-4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
8.3 KiB
Python
203 lines
8.3 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_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 한 부름 ·
|
|
편집만 부른 값(seat 없음)과 같은 편집."""
|
|
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_seat(old_xy, new_xy, base, edits, stations, 10.0)[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)
|