- 남은 차이의 주인 = 초안 상세 지반 표본: 설계는 넓힌 표본으로 셈했는데 상세엔 20 m 표본을 실어 관 측점 구조물 면적이 잘린 지반으로 닫힘(검증_sub2 805 측점 성토 27.5 대 63.4 ㎡) — cross 단계 결과에 넓힌 표본을 싣고 초안 상세가 그 표본을 씀(서버도 넓힌 표본을 상세에 남김) - 초안 상세 제원 누가거리 = 새 측점(서버 부착과 같음) - 새 짝 logic/B06_Section_Cross_Enforce — 포장 구간 · 물넘이 기울임 · 세월교 하강 = 새 노선 관 · 구조물 자리로 판정(`pavement_ranges` · `tilt_ranges` · `ford_surface_drops` 짝) · 사토장 맨 뒤 기준 측점 하나 · 용량 이분법(`enforce_spoil_fills` 짝) · 파이썬 세 파일 머리에 짝 표시 - 검증_sub2 실측(서버 체인 실제로 돌리고 일부러 실패시켜 되살림): 25+0 부근 꺾임점 끌기 미리보기 -12,816.9 대 정본 -12,817.1 · 세월교 월류 0.22 · 0.29 넣고 -12,788.7 대 -12,789.0 · 끌기 한 번 103 ms - 시험: test_349_0n_cross_enforce_mirror 5 · test_349_0j 표본 1 더함 · 관련 114 통과 · typecheck 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
184 lines
7.1 KiB
Python
184 lines
7.1 KiB
Python
"""PLAN 349-0j — 노선 초안 미리보기 누가토량 대 [저장] 정본.
|
|
|
|
갈린 자리(검증_sub2 · 2번째 꺾임점 서쪽 20 m · 서버 체인을 실제로 돌리고 체인 스스로 되살림):
|
|
미리보기 = 서버 이월 직후(보정 전)와 측점 · 계획고 · 면적이 같음(-11,378.5 대 -11,391.3 ㎥)
|
|
· [확정] 보정 중 미교차 넓힘(표본 20 m → 지표면 끝) -9,531 ㎥ · 관 구조물 면적 -334 ㎥(넓힌 뒤 기준 1,971 ㎥).
|
|
· cross 단계에 설계 지형 격자를 넘기면 미교차 측점을 서버와 같은 규칙(`extendUnclosed` ↔
|
|
`extend_unclosed`)으로 넓힘 — 넓힌 끝 · 면적 · 누가토량이 서버 엔진과 같음
|
|
· 격자를 안 넘기면(지금 묶음) 넓힘 없음 — 서버와 갈림
|
|
· 초안 상세의 설계 = cross 단계 결과의 사본(구조물 면적 얹기가 단계 기억을 안 고침)
|
|
짝: `helper_349_0j_draft_vs_saved.cjs` · 지형 · 노선 · 관 틀 = `test_346_2_pipeline_cross_page.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ROOT = HERE.parents[1]
|
|
for path in (ROOT, HERE):
|
|
if str(path) not in sys.path:
|
|
sys.path.insert(0, str(path))
|
|
|
|
from B05_Profile.B05_Profile_Engine_Grade import resolve_grade_options # noqa: E402
|
|
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
|
|
design_ground_following_profile,
|
|
)
|
|
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
|
from B06_Section.B06_Section_Engine_SampleExtend import extend_unclosed # noqa: E402
|
|
from common_util.common_util_surface_sampler import DtmGridSampler, dtm_grid_bytes # noqa: E402
|
|
from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402
|
|
from test_346_2_pipeline_cross_page import ( # noqa: E402
|
|
INTERVAL_M,
|
|
MIN_RADIUS_M,
|
|
ONES,
|
|
_fake,
|
|
_line,
|
|
_pipes,
|
|
_real,
|
|
_saved_detail,
|
|
_sections,
|
|
_types,
|
|
)
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
|
reason="프론트엔드 의존성 미설치",
|
|
)
|
|
|
|
|
|
def _server(ts: dict, sampler) -> list[dict]:
|
|
"""[저장] 정본 길 — 같은 노선 서버 측점 · 지반 → 엔진 설계 → 미교차 넓힘(확정 보정)."""
|
|
pipes = [
|
|
{"id": s["key"].split(":", 1)[1], "chainage_m": s["chainage"], "source": "user"}
|
|
for s in ts["stations"]
|
|
if s["key"].startswith("pipe:")
|
|
]
|
|
data = _sections(ts["line"], sampler, pipes, ts["curves"])
|
|
out = []
|
|
for a, b in zip(ts["stations"], data["cross_sections"]):
|
|
assert abs(a["chainage"] - b["chainage_m"]) < 1e-3
|
|
|
|
def compute(samples, a=a, b=b):
|
|
return compute_cross_design(
|
|
samples,
|
|
a["elevation"],
|
|
ground_type="ripping_rock",
|
|
section_mode=f"{b.get('uphill_side') or 'left'}_cut",
|
|
plan_radius_m=b.get("plan_radius_m"),
|
|
curve_outer_side=b.get("curve_outer_side"),
|
|
curve_widening_m=b.get("curve_widening_m"),
|
|
)
|
|
|
|
design = compute(b["samples"])
|
|
wide = extend_unclosed(b["samples"], b["frame"], compute, sampler)
|
|
if wide is not None and (wide[1]["left_m"] or wide[1]["right_m"]):
|
|
design = compute(wide[0])
|
|
line = design["design_line"]
|
|
out.append(
|
|
{
|
|
"chainage": b["chainage_m"],
|
|
"cut": design["cut_area_m2"],
|
|
"fill": design["fill_area_m2"],
|
|
"lineEnds": [line[0]["offset_m"], line[-1]["offset_m"]],
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def _final(rows) -> float:
|
|
nets = [(r["chainage"], r["cut"] - r["fill"]) for r in rows]
|
|
return sum((n0 + n1) / 2 * (c1 - c0) for (c0, n0), (c1, n1) in zip(nets, nets[1:]))
|
|
|
|
|
|
@pytest.fixture(scope="module", params=[_fake, _real], ids=["가짜", "검증_sub7"])
|
|
def r(request) -> dict:
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
npz, nodes = request.param(Path(folder))
|
|
line = _line(nodes)
|
|
sampler = DtmGridSampler.from_npz(npz)
|
|
pipes = _pipes(line)
|
|
saved = _sections(line, sampler, pipes)
|
|
options = resolve_grade_options("trunk", terrain_type="normal", paved=False, stored=None)
|
|
policy = design_ground_following_profile(saved["longitudinal"], options)[0]["policy"]
|
|
job = {
|
|
"nodes": nodes,
|
|
"line": line,
|
|
"minRadius": MIN_RADIUS_M,
|
|
"policy": policy,
|
|
"types": _types(),
|
|
"interval": INTERVAL_M,
|
|
"pipes": pipes,
|
|
"standard": STANDARD_CROSS_SECTION,
|
|
"conversion": ONES,
|
|
"detail": _saved_detail(saved),
|
|
"haulDelayMs": 20,
|
|
"drags": [{"index": 1, "dx": -20.0, "dy": 0.0}],
|
|
}
|
|
path = Path(folder) / "job.json"
|
|
path.write_text(json.dumps(job), encoding="utf-8")
|
|
grid = Path(folder) / "grid.bin"
|
|
grid.write_bytes(dtm_grid_bytes(npz))
|
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
|
["node", str(HERE / "helper_349_0j_draft_vs_saved.cjs"), str(path), str(grid)],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
timeout=180,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
out = json.loads(proc.stdout)
|
|
out["server"] = _server(out["wide"], sampler)
|
|
out["kind"] = request.param.__name__
|
|
return out
|
|
|
|
|
|
def test_격자를_넘기면_서버와_같은_넓힘_면적(r):
|
|
wide, server = r["wide"], r["server"]
|
|
assert wide["outcome"] == "done"
|
|
assert len(wide["stations"]) == len(server)
|
|
for a, b in zip(wide["stations"], server):
|
|
assert a["lineEnds"] == pytest.approx(b["lineEnds"]), a["key"]
|
|
assert abs(a["cut"] - b["cut"]) < 0.05 + 0.01 * b["cut"], a["key"]
|
|
assert abs(a["fill"] - b["fill"]) < 0.05 + 0.01 * b["fill"], a["key"]
|
|
|
|
|
|
def test_미리보기_누가토량_서버_정본과_같음(r):
|
|
want = _final(r["server"])
|
|
got = r["wide"]["final"]
|
|
assert got is not None
|
|
assert abs(got - want) <= max(1.0, abs(want) * 0.005), (got, want)
|
|
|
|
|
|
def test_격자_없으면_넓힘_없음(r):
|
|
plain, wide = r["plain"], r["wide"]
|
|
widened = [
|
|
s["key"] for s in wide["stations"] if s["lineEnds"] and max(map(abs, s["lineEnds"])) > 20.01
|
|
]
|
|
assert all(
|
|
s["lineEnds"] is None or max(map(abs, s["lineEnds"])) <= 20.01 for s in plain["stations"]
|
|
)
|
|
if r["kind"] == "_real": # 실제 지형 — 미교차가 있어 넓힘이 누가토량을 바꿈
|
|
assert widened
|
|
assert abs(plain["final"] - wide["final"]) > 1.0
|
|
else:
|
|
assert plain["final"] == pytest.approx(wide["final"]) or widened
|
|
|
|
|
|
def test_초안_상세_표본은_넓힌_표본(r):
|
|
widened = [
|
|
s for s in r["wide"]["stations"] if s["lineEnds"] and max(map(abs, s["lineEnds"])) > 20.01
|
|
]
|
|
assert r["wide"]["shownWide"] == len(widened) # 349-0n — 서버도 넓힌 표본을 상세에 남김
|
|
assert r["plain"]["shownWide"] == 0
|
|
|
|
|
|
def test_초안_상세_설계는_사본(r):
|
|
assert r["wide"]["copied"] is True
|