Files
Aislo/resources/tester/test_b08_revegetation_unit.py

194 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""비탈면 녹화 ㎡ 넷 — 새심기·평떼·줄떼·비탈덮기 (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ).
근거
산림품셈 5-14 새심기(㎡당) · 5-22 평떼(떼 0.3×0.3 11매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) ·
5-23 줄떼(떼 3.33매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) · 5-25 거적덮기(㎡당) · 5-28 비탈덮기(짚망 · 매트)
판정 — 면적 = 구간 안 사면 면적(칸: 절토면/성토면/양쪽 · 기본값 없음) · 초류종자 살포는 공법에서 뺌
(토공집계가 5-24 로 사면 전체를 셈) · 단 직고·선떼 급수 칸은 기본값 없음 · 길이형은 다음 차례.
"""
from __future__ import annotations
import pytest
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import (
revegetation_from_designs,
)
AREAS = {"b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0}
def _row(**options) -> dict:
structure = {
"structure_id": "g1",
"type_id": "revegetation",
"start_m": 0.0,
"end_m": 20.0,
"options": {**AREAS, **options},
}
table = build_table([structure], {"revegetation": "비탈면 녹화"})
return table
def _parts(table: dict) -> dict:
row = next(
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "녹화" in r["name"]
)
return {part["code"]: part for part in row["composite_parts"] or []}
def test_평떼_양쪽은_면적과_떼_11매() -> None:
table = _row(method="평떼", face="양쪽")
row = table["structures"][0]
got = {(c["name"], c["unit"]): c["amount"] for c in row["components"]}
assert (row["billing_unit"], row["billing_quantity"]) == ("㎡", pytest.approx(100.0))
assert got[("평떼", "㎡")] == pytest.approx(100.0)
assert got[("떼", "매")] == pytest.approx(1100.0)
text = " ".join(row["notes"])
# 병행시공(임도기술교본 7장 · 울진1공구) — 흠이 아니라 정보(⚠ 안 붙임 · 안티그래비티 확인)
assert row["notes"][0].startswith("녹화와 초류종자살포는 병행시공(임도기술교본 7장)")
assert "떼 면적을 파종에서 빼지 않음" in text and "⚠" not in row["notes"][0]
assert "5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈" in text
parts = _parts(table)
assert parts["FP-05-22-02"]["quantity"] == pytest.approx(100.0)
assert parts["FP-05-22-03"]["quantity"] == pytest.approx(100.0)
def test_줄떼_절토면은_3_33매() -> None:
table = _row(method="줄떼", face="절토면")
got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]}
assert got[("줄떼", "㎡")] == pytest.approx(40.0)
assert got[("떼", "매")] == pytest.approx(40.0 * 3.33)
assert set(_parts(table)) == {"FP-05-23-02", "FP-05-23-03"}
def test_새심기_성토면() -> None:
table = _row(method="새심기", face="성토면")
assert _parts(table)["FP-05-14"]["quantity"] == pytest.approx(60.0)
def test_비탈덮기는_덮개를_골라야_선다() -> None:
table = _row(method="비탈덮기", face="양쪽", cover_kind="거적")
assert _parts(table)["FP-05-25"]["quantity"] == pytest.approx(100.0)
unset = _row(method="비탈덮기", face="양쪽")["structures"][0]
assert not unset["components"] and any("덮개" in n for n in unset["notes"])
def test_면을_안_고르면_사유() -> None:
row = _row(method="평떼")["structures"][0]
assert not row["components"] and any("절토면" in n and "성토면" in n for n in row["notes"])
def test_조공은_아직_산출식이_없다() -> None:
row = _row(method="조공", face="양쪽")["structures"][0]
assert not row["components"]
assert any("산출식 아직 없음" in n for n in row["notes"])
SOD = {
"method": "선떼붙이기",
"face": "양쪽",
"step_height_m": 1.5,
"sod_grade": "5급",
"sod_size": "대형 40×40",
"cutting_soil": "보통토사",
# 구간 안 사면 낙차 × 거리(m·m) — 절토 30 · 성토 60 ⇒ 연장 = 90 ÷ 1.5 = 60m
"b06_cut_rise_area_m2": 30.0,
"b06_fill_rise_area_m2": 60.0,
}
def test_선떼는_낙차_나누기_단_직고로_연장_표_2_2_6_매수() -> None:
"""품셈 5-16-1 [주]② 연장 = 대상지 직고 ÷ 단 직고 × 연장 · 표 2-2-6 표 값(식 아님)."""
table = _row(**SOD)
row = table["structures"][0]
got = {(c["name"], c["unit"]): c for c in row["components"]}
assert (row["billing_unit"], row["billing_quantity"]) == ("m", pytest.approx(60.0))
assert got[("선떼붙이기", "m")]["amount"] == pytest.approx(60.0)
# 5급 대형 표 값 7.80매/m(단면식 3.0 × 2.5 = 7.50 과 어긋남 — 표가 이김)
assert got[("떼", "매")]["amount"] == pytest.approx(60.0 * 7.80)
assert got[("떼", "매")]["spec"] == "40×40"
assert got[("떼붙임", "㎡")]["amount"] == pytest.approx(60.0 * 7.80 * 0.16)
text = " ".join(row["notes"])
assert "5급 대형 표 7.80 ↔ 단면식 7.50" in text and "소형 2급 표 27.0 ↔ 단면 4.0" in text
parts = _parts(table)
assert parts["FP-05-16-01#보통토사"]["quantity"] == pytest.approx(60.0)
# 떼붙임은 수량만 — 호표가 없어 막힘 + 사유(금액 안 섬)
assert parts["FP-05-16-02"]["quantity"] == pytest.approx(60.0 * 7.80 * 0.16)
assert parts["FP-05-16-02"]["not_ready"]
assert "떼붙임 호표 없음" in parts["FP-05-16-02"]["why"]
def test_선떼_소형_2급은_표_값_27() -> None:
table = _row(**{**SOD, "sod_grade": "2급", "sod_size": "소형 18×18"})
got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]}
assert got[("떼", "매")] == pytest.approx(60.0 * 27.0)
assert got[("떼붙임", "㎡")] == pytest.approx(60.0 * 27.0 * 0.18 * 0.18)
@pytest.mark.parametrize("missing", ["step_height_m", "sod_grade", "sod_size"])
def test_선떼_칸이_비면_사유(missing: str) -> None:
row = _row(**{k: v for k, v in SOD.items() if k != missing})["structures"][0]
assert not row["components"] and row["notes"]
def test_단끊기_토질을_안_고르면_그_조각만_막힘() -> None:
table = _row(**{k: v for k, v in SOD.items() if k != "cutting_soil"})
part = _parts(table)["FP-05-16-01"]
assert part["not_ready"] and "토질" in part["why"]
def test_구간_안_사면_면적은_B06_측점에서_보간() -> None:
designs = [
{"chainage_m": 0.0, "design": {}},
{"chainage_m": 20.0, "design": {}},
]
from B08_Quantity.B08_Quantity_Engine_SlopeLength import StationSlope
slopes = [
StationSlope(chainage_m=0.0, cut_length_m=2.0, fill_length_m=4.0),
StationSlope(chainage_m=20.0, cut_length_m=4.0, fill_length_m=0.0),
]
structure = {"type_id": "revegetation", "start_m": 5.0, "end_m": 20.0, "options": {}}
filled = revegetation_from_designs(structure, designs, slopes)
# 절토 5→2.5, 20→4 : (2.5+4)/2×15 · 성토 5→3, 20→0 : (3+0)/2×15
assert filled["options"]["b06_cut_area_m2"] == pytest.approx(48.75)
assert filled["options"]["b06_fill_area_m2"] == pytest.approx(22.5)
def test_구간_안_사면_낙차_면적도_B06_측점에서() -> None:
from B08_Quantity.B08_Quantity_Engine_SlopeLength import SlopeSegment, StationSlope
def seg(role: str, rise: float) -> SlopeSegment:
return SlopeSegment("left", role, 0.0, 1.0, rise, 1.0, 1.0)
slopes = [
StationSlope(chainage_m=0.0, segments=(seg("cut", 2.0), seg("fill", -4.0))),
StationSlope(chainage_m=20.0, segments=(seg("cut", 4.0), seg("cut", 1.0))),
]
structure = {"type_id": "revegetation", "start_m": 0.0, "end_m": 20.0, "options": {}}
options = revegetation_from_designs(structure, [], slopes)["options"]
assert options["b06_cut_rise_area_m2"] == pytest.approx((2.0 + 5.0) / 2 * 20)
assert options["b06_fill_rise_area_m2"] == pytest.approx((4.0 + 0.0) / 2 * 20)
def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인다() -> None:
options = {o.key: o for o in structure_type_map()["revegetation"].options}
assert "초류종자 살포" not in options["method"].choices
assert "토공집계" in (options["method"].default_basis or "")
assert (
options["face"].choices == ["절토면", "성토면", "양쪽"] and options["face"].default is None
)
for key in ("step_height_m", "sod_grade", "sod_size", "cutting_soil"):
assert options[key].default is None and not options[key].not_in_table
assert options["sod_size"].choices == ["대형 40×40", "소형 18×18"]
# 단끊기 토질은 품셈 5-16-1 표 갈래 이름 그대로(B06 지반 프리셋과 다른 축)
assert options["cutting_soil"].choices == [
"보통토사",
"경질ㆍ고사점토 및 자갈섞인 점토",
"호박돌 섞인 토사",
]