Files
Aislo/resources/tester/test_b06_spoil_fill_allocation.py
T
eomsangdonandClaude Opus 5 0ef32b5279 chore(tester): 시험을 resources/tester/ 로 옮김 — 창끼리 건너가게
⚠ **뿌리** — `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>
2026-09-09 17:12:30 +09:00

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)