fix(455 433-13② 폴백 첫 종단): 관 정착 · 직선 분할 선형도 초기값 직선화 — R 500 넘는 꼭짓점 묶음 직선 위 · 여유로 들린 관 꼭짓점은 경계로 그대로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -367,11 +367,13 @@ def design_pipe_anchored_profile(
|
||||
# 높이로, 정본 산식은 `common_util_drainage_pipes.facility_clearance_m`이다.
|
||||
# 시·종점만 오프셋을 얹는다.
|
||||
target = np.interp(base_s, chainage, ground)
|
||||
lifted = np.zeros(len(base_s), dtype=bool)
|
||||
if pipe_clearances:
|
||||
for index, value in enumerate(base_s):
|
||||
clearance = _clearance_at(pipe_clearances, float(value))
|
||||
if clearance > 0:
|
||||
target[index] += clearance
|
||||
lifted[index] = True
|
||||
target[0] = fixed[0]
|
||||
target[-1] = fixed[1]
|
||||
|
||||
@@ -380,12 +382,15 @@ def design_pipe_anchored_profile(
|
||||
edits = dict(edits or {})
|
||||
user_radii = dict(edits.get("curve_radii") or {})
|
||||
|
||||
def radii_for(nodes_z: np.ndarray) -> dict[str, float]:
|
||||
"""현 표고에서의 대수차로 R = L / A 를 역산한다(사용자 지정 R이 있으면 그쪽이 이긴다)."""
|
||||
def radii_for(nodes_z: np.ndarray, held: np.ndarray) -> dict[str, float]:
|
||||
"""현 표고에서의 대수차로 R = L / A 를 역산한다(사용자 지정 R이 있으면 그쪽이 이긴다) ·
|
||||
직선 위로 옮긴(`held`) 꼭짓점은 R 없음."""
|
||||
spans = base_s[1:] - base_s[:-1]
|
||||
grades = (nodes_z[1:] - nodes_z[:-1]) / np.where(spans > 0, spans, 1.0)
|
||||
resolved: dict[str, float] = {}
|
||||
for index in range(1, len(base_s) - 1):
|
||||
if held[index]:
|
||||
continue
|
||||
delta = abs(float(grades[index] - grades[index - 1]))
|
||||
radius = target_length / delta if delta > 1e-9 else min_radius
|
||||
resolved[chainage_key(base_s[index])] = max(radius, min_radius)
|
||||
@@ -394,19 +399,28 @@ def design_pipe_anchored_profile(
|
||||
# 대칭 종단곡선은 꼭짓점(변화점)을 지나지 않는다 — 곡선이 배관 지반고를 통과하도록
|
||||
# 변화점 표고를 반복 보정한다(호가 안쪽으로 파고드는 중앙종거만큼 꼭짓점을 밀어낸다).
|
||||
# 표고가 움직이면 대수차도 변하므로 R도 매 회 다시 역산한다.
|
||||
base_z = target.copy()
|
||||
radii = radii_for(base_z)
|
||||
for _ in range(12):
|
||||
radii = radii_for(base_z)
|
||||
curves, _curve_warnings = build_curves(base_s, base_z, policy, radii)
|
||||
plan_at = evaluate(base_s, base_z, curves, base_s)
|
||||
error = target - plan_at
|
||||
error[0] = 0.0
|
||||
error[-1] = 0.0
|
||||
if float(np.max(np.abs(error))) < 1e-4:
|
||||
# 초기값 직선화(438 · 433-13②) — 기본 L 기준 R > 500 m 관 꼭짓점 묶음은 앞뒤 꺾임을 잇는
|
||||
# 직선 위(높이 한도 없음 · 요구 269-2) · 경계가 새로 R > 500 m 가 되면 묶음을 늘려 다시 ·
|
||||
# 시설 여유로 들린 관 꼭짓점은 그대로(직선화 경계).
|
||||
held = np.zeros(len(base_s), dtype=bool)
|
||||
for _ in range(len(base_s)):
|
||||
base_z = straighten_held(base_s, target, held)
|
||||
for _ in range(12):
|
||||
radii = radii_for(base_z, held)
|
||||
curves, _curve_warnings = build_curves(base_s, base_z, policy, radii)
|
||||
plan_at = evaluate(base_s, base_z, curves, base_s)
|
||||
error = target - plan_at
|
||||
error[0] = 0.0
|
||||
error[-1] = 0.0
|
||||
error[held] = 0.0
|
||||
if float(np.max(np.abs(error))) < 1e-4:
|
||||
break
|
||||
base_z = straighten_held(base_s, base_z + error, held)
|
||||
flat = flat_bends(base_s, base_z, policy.default_curve_length_m) & ~held & ~lifted
|
||||
if not flat.any():
|
||||
break
|
||||
base_z = base_z + error
|
||||
edits["curve_radii"] = radii
|
||||
held = held | flat
|
||||
edits["curve_radii"] = radii_for(base_z, held)
|
||||
|
||||
alignment = build_alignment(
|
||||
base_s=base_s,
|
||||
@@ -498,6 +512,14 @@ def design_alignment_profile(
|
||||
down_limit,
|
||||
balance_tolerance_percent=policy.balance_tolerance_percent,
|
||||
)
|
||||
# 초기값 직선화(438 · 433-13②) — 기본 L 기준 R > 500 m 꼭짓점 묶음은 앞뒤 꺾임을 잇는 직선 위
|
||||
held = np.zeros(len(base_s), dtype=bool)
|
||||
flat = flat_bends(base_s, base_z, policy.default_curve_length_m)
|
||||
while flat.any():
|
||||
held = held | flat
|
||||
straight = straighten_held(base_s, base_z, held)
|
||||
flat = flat_bends(base_s, straight, policy.default_curve_length_m) & ~held
|
||||
base_z = straighten_held(base_s, base_z, held)
|
||||
alignment = build_alignment(
|
||||
base_s=base_s,
|
||||
base_z=base_z,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""455 (433-13 ②) 폴백 첫 종단 직선화 — 관 정착 · 직선 분할도 R > 500 m 꼭짓점 없음.
|
||||
|
||||
까닭: 초기값 직선화(438)가 1순위 지반 추종 선형에만 걸려, 1순위가 실패해 2 · 3순위(관 정착 ·
|
||||
직선 분할)로 내려가면 R > 500 m 꼭짓점이 남음. 사용자 확정(269-2) — 직선화 높이 한도 없음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402
|
||||
AlignmentPolicy,
|
||||
build_curves,
|
||||
is_straight_bend,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_CurveLaw import flat_bends # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
|
||||
design_alignment_profile,
|
||||
design_pipe_anchored_profile,
|
||||
)
|
||||
|
||||
PIPES = [60.0, 125.0, 190.0, 260.0, 330.0]
|
||||
|
||||
|
||||
def _ground(s: float) -> float:
|
||||
return 100.0 + 0.06 * s + 2.5 * math.sin(s / 45.0) + 0.05 * math.sin(s / 6.0)
|
||||
|
||||
|
||||
def _longitudinal() -> dict:
|
||||
samples = [
|
||||
{"chainage_m": float(s), "elevation_m": round(_ground(s), 6), "valid": True}
|
||||
for s in range(0, 401)
|
||||
]
|
||||
stations = [{"chainage_m": float(s), "kind": "regular"} for s in range(0, 401, 20)]
|
||||
return {"samples": samples, "stations": stations}
|
||||
|
||||
|
||||
OPTIONS = GradeDesignOptions(
|
||||
max_grade_pct=14.0,
|
||||
max_reverse_grade_pct=5.0,
|
||||
min_vertical_radius_m=100.0,
|
||||
min_curve_length_m=20.0,
|
||||
min_tangent_length_m=20.0,
|
||||
vertical_curve_skip_delta_pct=0.5,
|
||||
design_speed_kph=20,
|
||||
)
|
||||
|
||||
|
||||
def _flat(alignment: dict) -> tuple[list[float], int]:
|
||||
"""(R > 500 m 꺾임 측점 · 직선 위로 옮긴 꼭짓점 수) — 438 시험과 같은 판정."""
|
||||
s = np.array([n["chainage_m"] for n in alignment["base_pvi"]], dtype=np.float64)
|
||||
z = np.array([n["elevation_m"] for n in alignment["base_pvi"]], dtype=np.float64)
|
||||
policy = AlignmentPolicy.from_dict(alignment["policy"])
|
||||
grades = np.diff(z) / np.diff(s)
|
||||
straight = np.zeros(len(s), dtype=bool)
|
||||
for i in range(1, len(s) - 1):
|
||||
straight[i] = is_straight_bend(grades[i] - grades[i - 1], s[i] - s[i - 1], s[i + 1] - s[i])
|
||||
flat = flat_bends(s, z, policy.default_curve_length_m) & ~straight
|
||||
assert all(float(c["r_m"]) <= 500.0 for c in alignment["curves"])
|
||||
return s[flat].tolist(), int(straight.sum())
|
||||
|
||||
|
||||
def test_관_정착_선형_R500_넘는_꼭짓점_없음() -> None:
|
||||
alignment, _profile = design_pipe_anchored_profile(_longitudinal(), OPTIONS, PIPES)
|
||||
flat, straight = _flat(alignment)
|
||||
assert flat == [] and 0 < straight < len(PIPES)
|
||||
# 직선 위로 옮긴 관 꼭짓점은 자동 R 을 안 남김 · 남은 꺾임은 곡선이 관 자리를 지남
|
||||
radii = alignment["edits"]["curve_radii"]
|
||||
kinks = {n["chainage_m"] for n in alignment["base_pvi"][1:-1]}
|
||||
assert set(map(float, radii)) <= kinks
|
||||
s = np.array([n["chainage_m"] for n in alignment["base_pvi"]], dtype=np.float64)
|
||||
z = np.array([n["elevation_m"] for n in alignment["base_pvi"]], dtype=np.float64)
|
||||
curves, _ = build_curves(s, z, AlignmentPolicy.from_dict(alignment["policy"]), radii)
|
||||
assert all(float(c["chainage_m"]) in kinks for c in curves)
|
||||
|
||||
|
||||
def test_직선_분할_선형_R500_넘는_꼭짓점_없음(monkeypatch) -> None:
|
||||
alignment, _profile = design_alignment_profile(_longitudinal(), OPTIONS)
|
||||
assert _flat(alignment)[0] == []
|
||||
# 풀이가 R > 500 m 꺾임(100 m · 대수차 0.5 %)을 낸 꼴 — 직선 위로 · 200 m 꺾임은 그대로
|
||||
import B05_Profile.B05_Profile_Engine_Grade_Profile as engine
|
||||
|
||||
s = np.array([0.0, 100.0, 200.0, 400.0])
|
||||
z = np.array([100.0, 103.0, 106.5, 124.5])
|
||||
monkeypatch.setattr(engine, "station_breakpoints", lambda *_a, **_k: s)
|
||||
monkeypatch.setattr(engine, "solve_alignment_elevations", lambda *_a, **_k: (z.copy(), True))
|
||||
alignment, _profile = engine.design_alignment_profile(_longitudinal(), OPTIONS)
|
||||
flat, straight = _flat(alignment)
|
||||
assert flat == [] and straight == 1
|
||||
nodes = {n["chainage_m"]: n["elevation_m"] for n in alignment["base_pvi"]}
|
||||
assert nodes[100.0] == pytest.approx(103.25)
|
||||
assert nodes[200.0] == pytest.approx(106.5)
|
||||
|
||||
|
||||
def test_부르는_곳() -> None:
|
||||
src = (ROOT / "B05_Profile" / "B05_Profile_Engine_Grade_Profile.py").read_text(encoding="utf-8")
|
||||
assert src.count("flat_bends(base_s, ") >= 3 # 지반 추종(straight_pass_through 안) 밖 둘
|
||||
|
||||
|
||||
def test_여유로_들린_관_꼭짓점은_그대로() -> None:
|
||||
"""관 여유 강제(+1.5 m)로 들린 관 꼭짓점 = 직선화 경계 · 여유 없는 관 꼭짓점만 직선 위."""
|
||||
clearances = {125.0: 1.5}
|
||||
alignment, _profile = design_pipe_anchored_profile(
|
||||
_longitudinal(), OPTIONS, PIPES, pipe_clearances=clearances
|
||||
)
|
||||
flat, straight = _flat(alignment)
|
||||
assert straight > 0
|
||||
assert flat in ([], [125.0]) # 들린 꼭짓점만 R > 500 이 남을 수 있음
|
||||
rows = {r["chainage_m"]: r["plan_elevation_m"] for r in alignment["stations"]}
|
||||
plan = np.interp(125.0, list(rows), list(rows.values()))
|
||||
assert plan > _ground(125.0) + 1.0
|
||||
Reference in New Issue
Block a user