diff --git a/B09_Estimation/B09_Estimation_MachineProductivity.py b/B09_Estimation/B09_Estimation_MachineProductivity.py index e7a41d35..79bc2e56 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity.py @@ -299,3 +299,83 @@ _RE_RATIO = re.compile(r"[((]\s*(\d+(?:\.\d+)?)\s*%\s*[))]") def _ratio_of(cell: str) -> Decimal | None: found = _RE_RATIO.search(str(cell)) return Decimal(found.group(1)) if found else None + + +# ── 불도저 — 굴착기와 **공식이 다르다** (건설품셈 8-2-1) ────────────────────── +# +# Q = 60 · q · f · E ÷ cm q = q₀ × e +# cm = L/V1 + L/V2 + t t = 기어 변속시간 0.25 분 +# +# q₀ 거리를 고려하지 않은 삽날 용량(㎥) · e 운반거리계수 · L 운반거리(m) +# V1 전진속도(m/분) · V2 후진속도(m/분) +# +# ⚠ **굴착기 식(3600 ÷ Cm 초)과 섞지 말 것** — 불도저 cm 은 **분**이고 왕복 주행으로 +# 만든다. 그래서 밑수가 60 이다. 섞으면 60배 어긋난다. +# +# ⚠ **속도는 「단」에 따라 다르다** (8-2-1 [주]) — 굴착·운반은 전진 1단·후진 1단, +# 흐트러진 토사운반은 2단, 평탄 정지는 3단. **임도 운반은 [주]② 「흐트러진 상태의 +# 토사운반」이 가장 가까워 2단을 잠정 채택**한다. ⚠ TODO(미결) 사용자 확정 대기. +_DOZER_SPEEDS_2ND_GEAR = { + Decimal("4"): (Decimal(57), Decimal(85)), + Decimal("7"): (Decimal(67), Decimal(78)), + Decimal("10"): (Decimal(64), Decimal(75)), + Decimal("12"): (Decimal(55), Decimal(70)), + Decimal("13"): (Decimal(55), Decimal(70)), + Decimal("19"): (Decimal(55), Decimal(70)), + Decimal("32"): (Decimal(52), Decimal(58)), +} +#: 기어 변속시간 (분) — 8-2-1 「t: 기어 변속시간(0.25분)」 +_DOZER_GEAR_SHIFT_MIN = Decimal("0.25") +_MINUTES_PER_HOUR = Decimal(60) + + +@dataclass(frozen=True) +class DozerFactors: + """불도저 시공능력 계수 한 벌 (건설품셈 8-2-1).""" + + work_item_code: str + blade_capacity_m3: Decimal # q₀ + distance_factor: Decimal # e + volume_factor: Decimal # f + efficiency: Decimal # E + haul_distance_m: Decimal # L + forward_speed_m_min: Decimal # V1 + reverse_speed_m_min: Decimal # V2 + + @property + def cycle_minutes(self) -> Decimal: + """cm = L/V1 + L/V2 + t — **분**이다.""" + return ( + self.haul_distance_m / self.forward_speed_m_min + + self.haul_distance_m / self.reverse_speed_m_min + + _DOZER_GEAR_SHIFT_MIN + ) + + @property + def formula_text(self) -> str: + return ( + f"Q = 60 ÷ {self.cycle_minutes:.4f}분 × ({self.blade_capacity_m3} × " + f"{self.distance_factor}) × {self.volume_factor} × {self.efficiency}" + ) + + +def dozer_hourly_output(factors: DozerFactors) -> Decimal: + """불도저 시간당 작업량 `Q` (㎥/hr). **밑수는 60(분)** 이다.""" + if factors.cycle_minutes <= 0: + raise ProductivityError(f"{factors.work_item_code}: 싸이클 시간이 0 이하입니다.") + blade = factors.blade_capacity_m3 * factors.distance_factor + output = ( + _MINUTES_PER_HOUR + / factors.cycle_minutes + * blade + * factors.volume_factor + * factors.efficiency + ) + if output <= 0: + raise ProductivityError(f"{factors.work_item_code}: 시간당 작업량이 {output} 입니다.") + return output + + +def dozer_speeds(tonnage: Decimal) -> tuple[Decimal, Decimal] | None: + """그 규격의 전진·후진 속도(2단). 표에 없는 규격이면 `None` — 지어내지 않는다.""" + return _DOZER_SPEEDS_2ND_GEAR.get(tonnage)