Files
Aislo/B08_Quantity/B08_Quantity_Engine_GravelSurfacing.py
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

166 lines
7.3 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).
근거
① 법령 별표2 Ⅰ.2.바.(2) — 종단 8% 초과 사질·점토 구간 · 8% 이하 연약·습윤 구간에 쇄석·자갈 부설
(최소 요구 · 조건이 또렷함 ⇒ 기본)
② 임도기술교본 3-2 — 「콘크리트 포장 구간 이외에는 혼합석 부설 다짐 후 0.10m 내외」
(더 넓은 권장 ⇒ 고르개)
③ 임도기술교본 10-1 · 부록 7-2 — 포설 전 10㎝ 터파기 · 적용범위 「노면의 토사구간」
④ 품셈 11-4 쇄석·혼합석 부설(㎥ · 백호 부설만) · 1-2-3 체적환산표(혼합석 줄 없음)
⚠ 면적은 측점 사이 평균(끝 둘 중 하나만 해당이면 반만) — 토적표 평균단면적법과 같은 결.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import _num
LEGAL_GRADE_PCT = 8.0
THICKNESS_SUGGESTED_M = 0.10 # 교본 3-2 「다짐 후 0.10m 내외」
PRE_EXCAVATION_M = 0.10 # 교본 10-1 「포설 전 10㎝ 터파기」
C_SUGGESTED, L_SUGGESTED = 0.85, 1.25 # 실무 소광 관측
NOTE_LEGAL = (
"법령 별표2 Ⅰ.2.바.(2) 조건으로 자동 — 종단 8% 초과 토사 측점 + 8% 이하 중 연약·습윤 칸 구간"
" · 교본 3-2 는 「콘크리트 포장 구간 이외」 전부(산출 조건 「비포장 전 구간」으로 넓힘)"
)
NOTE_ALL = (
"비포장 전 구간 — 임도기술교본 3-2 「콘크리트 포장 구간 이외에는 혼합석 부설」 · 법령 별표2"
" .2.바.(2) 는 종단 8% 초과 사질·점토 / 8% 이하 연약·습윤 구간만 요구"
)
NOTE_SOIL = (
"법령은 사질·점토를 가르는데 우리 프리셋은 토사 하나 — 토사 전부를 해당으로 봄(암은 비해당)"
" · 토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)"
)
NOTE_GRADE = "종단 경사 = 종단 계획선에서 측점을 감싼 구간 경사 중 큰 값(B05 포장 제안과 같은 식)"
NOTE_WIDTH = "폭 = 차도 표준 폭 + 확폭(노견 제외 · 실무 소광 「혼합석부설 B=3.0」)"
NOTE_COMPACTION = (
"다짐 줄 없음 — 품셈 11-4 는 부설만이고 산림품셈에 쇄석 다짐 공종이 없음"
" · 9-16-2 노체다짐은 「대규모 성토지 층다짐」 조건이라 안 이음"
)
NOTE_EXCAVATION = (
"사전 터파기 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 적용범위 「노면의 토사구간」이라"
" 토사 측점만 · 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음"
" · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈"
)
def _factor_note(c: float, l_factor: float, suggested: bool) -> str:
head = (
f"C {c:g} · L {l_factor:g} — 제안값 실무 소광 관측"
if suggested
else f"C {c:g} · L {l_factor:g} — 산출 조건 칸"
)
return (
f"{head} · 자재 = 다짐 후 부피 ÷ C × L · 원문 표에 혼합석 줄 없음(품셈 1-2-3 가까운 줄"
" 역(礫) L 1.10~1.20 · C 1.05~1.10) · ⚠ 소광 C 0.85(다지면 줄어듦)와 원문 역 C(늘어남)는"
" 방향이 반대 — 같은 C 라도 뜻이 다를 수 있음"
)
def _qualifies(
chainage: float,
design: dict[str, Any],
grades: dict[float, float],
soft_wet: list[tuple[float, float]],
all_unpaved: bool,
) -> bool | None:
"""이 측점에 까는가 — 경사를 모르면 `None`(판정 못 함)."""
if design.get("paved"):
return False
if all_unpaved:
return True
grade = grades.get(round(chainage, 3))
if grade is None:
return None
if grade > LEGAL_GRADE_PCT:
return design.get("ground_type") == "soil"
return any(start <= chainage <= end for start, end in soft_wet)
def gravel_surfacing(
designs: list[dict[str, Any]] | None,
grades: dict[float, float] | None,
settings: dict[str, Any],
) -> dict[str, Any]:
"""노선 혼합석 부설 한 벌 — 면적·부피·사전 터파기·사유."""
grades = {round(float(k), 3): float(v) for k, v in (grades or {}).items()}
all_unpaved = bool(settings.get("gravel_all_unpaved"))
soft_wet = [
tuple(sorted((_num(r.get("from_m")), _num(r.get("to_m")))))
for r in settings.get("gravel_soft_wet_ranges") or []
if isinstance(r, dict)
]
stations = sorted(
(
(_num(item.get("chainage_m")), item["design"])
for item in designs or ()
if isinstance(item, dict) and isinstance(item.get("design"), dict)
),
key=lambda row: row[0],
)
weights: list[tuple[float, float, float]] = [] # (측점, 폭×해당, 폭×해당×토사)
unknown = 0
for chainage, design in stations:
hit = _qualifies(chainage, design, grades, soft_wet, all_unpaved)
unknown += hit is None
width = (
_num(design.get("carriageway_standard_width_m"))
+ _num(design.get("widening_left_m"))
+ _num(design.get("widening_right_m"))
)
on = width if hit else 0.0
weights.append((chainage, on, on if design.get("ground_type") == "soil" else 0.0))
area = soil_area = 0.0
for (s0, w0, t0), (s1, w1, t1) in zip(weights, weights[1:]):
area += (w0 + w1) / 2 * (s1 - s0)
soil_area += (t0 + t1) / 2 * (s1 - s0)
thickness = _num(settings.get("gravel_thickness_m")) or THICKNESS_SUGGESTED_M
c = _num(settings.get("gravel_conversion_c")) or C_SUGGESTED
l_factor = _num(settings.get("gravel_conversion_l")) or L_SUGGESTED
suggested = not (settings.get("gravel_conversion_c") or settings.get("gravel_conversion_l"))
notes = [NOTE_ALL if all_unpaved else NOTE_LEGAL, NOTE_SOIL, NOTE_GRADE, NOTE_WIDTH]
notes.append(
f"두께(다짐 후) {thickness:g}m — "
+ (
"산출 조건 칸"
if settings.get("gravel_thickness_m")
else "제안값 교본 3-2 「0.10m 내외」"
)
)
notes += [_factor_note(c, l_factor, suggested), NOTE_COMPACTION, NOTE_EXCAVATION]
if unknown:
notes.append(f"종단 경사를 못 읽은 측점 {unknown}곳은 안 깜(판정 못 함)")
compacted = area * thickness
return {
"all_unpaved": all_unpaved,
"area_m2": area,
"soil_area_m2": soil_area,
"thickness_m": thickness,
"compacted_m3": compacted,
"loose_m3": compacted / c * l_factor,
"excavation_soil_m3": soil_area * PRE_EXCAVATION_M,
"notes": notes,
# 칸이 비면 쓰는 제안값 — 화면이 회색으로 보임(값의 정의처는 여기 한 곳).
"suggested": {"thickness_m": THICKNESS_SUGGESTED_M, "c": C_SUGGESTED, "l": L_SUGGESTED},
}
def gravel_material_rows(result: dict[str, Any] | None) -> list[dict[str, Any]]:
"""혼합석 자재(흐트러진 부피) → 자재총괄 성분 한 줄 — 부피가 없으면 빈 목록."""
amount = _num((result or {}).get("loose_m3"))
if amount <= 0:
return []
return [
{
"name": "혼합석",
"spec": "",
"unit": "㎥",
"amount": amount,
"destination": "material",
"source": "혼합석 부설",
}
]