Files
Aislo/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py
T
eomsangdonandClaude Opus 5 58afe8f2d2 feat(b09): 덤프 적재 10-12 「1. 적재」 — 굴착기 0.7㎥ Q1 호표(#적재) · E0 토사 0.60(본문 0.75 버림을 사유에)
- Q1 = 3600×0.7×K×f×E0/22초 · 토사 47.63 · 암절취 16.32 · 발파암 13.67 ㎥/hr
- E0 는 운반 식 Es 와 다른 기호 — 토사 0.60(표 「임도」·10-12-3 [주]③) 채택 · 본문 0.75 버림(브레인 판정)
- 거리 무관이라 늘 세움 · 내역은 인계 수단 표지 dump_loading 줄을 잎 #적재 로(B08 짝 줄은 다음 커밋)
- 시험 1건 보탬

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 08:37:58 +09:00

276 lines
11 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.
"""B09 원가계산 — **덤프트럭 운반** 시공능력 (산림사업 표준품셈 10-12 「2. 운반」 · 2026-09-14).
원문(고시 2025-82호 10-12-1·2·3 「2. 운반」)은 표가 아니라 **채워 넣는 식 서식**이라 공종 마스터에
안 실렸다. 값은 원문 본문에서 읽어 여기 한 곳에 둔다(식을 데이터로).
Qt = T / γt × L 덤프 1대 적재토량(㎥, 흐트러진 상태) · T 15ton
n = Qt / (q × K) 적재기계 싸이클 횟수 · q 버켓 0.7㎥
t1 = ㎝s × n / (60 × Es) 적재 대기(분) · ㎝s 20초
t2 = (D/V1 + D/V2) × 60 왕복(분) · D 운반거리 km · V1 5 · V2 6 km/hr
㎝t = t1 + t2 + t3 + t4 + t5 t3 적하 1.1 · t4 대기 0.9 · t5 덮개 0.5 (분)
Q = 60 × Qt × f × E / ㎝t ㎥/hr (자연상태) · f = 1/L
⚠ **원문이 서로 어긋나는 자리 — 고른 값과 버린 값을 사유에 나란히 둔다**(2026-09-14 브레인 판정).
사용자가 뒤집을 수 있어야 한다.
① n 의 Qt — 원문 표기 「10/(0.7×K)」(셋 다 10) ↔ **계산값 T/γt×L**(토사 10.26·암절취 8.44·
발파암 10.16). 원문이 `q` 한 글자를 적재토량·버켓용량 두 뜻으로 써 10 은 토사 어림수
복사로 봄.
② 발파암 운반 줄에 E 누락 — **0.9**(덤프 작업효율 · 같은 절 토사·암절취 0.9). 빼면 Q 를 못 구함.
④ Es(적재기계 작업효율 · t1) ≠ E0(적재 식) — **운반 줄 값 그대로**(토사 0.85 ·
암 0.35).
⚠ 운반거리 D 는 원문에 없다 — **설계자 값**(B08 유토곡선·사토장). 없으면 줄을 안 세우고
까닭(0 원 금지).
⚠ 적재(「1. 적재」 굴착기 Q1)는 **별도 줄**이다 — 이 식의 t1 은 덤프가 기다리는 시간이지
굴착기 품이 아님(흙깎기 9-3-2 ㎝ 20초·135° 는 깎아 옆에 둠 — 22초·180° 싣기와 다른 일).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from decimal import Decimal
from typing import Any
from B09_Estimation.B09_Estimation_MachineProductivity import fix2
#: 「[주] 장비는 덤프트럭(15ton)을 적용한다」 — 기계 카탈로그 덤프트럭 15.
DUMP_TRUCK_CODE = "0602-0150"
DUMP_PARENT = "FP-10-12"
_TRUCK_TON = Decimal(15)
_BUCKET_M3 = Decimal("0.7")
_LOADER_CYCLE_SEC = Decimal(20)
_V_LOADED_KMH = Decimal(5)
_V_EMPTY_KMH = Decimal(6)
_T3_UNLOAD, _T4_WAIT, _T5_COVER = Decimal("1.1"), Decimal("0.9"), Decimal("0.5")
_SIXTY = Decimal(60)
@dataclass(frozen=True)
class DumpMaterial:
"""10-12 절 하나의 재료 값 — 원문 「2. 운반」 줄."""
work_item_code: str
label: str
unit_weight: Decimal # γt ton/㎥
loose_factor: Decimal # L 토량환산계수
bucket_factor: Decimal # K
loader_efficiency: Decimal # Es
truck_efficiency: Decimal # E
notes: tuple[str, ...] = ()
_COMMON_NOTE = (
"n 의 Qt = 계산값 T/γt×L(원문 표기 「10/(0.7×K)」의 10 은 토사 어림수 복사로 봄 — 버림)"
)
_ES_NOTE = "Es = 운반 줄 값(적재 식의 E0 와 다른 기호)"
DUMP_MATERIALS: dict[str, DumpMaterial] = {
"FP-10-12-01": DumpMaterial(
"FP-10-12-01", "토사", Decimal("1.9"), Decimal("1.3"), Decimal("0.9"), Decimal("0.85"),
Decimal("0.9"), (_COMMON_NOTE, _ES_NOTE),
),
"FP-10-12-02": DumpMaterial(
"FP-10-12-02", "암절취", Decimal("2.4"), Decimal("1.35"), Decimal("0.55"), Decimal("0.35"),
Decimal("0.9"), (_COMMON_NOTE + " · 원문 10 이면 n 1.18배", _ES_NOTE),
),
"FP-10-12-03": DumpMaterial(
"FP-10-12-03", "발파암", Decimal("2.4"), Decimal("1.625"), Decimal("0.55"), Decimal("0.35"),
Decimal("0.9"),
(_COMMON_NOTE, _ES_NOTE, "E 원문 누락 — 0.9 적용(같은 절 토사·암절취 · 덤프 작업효율)"),
),
} # fmt: skip
#: 「1. 적재」 — Q1 = 3600 × q0 × K × f × E0 / ㎝ · 굴착기(무한궤도) 0.7㎥ · ㎝ 22초(180°).
#: ⚠ E0 는 운반 식의 Es 와 **다른 기호**다(판정 ④⑥). 토사 본문 「E0=0.75」 는 표(10-12-1 [주]⑤
#: 「E0 토사 0.60(불량) — 임도」)·10-12-3 [주]③(「토사 0.6」)과 어긋나 **0.60 채택 · 0.75 버림**.
LOADER_CODE = "0201-0070"
_LOADING_CYCLE_SEC = Decimal(22)
_LOADING_E0 = {
"FP-10-12-01": Decimal("0.60"),
"FP-10-12-02": Decimal("0.35"),
"FP-10-12-03": Decimal("0.35"),
}
_LOADING_NOTES = {
"FP-10-12-01": "E0 = 0.60(표 「임도」·10-12-3 [주]③) — 본문 표기 0.75 버림",
"FP-10-12-02": "E0 = 0.35(본문·표 「파쇄암」 같음)",
"FP-10-12-03": "E0 = 0.35(10-12-3 [주]③ 「파쇄암 0.35」)",
} # fmt: skip
def loading_output(code: str) -> Decimal:
"""적재 Q1 (㎥/hr, 자연상태) — f·Q1 소수 2자리 확정(명세 7장)."""
material = DUMP_MATERIALS[code]
return fix2(
Decimal(3600)
* _BUCKET_M3
* material.bucket_factor
* fix2(Decimal(1) / material.loose_factor)
* _LOADING_E0[code]
/ _LOADING_CYCLE_SEC
)
def loading_title_code(code: str) -> str:
return f"B-{code}#적재"
@dataclass(frozen=True)
class DumpHaul:
"""덤프 운반 한 벌 — 재료 + 운반거리."""
material: DumpMaterial
distance_m: Decimal
@property
def truck_load_m3(self) -> Decimal:
return _TRUCK_TON / self.material.unit_weight * self.material.loose_factor
@property
def loader_cycles(self) -> Decimal:
return self.truck_load_m3 / (_BUCKET_M3 * self.material.bucket_factor)
@property
def cycle_minutes(self) -> Decimal:
wait = _LOADER_CYCLE_SEC * self.loader_cycles / (_SIXTY * self.material.loader_efficiency)
km = self.distance_m / Decimal(1000)
travel = (km / _V_LOADED_KMH + km / _V_EMPTY_KMH) * _SIXTY
return wait + travel + _T3_UNLOAD + _T4_WAIT + _T5_COVER
@property
def volume_factor(self) -> Decimal:
return fix2(Decimal(1) / self.material.loose_factor)
@property
def hourly_output(self) -> Decimal:
"""Q (㎥/hr) — f 와 Q 는 소수 2자리로 먼저 확정(명세 7장)."""
return fix2(
_SIXTY
* self.truck_load_m3
* self.volume_factor
* self.material.truck_efficiency
/ self.cycle_minutes
)
@property
def formula_text(self) -> str:
m = self.material
return (
f"Qt = 15/{m.unit_weight}×{m.loose_factor} = {self.truck_load_m3:.2f}㎥ · "
f"n = {self.truck_load_m3:.2f}/(0.7×{m.bucket_factor}) = {self.loader_cycles:.2f}회 · "
f"㎝t = {self.cycle_minutes:.2f}분(L={self.distance_m}m) · "
f"Q = 60×{self.truck_load_m3:.2f}×{self.volume_factor}×{m.truck_efficiency}"
f"/{self.cycle_minutes:.2f} = {self.hourly_output}㎥/hr"
)
def dump_title_code(work_item_code: str, distance_m: Decimal) -> str:
"""운반거리별 갈래 — 사토장이 여럿이면 거리마다 호표가 갈림(STmate 도 그 모양)."""
return f"B-{work_item_code}#L{distance_m.normalize():f}m"
def attach_dump_hauls(build: Any, master: dict[str, Any], distances_m: tuple[Decimal, ...]) -> None:
"""거리마다 덤프 운반 일위대가를 세운다 — X(덤프트럭 15ton) → D → B.
시간당 사용료가 안 섰으면 세우지 않고 까닭을 남긴다(0 원 일위대가 금지).
"""
from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle
names = {
str(node.get("work_item_code")): str(node.get("name") or "")
for node in master.get("work_items", [])
}
# 적재 — 거리와 무관해 늘 세움(굴착기 0.7㎥ 사용료 층이 있으면). 운반과 한 벌이라 여기 둠.
loader = f"X-{LOADER_CODE}"
for code in DUMP_MATERIALS:
title_code = loading_title_code(code)
if loader not in build.book.titles or title_code in build.book.titles:
continue
output = loading_output(code)
material = DUMP_MATERIALS[code]
label = names.get(code) or material.label
build.book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{names.get(DUMP_PARENT) or '덤프운반'} {label} 적재",
spec="굴착기(무한궤도) 0.7㎥",
unit="㎥",
)
)
build.book.add_output_detail(
title_code,
loader,
Decimal(1) / output,
f"Q1 = 3600×0.7×{material.bucket_factor}×{fix2(Decimal(1) / material.loose_factor)}"
f"×{_LOADING_E0[code]}/22 = {output}㎥/hr · {_LOADING_NOTES[code]}",
output=output,
)
if not distances_m:
return
hourly = f"X-{DUMP_TRUCK_CODE}"
for code, material in DUMP_MATERIALS.items():
if hourly not in build.book.titles:
build.component_gaps[code] = "덤프트럭(15ton) 시간당 사용료가 아직 안 섰습니다"
continue
for distance in distances_m:
haul = DumpHaul(material, distance)
title_code = dump_title_code(code, distance)
if title_code in build.book.titles:
continue
build.book.add_title(
PriceTitle(
code=title_code,
kind=PriceKind.UNIT_PRICE,
name=f"{names.get(DUMP_PARENT) or '덤프운반'} "
f"{names.get(code) or material.label}",
spec=f"L={distance.normalize():f}m",
unit="㎥",
)
)
build.book.add_output_detail(
title_code,
hourly,
Decimal(1) / haul.hourly_output,
haul.formula_text + " · " + " · ".join(material.notes),
output=haul.hourly_output,
)
def dump_haul_distances(payload: dict[str, Any]) -> tuple[str, ...]:
"""B08 인계에서 덤프 운반 거리(m) 갈래 — 거리가 없는 줄은 안 셈(그 줄은 「운반거리 미입력」)."""
found = {
str(Decimal(str(row["haul_distance_m"])))
for row in payload.get("work_items") or []
if str(row.get("work_item_code") or "").startswith(DUMP_PARENT)
and row.get("haul_equipment") == "dump_truck"
and row.get("haul_distance_m") not in (None, "")
}
return tuple(sorted(found, key=Decimal))
_RE_DUMP_CODE = re.compile(rf"^[BD]-{DUMP_PARENT}-0[1-3]#L(\d+(?:\.\d+)?)m$")
#: 인계 「덤프 적재」 짝 줄의 수단 표지 — 운반 줄(`dump_truck`)과 갈라 잎 `#적재` 로 감.
LOADING_EQUIPMENT = "dump_loading"
def dump_distance_from_code(code: str) -> tuple[str, ...]:
"""호표 한 장을 여는 쪽 — 코드 `#L164.23m` 에서 거리를 되읽음(인계를 다시 안 셈)."""
found = _RE_DUMP_CODE.match(code)
return (found.group(1),) if found else ()
def dump_child_for(ground: str | None, edition: str) -> str | None:
"""인계 갈래(토사·리핑암·발파암) → 10-12 잎. 모르는 갈래는 `None` — 가까운 것을 안 고름."""
from common_util.common_util_aliases import alias_target, load_aliases
wanted = str(ground or "").strip()
if not wanted:
return None
alias = alias_target(load_aliases("variant"), wanted, DUMP_PARENT, edition) or wanted
return next(
(code for code, material in DUMP_MATERIALS.items() if material.label == alias), None
)