Files
Aislo/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Revegetation.py

228 lines
11 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 새심기(㎡당 · 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 측점 사면길이 · 평균단면적법) × 고른 면(절토면/성토면/양쪽).
⭐ 선떼붙이기(길이형) — 연장 = 대상지 직고 ÷ 단 직고 × 연장(품셈 5-16-1 [주]②) · m당 떼 매수 =
사방기술교본 표 2-2-6 **표 값**(단면식은 역산 · 어긋나면 표가 이김 · 2026-09-15 브레인 ⓔ).
⚠ 조공은 다음 차례 · 나머지 공법은 사유.
"""
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}은(는) 길이형(100m·m당) — 산출식 아직 없음(선떼붙이기 다음 차례)"
CUT_RISE, FILL_RISE = "b06_cut_rise_area_m2", "b06_fill_rise_area_m2"
RISES = {"절토면": (CUT_RISE,), "성토면": (FILL_RISE,), "양쪽": (CUT_RISE, FILL_RISE)}
#: 사방기술교본 표 2-2-6 「각 급별 선떼붙이기 m당 떼사용 매수표」 — 1m당 **표 값** · 한 장 ㎡.
SOD_TABLE: dict[str, tuple[str, float, tuple[float, ...]]] = {
"대형 40×40": ("40×40", 0.16, (12.50, 11.25, 10.00, 8.75, 7.80, 6.25, 5.00, 3.75, 2.50)),
"소형 18×18": ("18×18", 0.0324, (30.0, 27.0, 24.0, 21.0, 18.0, 15.0, 12.0, 9.0, 6.0)),
}
NOTE_SOD = (
"선떼붙이기 — 연장 = 대상지 직고(B06 사면 낙차) ÷ 단 직고 × 연장(품셈 5-16-1 [주]②)"
" · m당 떼 매수 = 사방기술교본 표 2-2-6 표 값(식은 역산) · 떼붙임 ㎡ = 매수 × 떼 한 장 면적"
)
NOTE_SOD_TABLE_GAP = (
"⚠ 표 2-2-6 안 어긋남 — 5급 대형 표 7.80 ↔ 단면식 7.50 · 소형 2급 표 27.0 ↔ 단면 4.0"
"(×6 = 24.0) · 표 값을 씀(표가 고쳐지면 그때 봄)"
)
NOTE_SOD_HAUL = (
"떼운반·지게운반(5-16-2 표 「물량산출」)은 운반거리(200m 조견표)가 설계 입력이라 안 섬"
)
NOTE_SOD_MISSING = "선떼붙이기 {what} 칸이 비어 연장·떼 매수가 안 섬 — 칸에 적을 것(기본 없음)"
NOTE_LATER = (
"{method}은(는) 산출식이 아직 없음 — 12장 D 녹화 차례 뒤(㎡ 넷 먼저 · 2026-09-15 브레인 ⓓ)"
)
NOTE_FACE = "대상 면을 안 고름 — 절토면/성토면/양쪽 중 고를 것(공법마다 달라 기본값 없음)"
NOTE_COVER = (
"비탈덮기 덮개를 안 고름 — 거적(5-25)·짚망(5-28-1)·방초·야자섬유매트(5-28-2) 중 고를 것"
)
NOTE_OVERLAP = (
"녹화와 초류종자살포는 병행시공(임도기술교본 7장) — 떼 면적을 파종에서 빼지 않음."
" 실무도 같음(울진1공구 수량산출)"
)
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)
# 사면 낙차(직고) × 거리 — 선떼 연장의 밑수(5-16-1 [주]② 「대상지 직고」 · 양쪽 사면 합).
for key, role in ((CUT_RISE, "cut"), (FILL_RISE, "fill")):
points = [
(s.chainage_m, sum(abs(g.rise_m) for g in s.segments if g.role == role))
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 == "선떼붙이기":
return _sod_wall(options)
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 _sod_length(options: dict[str, Any]) -> float | None:
"""선떼 연장(m) = Σ 사면 낙차 × 거리 ÷ 단 직고 — 면·단 직고가 없으면 None."""
keys = RISES.get(str(options.get("face") or ""))
step = _num(options.get("step_height_m"))
if keys is None or step <= 0:
return None
return sum(_num(options.get(key)) for key in keys) / step
def _sod_wall(options: dict[str, Any]) -> tuple[list[Component], list[str]]:
"""선떼붙이기 — 연장(m) · 떼(매) · 떼붙임(㎡)."""
if str(options.get("face") or "") not in RISES:
return [], [NOTE_FACE]
missing = [
label
for key, label in (
("step_height_m", "단 직고"),
("sod_grade", "급수"),
("sod_size", "떼 크기"),
)
if options.get(key) in (None, "", 0)
]
grade, size = str(options.get("sod_grade") or ""), str(options.get("sod_size") or "")
if not missing and (size not in SOD_TABLE or not grade.rstrip("급").isdigit()):
missing = ["급수·떼 크기"]
if missing:
return [], [NOTE_SOD_MISSING.format(what="·".join(missing))]
length = _sod_length(options) or 0.0
spec, sheet_area, per_m = SOD_TABLE[size]
sheets = per_m[int(grade.rstrip("급")) - 1]
notes = [NOTE_OVERLAP, NOTE_SOD, NOTE_SOD_TABLE_GAP, NOTE_SOD_HAUL, NOTE_FIGURES]
rows = [
(
"선떼붙이기",
"m",
length,
f"{options.get('face')} 사면 낙차 × 거리 ÷ 단 직고"
f" {_num(options.get('step_height_m')):g}",
"",
),
("떼", "매", length * sheets, f"연장 {length:,.2f} × {grade} {size} {sheets:g}매/m", spec),
(
"떼붙임",
"㎡",
length * sheets * sheet_area,
f"떼 {length * sheets:,.1f}× 한 장 {sheet_area:g}㎡",
"",
),
]
components = [
Component(item, unit, amount, destination, text, spec=item_spec)
for item, unit, amount, text, item_spec in rows
if (destination := routed(item, notes))
]
return components, notes
def billing(options: dict[str, Any]) -> tuple[str, float]:
"""내역 줄 — 녹화 면적 ㎡ · 선떼붙이기는 연장 m."""
if str(options.get("method") or "") == "선떼붙이기":
return "m", _sod_length(options) or 0.0
return "㎡", _area(options) or 0.0