Files
Aislo/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py
T
eomsangdonandClaude Opus 5 0d4f46ecbe feat(b05·b08): 비탈면 녹화 ㎡ 넷(새심기·평떼·줄떼·비탈덮기) — 구간 사면 면적 × 대상 면 칸 · 떼 11매/3.33매(품셈 5-22-1·5-23-1) · 초류종자 살포는 공법에서 빼고 칸 밑 까닭 · 단 직고·선떼 급수 칸 기본값 없음(12장 D 녹화 · 브레인 ⓐ~ⓓ)
- 겹친 사면(초류종자살포·면고르기)은 자동으로 안 빼고 맨 앞 사유 · 평떼 떼꽂이 5-22-3 [주] 읽기 사유
- 혼합석 사유 정정: 토사 = B06 측점 토사 토글(구성비는 암 몫만) · 사전터파기 「산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음」
- UnitQuantity 700줄 — 늦은 import 를 한 벌(_lazy)로

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 05:30:32 +09:00

143 lines
6.7 KiB
Python
Raw 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 새심기(㎡당 · 1㎡당 10주) · 5-22 평떼(5-22-1 떼 0.3×0.3 11매/㎡ · 5-22-2 붙임 ㎡ ·
5-22-3 떼꽂이 ㎡) · 5-23 줄떼(5-23-1 떼 3.33매/㎡ · 5-23-2 붙임 · 5-23-3 떼꽂이) ·
5-25 거적덮기(㎡당) · 5-28 비탈덮기(5-28-1 짚망 · 5-28-2 방초·야자섬유매트)
교본 7-1 그림 7-8~7-12 는 사진·개념도(치수 없음) — 표준도가 아님.
⭐ 면적 = 구간 안 사면 면적(B06 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽).
⚠ 길이형(선떼·조공)은 다음 차례(선떼 = 사방기술교본 표 2-2-6 급별 m당 매수) · 나머지 공법은 사유.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import Component, _num, routed
CUT, FILL = "b06_cut_area_m2", "b06_fill_area_m2"
FACES = {"절토면": (CUT,), "성토면": (FILL,), "양쪽": (CUT, FILL)}
#: 공법 → (성분 이름, 떼 매수/㎡ 또는 None, 근거). 떼 매수 = 품셈 5-22-1·5-23-1 「소요 매수」.
AREA_METHODS: dict[str, tuple[str, float | None, str]] = {
"새심기": ("새심기", None, "품셈 5-14 새심기(㎡당 · 1㎡당 10주 · 요소·인산은 표 안)"),
"평떼": ("평떼", 11.0, "품셈 5-22 평떼 — 떼 0.3×0.3 11매/㎡ · 붙임 5-22-2 · 떼꽂이 5-22-3"),
"줄떼": (
"줄떼",
3.33,
"품셈 5-23 줄떼 — 떼 0.3×0.3(30㎝ 간격) 3.33매/㎡ · 붙임 5-23-2 · 떼꽂이 5-23-3",
),
"비탈덮기": (
"비탈덮기",
None,
"품셈 5-25 거적덮기 · 5-28 비탈덮기(짚망·매트) — 덮개가 공종을 가름",
),
}
SOD_SPEC = "0.3×0.3"
LENGTH_METHODS = ("선떼붙이기", "조공")
NOTE_LENGTH = (
"{method}은(는) 길이형(10m·100m당) — 산출식 아직 없음 · 떼 매수는 품셈이"
" 「사방기술교본 기준표」라 함(선떼 = 표 2-2-6 급별 m당 매수 · 다음 차례)"
" · 단 직고·급수 칸은 기본값 없음(브레인 ⓒ)"
)
NOTE_LATER = (
"{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)"
)
NOTE_FACE = "대상 면을 안 고름 — 절토면/성토면/양쪽 중 고를 것(공법마다 달라 기본값 없음)"
NOTE_COVER = (
"비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것"
)
NOTE_OVERLAP = (
"⚠ 같은 사면이 초류종자살포·면고르기에도 들어 있음 — 겹친 몫을 자동으로 빼지 않음"
"(파종 구간을 줄일지 설계자가 고름 · 2026-09-15 브레인 ⓑ)"
)
NOTE_STAKE = (
"평떼 5-22-3 [주] 「평떼(0.06인) 품에는 떼꽂이 포함」은 5-12 품에 걸린 것으로 읽음"
" — 5-22-3 떼꽂이는 따로 셈"
)
NOTE_FIGURES = "교본 그림 7-8·7-10·7-11 은 사진 · 7-9·7-12 는 개념도(치수 없음) — 표준도가 아님"
def _length_at(points: list[tuple[float, float]], at: float) -> float:
"""그 자리의 사면길이 — 앞뒤 측점 사이 직선 보간 · 끝 밖이면 가장 가까운 측점."""
if at <= points[0][0]:
return points[0][1]
for (s0, v0), (s1, v1) in zip(points, points[1:]):
if s0 <= at <= s1:
return v0 if s1 == s0 else v0 + (v1 - v0) * (at - s0) / (s1 - s0)
return points[-1][1]
def _interval_area(points: list[tuple[float, float]], start: float, end: float) -> float:
if len(points) < 1 or end <= start:
return 0.0
cuts = [start, *[s for s, _v in points if start < s < end], end]
return sum(
(_length_at(points, a) + _length_at(points, b)) / 2 * (b - a)
for a, b in zip(cuts, cuts[1:])
)
def revegetation_from_designs(
structure: dict[str, Any],
designs: list[dict[str, Any]] | None,
slopes: list[Any] | None = None,
) -> dict[str, Any]:
"""구간 안 절토·성토 사면 면적을 B06 측점에서 셈해 실은 **새** 구조물(칸 값은 안 건드림)."""
if slopes is None:
from B08_Quantity.B08_Quantity_Engine_SlopeLength import station_slopes
slopes = station_slopes(designs or [])
ordered = sorted(slopes, key=lambda s: s.chainage_m)
options = dict(structure.get("options") or {})
notes = list(structure.get("notes") or [])
start, end = sorted((_num(structure.get("start_m")), _num(structure.get("end_m"))))
if not ordered:
notes.append("B06 횡단 설계가 없어 구간 사면 면적을 못 읽음")
return {**structure, "options": options, "notes": notes}
for key, attr in ((CUT, "cut_length_m"), (FILL, "fill_length_m")):
points = [(s.chainage_m, float(getattr(s, attr))) for s in ordered]
options[key] = round(_interval_area(points, start, end), 4)
notes.append(
f"구간 {start:g}~{end:g}m 사면 면적 — 절토면 {options[CUT]:,.2f}㎡ · 성토면"
f" {options[FILL]:,.2f}㎡ (B06 측점 사면길이 · 평균단면적법)"
)
return {**structure, "options": options, "notes": notes}
def _area(options: dict[str, Any]) -> float | None:
keys = FACES.get(str(options.get("face") or ""))
return None if keys is None else sum(_num(options.get(key)) for key in keys)
def revegetation(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
"""녹화 한 구간 — (성분, 사유)."""
method = str(options.get("method") or "")
if not method:
return [], ["녹화 공법을 안 고름 — 공법을 고르면 섬"]
if method not in AREA_METHODS:
template = NOTE_LENGTH if method in LENGTH_METHODS else NOTE_LATER
return [], [template.format(method=method), NOTE_FIGURES]
area = _area(options)
if area is None:
return [], [NOTE_FACE]
if method == "비탈덮기" and not options.get("cover_kind"):
return [], [NOTE_COVER]
name, sheets, basis = AREA_METHODS[method]
notes = [NOTE_OVERLAP, basis, NOTE_FIGURES] + ([NOTE_STAKE] if method == "평떼" else [])
rows: list[tuple[str, str, float, str, str]] = [
(name, "㎡", area, f"{options.get('face')} 사면 면적 {area:,.2f}㎡", "")
]
if sheets is not None:
rows.append(("떼", "매", area * sheets, f"면적 {area:,.2f} × {sheets:g}매/㎡", SOD_SPEC))
components = [
Component(item, unit, amount, destination, text, spec=spec)
for item, unit, amount, text, spec in rows
if (destination := routed(item, notes))
]
return components, notes
def billing(options: dict[str, Any]) -> tuple[str, float]:
"""내역 줄 — 녹화 면적 ㎡."""
return "㎡", _area(options) or 0.0