Files
Aislo/B09_Estimation/B09_Estimation_MachineProductivity_Dump.py
T
eomsangdonandClaude Opus 5 139e505843 feat(b09): 덤프 운반 10-12 — 운반거리마다 호표(X 덤프트럭 15ton → D → B) · 원문 어긋남은 고른 값·버린 값을 사유에
- 원문 본문 식(채워 넣는 서식)을 데이터로: Qt = T/γt×L · n = Qt/(0.7×K) · ㎝t = t1+t2+t3+t4+t5 · Q = 60×Qt×f×E/㎝t
- 브레인 판정: n 의 Qt 계산값(원문 표기 10 버림) · 발파암 E 0.9(원문 누락) · Es 운반 줄 값 · 거리별 갈래
- 거리는 B08 인계(유토곡선·사토장)에서 — 내역이 조립에 넘기고, 호표 한 장은 코드 #L…m 에서 되읽음
- 거리 없는 줄은 0 원 아니라 「운반거리 미입력」 · 갈래는 범위 별칭 리핑암 → 암절취
- 배정 프로젝트 164.23m: 토사 3,249원/㎥ · 암절취 8,882원/㎥ · 같은 인계 대조로 바뀐 줄은 이 둘뿐(+1,437,981원)
- 시험 5건 · 전체 1761 통과

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

213 lines
8.9 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
@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
if not distances_m:
return
hourly = f"X-{DUMP_TRUCK_CODE}"
names = {
str(node.get("work_item_code")): str(node.get("name") or "")
for node in master.get("work_items", [])
}
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$")
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
)