Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
228 lines
11 KiB
Python
228 lines
11 KiB
Python
"""비탈면 녹화 전개식 — **㎡ 넷(새심기·평떼·줄떼·비탈덮기)** (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
|