⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
189 lines
7.2 KiB
Python
189 lines
7.2 KiB
Python
"""사토장 — 용량에서 폭을 정해 측점마다 단면을 세운다 (2026-09-09).
|
|
|
|
⚠ 잠그는 것
|
|
① 용량이 커질수록 폭이 넓어지고, 상한에서 멈추면 못 담은 몫이 값으로 남는다.
|
|
② 사토장이 선 측점은 **노선 성토가 줄고** 그만큼이 사토장 몫으로 간다(두 번 세지 않기).
|
|
③ 「자동」이면 그 측점의 **성토 쪽**에 선다.
|
|
④ 구간 밖 측점은 안 건드린다 — **새 측점을 만들지 않는다**.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402
|
|
from B06_Section.B06_Section_Engine_SpoilFill import enforce_spoil_fills # noqa: E402
|
|
|
|
|
|
def _samples() -> list[dict]:
|
|
"""우측(-offset)이 높고 좌측(+offset)이 낮은 지반 — 좌가 성토 쪽이다."""
|
|
return [
|
|
{"offset_m": round(offset, 3), "elevation_m": round(100.0 - 0.5 * offset, 4), "valid": True}
|
|
for offset in [x * 0.5 for x in range(-30, 31)]
|
|
]
|
|
|
|
|
|
def _sections(chainages: list[float]) -> list[dict]:
|
|
return [
|
|
{
|
|
"chainage_m": chainage,
|
|
"samples": _samples(),
|
|
"design": compute_cross_design(
|
|
_samples(),
|
|
99.8,
|
|
ground_type="soil",
|
|
section_mode="right_cut",
|
|
ditch_side="right",
|
|
),
|
|
}
|
|
for chainage in chainages
|
|
]
|
|
|
|
|
|
_LONGITUDINAL = {
|
|
"design_profiles": [
|
|
{"samples": [{"chainage_m": float(c), "elevation_m": 99.8} for c in range(0, 201, 20)]}
|
|
]
|
|
}
|
|
|
|
|
|
def _write_site(root: Path, capacity_m3: float, **options) -> None:
|
|
(root / "B05_Profile" / "route").mkdir(parents=True, exist_ok=True)
|
|
(root / "B05_Profile" / "route" / "structures.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"revision": 1,
|
|
"structures": [
|
|
{
|
|
"structure_id": "sp-1",
|
|
"type_id": "spoil_bank",
|
|
"placement": "interval",
|
|
"start_m": 100.0,
|
|
"end_m": 160.0,
|
|
"options": {"capacity_m3": capacity_m3, **options},
|
|
}
|
|
],
|
|
},
|
|
ensure_ascii=False,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _run(root: Path, capacity_m3: float, **options) -> list[dict]:
|
|
_write_site(root, capacity_m3, **options)
|
|
sections = _sections([80.0, 100.0, 120.0, 140.0, 160.0, 180.0])
|
|
enforce_spoil_fills(_LONGITUDINAL, sections, root)
|
|
return sections
|
|
|
|
|
|
def _placed(sections: list[dict]) -> float:
|
|
inside = [s for s in sections if 100.0 <= s["chainage_m"] <= 160.0]
|
|
return float(inside[0]["design"].get("spoil_fill_placed_m3") or 0.0)
|
|
|
|
|
|
def test_용량이_클수록_폭이_넓어진다(tmp_path: Path) -> None:
|
|
small = _run(tmp_path, 200.0)
|
|
large = _run(tmp_path, 800.0)
|
|
narrow = small[1]["design"]["spoil_fill_width_m"]
|
|
wide = large[1]["design"]["spoil_fill_width_m"]
|
|
assert 0 < narrow < wide
|
|
assert _placed(small) == pytest.approx(200.0, rel=0.02)
|
|
assert _placed(large) == pytest.approx(800.0, rel=0.02)
|
|
|
|
|
|
def test_담을_수_없으면_남은_몫이_값으로_남는다(tmp_path: Path) -> None:
|
|
"""상한에서 멈춘다 — 임의로 더 넓히지 않는다."""
|
|
sections = _run(tmp_path, 1_000_000.0)
|
|
design = sections[1]["design"]
|
|
assert design["spoil_fill_unplaced_m3"] > 0
|
|
assert design["spoil_fill_width_m"] == pytest.approx(design["spoil_fill_max_width_m"])
|
|
|
|
|
|
def test_노선_성토가_그만큼_줄어든다(tmp_path: Path) -> None:
|
|
"""같은 흙을 두 번 세지 않는다(확정 ㉠)."""
|
|
plain = _sections([120.0])[0]["design"]
|
|
sections = _run(tmp_path, 400.0)
|
|
inside = sections[2]["design"]
|
|
assert inside["spoil_fill_area_m2"] > 0
|
|
assert inside["spoil_fill_replaced_fill_m2"] > 0
|
|
assert inside["fill_area_m2"] == pytest.approx(
|
|
plain["fill_area_m2"] - inside["spoil_fill_replaced_fill_m2"], abs=1e-6
|
|
)
|
|
|
|
|
|
def test_자동이면_성토_쪽에_선다(tmp_path: Path) -> None:
|
|
sections = _run(tmp_path, 400.0, side="자동(성토 쪽)")
|
|
# right_cut = 우측 절토 ⇒ 성토는 좌측(+offset).
|
|
assert sections[1]["design"]["spoil_fill_side"] == "left"
|
|
|
|
|
|
def test_구간_밖_측점은_안_건드린다(tmp_path: Path) -> None:
|
|
"""새 측점을 만들지 않고, 구간 밖은 종전 그대로다(확정 ③)."""
|
|
sections = _run(tmp_path, 400.0)
|
|
assert len(sections) == 6
|
|
for section in sections:
|
|
design = section["design"]
|
|
if 100.0 <= section["chainage_m"] <= 160.0:
|
|
assert design["spoil_fill_area_m2"] > 0
|
|
else:
|
|
assert design.get("spoil_fill_area_m2", 0) == 0
|
|
|
|
|
|
def test_용량이_없으면_아무것도_안_세운다(tmp_path: Path) -> None:
|
|
(tmp_path / "B05_Profile" / "route").mkdir(parents=True, exist_ok=True)
|
|
(tmp_path / "B05_Profile" / "route" / "structures.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"revision": 1,
|
|
"structures": [
|
|
{
|
|
"structure_id": "sp-1",
|
|
"type_id": "spoil_bank",
|
|
"placement": "interval",
|
|
"start_m": 100.0,
|
|
"end_m": 160.0,
|
|
"options": {},
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
sections = _sections([120.0])
|
|
assert enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path) == 0
|
|
assert sections[0]["design"]["spoil_fill_area_m2"] == 0
|
|
|
|
|
|
def test_사토장을_지우면_값도_사라진다(tmp_path: Path) -> None:
|
|
"""⚠ 화면 실측으로 잡은 자리(2026-09-09) — 얹기만 하고 **지우지 않아** 구조물을 없앤 뒤에도
|
|
횡단도에 계속 그려지고 면적표에도 서 있었다.
|
|
"""
|
|
sections = _run(tmp_path, 400.0)
|
|
inside = sections[2]["design"]
|
|
assert inside["spoil_fill_area_m2"] > 0
|
|
plain_fill = _sections([120.0])[0]["design"]["fill_area_m2"]
|
|
|
|
# 구조물을 지운다 — 빈 목록으로 덮어쓴다.
|
|
(tmp_path / "B05_Profile" / "route" / "structures.json").write_text(
|
|
json.dumps({"revision": 2, "structures": []}, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
changed = enforce_spoil_fills(_LONGITUDINAL, sections, tmp_path)
|
|
assert changed > 0, "지운 사토장을 되돌리지 않았다"
|
|
for section in sections:
|
|
design = section["design"]
|
|
# 칸은 남아도 **값이 0** 이어야 한다 — 설계 결과는 사토장이 없어도 칸을 늘 싣는다.
|
|
assert float(design.get("spoil_fill_area_m2") or 0) == 0, section["chainage_m"]
|
|
assert float(design.get("spoil_fill_width_m") or 0) == 0, section["chainage_m"]
|
|
assert design.get("spoil_fill_capacity_m3") in (None, 0), section["chainage_m"]
|
|
# 노선 성토도 원래 값으로 돌아온다 — 사토장 몫을 빼 두었던 것이 복구된다.
|
|
assert sections[2]["design"]["fill_area_m2"] == pytest.approx(plain_fill, abs=1e-6)
|