feat(B08): 수리집수면적표 박스암거 사각 단면 장 · 세월교 · 물넘이 월류 높이 장(73-11 · 73-12)

- 박스 = 내공 B · H · 수심비(기본 0.8) · n(기본 0.013) · A = B·h · P = B + 2h · Manning · Qd 판정
- 세월교 · 물넘이포장 = 월류 폭(기본 10 m) · 경사(1/10) · n(0.017) → 월류 높이 자동(ford_depth)
- ford_depth = fordHeightM 파이썬 짝(두 파일 머리 · 거울 시험 test_73_11) · 관 몫 안 뺌 · 여유 없음
- [저장]에 새 칸 · 빈 장 없앰 · 물넘이포장은 관 장에서 옮김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
This commit is contained in:
2026-10-07 09:09:25 +09:00
co-authored by Claude Opus 5.5
parent baa2438116
commit dc2c27313c
8 changed files with 356 additions and 93 deletions
@@ -0,0 +1,125 @@
"""B08 수리집수면적표 — 박스암거 사각 단면 장(73-11) · 세월교 · 물넘이 월류 높이 장(73-12).
못박는 것
- 장 = 시설이 정함(박스 · 세월교 · 물넘이포장 · 관)
- 박스 h = 수심비 × H · A = B·h · P = B + 2h · 울진 1공구 B 3.0 · H 1.5 → h 1.2 · A 3.6 · P 5.4
- 박스 n · 수심비 · 세월교 n · 월류 폭 · 경사 기본값 · 고치면 계산이 따라감
- 월류 높이 = TS `fordHeightM` 거울(helper_65_ford_box 표) · 폭 바꾸면 높이 바뀜
- [저장] 모델이 새 칸을 받음
"""
from __future__ import annotations
import json
import math
import os
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Hydro import compute, ford_depth # noqa: E402
from B08_Quantity.B08_Quantity_Hydro_Defaults import ( # noqa: E402
DEFAULTS,
SHEET_DEFAULTS,
station_rows,
)
from B08_Quantity.B08_Quantity_Router_Hydro import HydroSave # noqa: E402
from config.config_system import FORD_MANNING_N, FORD_SLOPE # noqa: E402
HERE = os.path.dirname(os.path.abspath(__file__))
HYDRO = {"area_km2": 0.5, "length_m": 800.0, "height_m": 200.0, "r24_mm": 300.0}
def _inputs(sheet: str, **extra) -> dict:
return {**DEFAULTS, **SHEET_DEFAULTS[sheet], "sheet": sheet, **HYDRO, **extra}
def test_장은_시설이_정함() -> None:
class P:
def __init__(self, c: float, f: str) -> None:
self.chainage_m, self.facility = c, f
rows = station_rows(
[P(3, "ford_pavement"), P(1, "pipe"), P(2, "box_culvert"), P(4, "ford_bridge")]
)
assert [(r["sheet"], r["kind"]) for r in rows] == [
("pipe", ""),
("box", "박스암거"),
("ford", "물넘이포장"),
("ford", "세월교"),
]
def test_박스_울진_산출서_단면() -> None:
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] == 0.8
assert r["n"] == 0.013
assert r["depth"] == pytest.approx(1.2)
assert r["section_area"] == pytest.approx(3.6)
assert r["perimeter"] == pytest.approx(5.4)
assert r["radius"] == pytest.approx(3.6 / 5.4)
# 실무 29.236 = 1/n 75 · R 0.67 반올림 — 같은 n 이면 R 반올림 차(0.3 %)만 남음
r75 = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, n=1 / 75))
assert r75["capacity"] == pytest.approx(29.236, rel=0.004)
assert 75 * 0.67 ** (2 / 3) * math.sqrt(1 / 50) * 3.6 == pytest.approx(29.236, abs=0.001)
assert r["capacity"] > r75["capacity"] # n 고치면 따라감
assert r["ok"] is (r["capacity"] >= r["design_flow"])
deeper = compute(
_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, depth_ratio=0.9)
)
assert deeper["depth"] == pytest.approx(1.35)
def test_박스_내공_경사_빈칸이면_대시() -> None:
r = compute(_inputs("box"))
assert r["design_flow"] is not None
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
@pytest.fixture(scope="module")
def ts_table() -> list:
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", os.path.join(HERE, "helper_65_ford_box.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)["table"]
def test_월류_높이_TS_거울(ts_table) -> None:
assert SHEET_DEFAULTS["ford"]["n"] == FORD_MANNING_N
assert 1 / SHEET_DEFAULTS["ford"]["slope_denominator"] == pytest.approx(FORD_SLOPE)
assert SHEET_DEFAULTS["ford"]["overflow_width_m"] == 10.0
for q, b, h in ts_table:
assert ford_depth(q, b, FORD_MANNING_N, FORD_SLOPE) == h, (q, b)
def test_세월교_폭_10_높이_자동_폭_바꾸면_바뀜(ts_table) -> None:
r = compute(_inputs("ford"))
qd = r["design_flow"]
assert r["depth"] == ford_depth(qd, 10.0, FORD_MANNING_N, FORD_SLOPE)
assert r["section_area"] == pytest.approx(10.0 * r["depth"])
assert r["capacity"] >= qd and r["ok"] is True
narrow = compute(_inputs("ford", overflow_width_m=5.0))
assert narrow["depth"] > r["depth"]
rough = compute(_inputs("ford", n=0.025))
assert rough["depth"] > r["depth"]
assert compute(_inputs("ford", overflow_width_m=None))["depth"] is None
def test_저장_모델이_새_칸을_받음() -> None:
box = _inputs("box", box_width_m=3.0, box_height_m=1.5, depth_ratio=0.8)
ford = _inputs("ford", overflow_width_m=12.0)
saved = HydroSave(stations={"1.000": box, "2.000": ford}).model_dump()["stations"]
assert saved["1.000"]["sheet"] == "box" and saved["1.000"]["box_height_m"] == 1.5
assert saved["2.000"]["sheet"] == "ford" and saved["2.000"]["overflow_width_m"] == 12.0
with pytest.raises(ValueError):
HydroSave(stations={"1.000": {**box, "depth_ratio": 1.2}})
@@ -1,4 +1,4 @@
"""B08 수리집수면적표 — 박스암거 · 세월교 지점은 빈 장(계산 방법 정하는 중 · PLAN 73-1)."""
"""B08 수리집수면적표 — 박스암거 · 세월교 지점은 따로 장(73-1 빈 장 → 73-11 · 73-12 계산 장)."""
from __future__ import annotations
@@ -29,9 +29,9 @@ def test_박스_세월교만_빈_장_탭_수는_그_구조물_수() -> None:
]
rows = station_rows(points)
assert [r["chainage_m"] for r in rows] == [85.0, 173.0, 300.0, 352.0, 400.0]
pending = [r for r in rows if r["pending"]]
assert len(pending) == 3 # 세월교 2 · 박스 1
assert {r["kind"] for r in pending} == {"세월교", "박스암거"}
# 관 · 물넘이포장은 지금 관 계산 장 그대로
assert [r["facility"] for r in rows if not r["pending"]] == ["pipe", "ford_pavement"]
assert all(r["kind"] == "" for r in rows if not r["pending"])
# 73-11 · 73-12 뒤 — 빈 장 대신 박스 · 월류 장(물넘이포장도 월류 장)
special = [r for r in rows if r["sheet"] != "pipe"]
assert len(special) == 4 # 세월교 2 · 박스 1 · 물넘이 1
assert {r["kind"] for r in special} == {"세월교", "박스암거", "물넘이포장"}
assert [r["facility"] for r in rows if r["sheet"] == "pipe"] == ["pipe"]
assert all(r["kind"] == "" for r in rows if r["sheet"] == "pipe")