diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py b/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py index c0f49e5e..8ff27a9b 100644 --- a/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Dozer.py @@ -36,14 +36,47 @@ from B09_Estimation.B09_Estimation_MachineProductivity import ( _ZERO = Decimal(0) -_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)), +#: 전진·후진 속도 (m/분) — 8-2-1 2.가·나. **단(gear)마다 다르다.** +#: {규격(ton): {단: (전진, 후진)}} +#: ⚠ 무한궤도 4·13 톤과 타이어 전 규격은 **후진 3단이 표에 없다**(「-」) — 그 단은 +#: 아예 안 담는다. 담으면 없는 속도로 싸이클이 서서 값이 조용히 틀린다. +#: ⚠ **단은 작업이 정한다** (8-2-1 2.가 [주]) — 굴착·굴착운반 1단, 흐트러진 토사운반 +#: 2단, 평탄 정지·전압 3단. 그래서 임도 표가 「55m/분(2단)」처럼 단을 적어 온다. +_DOZER_SPEEDS = { + "무한궤도": { + Decimal("4"): {1: (Decimal(40), Decimal(63)), 2: (Decimal(57), Decimal(85))}, + Decimal("7"): { + 1: (Decimal(43), Decimal(53)), + 2: (Decimal(67), Decimal(78)), + 3: (Decimal(92), Decimal(107)), + }, + Decimal("10"): { + 1: (Decimal(42), Decimal(50)), + 2: (Decimal(64), Decimal(75)), + 3: (Decimal(88), Decimal(105)), + }, + Decimal("12"): { + 1: (Decimal(40), Decimal(48)), + 2: (Decimal(55), Decimal(70)), + 3: (Decimal(75), Decimal(100)), + }, + Decimal("13"): {1: (Decimal(40), Decimal(48)), 2: (Decimal(55), Decimal(70))}, + Decimal("19"): { + 1: (Decimal(40), Decimal(46)), + 2: (Decimal(55), Decimal(70)), + 3: (Decimal(75), Decimal(98)), + }, + Decimal("32"): { + 1: (Decimal(40), Decimal(43)), + 2: (Decimal(52), Decimal(58)), + 3: (Decimal(70), Decimal(78)), + }, + }, + "타이어": { + Decimal("15"): {1: (Decimal(83), Decimal(92)), 2: (Decimal(200), Decimal(125))}, + Decimal("28"): {1: (Decimal(92), Decimal(92)), 2: (Decimal(200), Decimal(200))}, + Decimal("33"): {1: (Decimal(92), Decimal(110)), 2: (Decimal(210), Decimal(250))}, + }, } #: 기어 변속시간 (분) — 8-2-1 「t: 기어 변속시간(0.25분)」 _DOZER_GEAR_SHIFT_MIN = Decimal("0.25") @@ -100,9 +133,11 @@ def dozer_hourly_output(factors: DozerFactors) -> Decimal: return output -def dozer_speeds(tonnage: Decimal) -> tuple[Decimal, Decimal] | None: - """그 규격의 전진·후진 속도(2단). 표에 없는 규격이면 `None` — 지어내지 않는다.""" - return _DOZER_SPEEDS_2ND_GEAR.get(tonnage) +def dozer_speeds( + tonnage: Decimal, gear: int = 2, track: str = "무한궤도" +) -> tuple[Decimal, Decimal] | None: + """그 규격·그 단의 전진·후진 속도. 표에 없으면 `None` — 지어내지 않는다.""" + return _DOZER_SPEEDS.get(track, {}).get(tonnage, {}).get(gear) #: 삽날 용량 q゚(㎥) — 8-2-1 1.가. **규격을 되짚는 열쇠**로도 쓴다. @@ -120,13 +155,6 @@ _DOZER_BLADE_M3 = { ("타이어", Decimal("33")): Decimal("5.7"), } -#: 타이어형 2단 속도 — 8-2-1 2.나. -_DOZER_TIRE_SPEEDS_2ND_GEAR = { - Decimal("15"): (Decimal(200), Decimal(125)), - Decimal("28"): (Decimal(200), Decimal(200)), - Decimal("33"): (Decimal(210), Decimal(250)), -} - #: 습지·초습지 갈래는 카탈로그 이름이 따로다 — 「습지 불도저」. _DOZER_WET_TONS = (Decimal("4"), Decimal("13")) @@ -149,11 +177,6 @@ _DOZER_GROUP_KEYS = ("E", "f") _RE_GEAR = re.compile(r"(\d+)\s*단") -def dozer_tire_speeds(tonnage: Decimal) -> tuple[Decimal, Decimal] | None: - """타이어형 전진·후진 속도(2단). 표에 없으면 `None`.""" - return _DOZER_TIRE_SPEEDS_2ND_GEAR.get(tonnage) - - def resolve_dozer( blade_m3: Decimal, forward: Decimal, @@ -167,16 +190,13 @@ def resolve_dozer( q゚ 3.2㎥ + 55/70 m/분(2단) → 불도저(무한궤도) 19 톤 - ⚠ **2단만 되짚는다.** 1·3·4단 속도표는 여기 안 들고 있어, 다른 단이면 `None` 이다. + ⚠ **단(gear)까지 맞아야 한다** — 같은 삽날이라도 단이 다르면 다른 규격을 가리킨다. """ - if gear != 2: - return None candidates = [] for (track, tonnage), blade in _DOZER_BLADE_M3.items(): if blade != blade_m3: continue - speeds = dozer_speeds(tonnage) if track == "무한궤도" else dozer_tire_speeds(tonnage) - if speeds == (forward, reverse): + if dozer_speeds(tonnage, gear, track) == (forward, reverse): candidates.append((track, tonnage)) if len(candidates) != 1: return None @@ -235,7 +255,11 @@ def extract_dozer_factors( current = head label = cells[1] if len(cells) > 1 else "" value = _first_measure(cells[2:]) - if label and value is not None: + if value is None: + # 갈래가 없는 표 — 「f | 1/1.3」처럼 값이 바로 붙는다. 갈래 이름은 빈 문자열. + value = _measure_with_adjustment(label) + label = "" + if value is not None: groups[head][_normalize_label(label)] = value elif current is not None: # 딸린 줄 — 「파쇄암 | 1/1.35」. 값이 없으면 갈래 줄이 아니다. @@ -295,6 +319,23 @@ def extract_dozer_factors( ) +_RE_ADJUSTED = re.compile(r"^\(?\s*(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)\s*\)?$") + + +def _measure_with_adjustment(cell: str) -> Decimal | None: + """계수 한 칸. **「(0.55-0.1)」 같은 보정식도 읽는다.** + + 8-2-1 [주]④⑤ 가 「정지작업을 겸하면 0.1 을, 터파기는 0.05 를 뺀 값」이라 적어, + 임도 표가 뺄셈을 그대로 적어 온다(비다듬기·정지 9-17-2). ⚠ **범위(「0.55∼0.45」)와 + 헷갈리면 안 된다** — 범위는 확정값이 아니라 `parse_measure` 가 `None` 을 낸다. + 여기서 읽는 것은 **하이픈 뺄셈 한 가지**뿐이다. + """ + found = _RE_ADJUSTED.match(" ".join(str(cell).split())) + if found: + return Decimal(found.group(1)) - Decimal(found.group(2)) + return parse_measure(cell) + + def _normalize_label(text: str) -> str: return "".join(str(text).split())