Files
Aislo/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py
eomsangdonandClaude Opus 5 6cf7de9e82 feat(B09): 플레이트 콤펙터 다짐(9-14-2)도 세움 — 다짐 식 둘째
노체다짐과 식이 다름 — 롤러는 V·W·E·D·f/N, 콤펙터는 A·N·H·f·E/P.
원문이 답까지 적어 두어(「∙ = 4.26 ㎥/시간」) 표 계수로 셈한 값과 맞대는 시험을 둠.

- 표 칸에 단위가 붙어 오는 것(「0.09㎡」·「36,000회/hr」)을 수만 떼어 읽음.
- 기종은 절 제목이 정함(플레이트 콤팩터 1.5) — 표에 기종 칸이 없음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 23:29:30 +09:00

172 lines
7.4 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.
"""B09 — **다짐 공식**으로 서는 공종 (산림품셈 9-16-2 노체다짐 · 9-14-2 플레이트 콤펙터).
굴착·운반과 **식이 다르다.** 굴착기는 사이클(㎝)로 세는데 다짐은 롤러가 지나간 넓이로 센다.
Q = 1000 × V × W × E × D × f / N (㎥/시간)
V 다짐속도(㎞/hr) · W 롤러 유효폭(m) · E 작업효율 · D 펴는 흙의 두께(m)
f 토량환산계수 · N 소요다짐횟수
계수는 **표에 다 있고**(9-16-2 여섯 줄), 기종만 [주]에 있어 마스터가 못 싣는다.
**⚠⚠ [주]③ 을 어떻게 읽었나 — 이중계상이 갈리는 자리**
원문 [주]③ 「임도에서는 굴착기(0.7㎥, 무한궤도)와 진동롤러(자주식 10ton)을 **조합**하는
것을 적용한다」.
⇒ **포설(9-16-1)의 굴착기와 다짐(9-16-2)의 롤러를 함께 쓰라는 뜻**으로 읽는다.
다짐 줄 안에 굴착기를 **또** 넣으면 **굴착기가 두 번 선다**(포설에서 이미 셈).
그래서 이 줄은 **롤러만** 든다.
**⚠ 이 줄이 언제 서나 — [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」**
조건부 공종이라 늘 서는 것이 아니다. 지금 B08 인계에 이 줄이 **안 온다** —
실무 내역 셋(영월·울진·봉화)에 「노체포설·노체다짐」으로 가른 줄이 없어
**성토 본체 한 줄 + 성토면다짐(㎡) 따로**로 맞춰 둔 결정이다(2026-09-08 매핑 기록).
⚠ **다만 실무의 그 한 줄은 단가 안에 적사→성토→다짐 3단계를 품고 있고, 우리 성토 단가는
포설(굴착기)뿐이다** — 층다짐을 넣기로 하면 이 단가가 그날 바로 선다.
"""
from __future__ import annotations
import re
from decimal import Decimal
from typing import Any
_ZERO = Decimal(0)
#: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이거나 「A」처럼 기호뿐이다.
#: ⚠ **식이 둘이다** — 롤러는 `V·W·E·D·f/N`, 플레이트 콤펙터는 `A·N·H·f·E/P` 다.
FORMULA_ROLLER = "roller"
FORMULA_PLATE = "plate"
_KEYS = {
FORMULA_ROLLER: ("V", "W", "E", "D", "f", "N"),
FORMULA_PLATE: ("A", "N", "H", "f", "E", "P"),
}
_RE_HEAD = re.compile(r"^([VWEDfNAHP])(?:[(]|$)")
#: 「0.09㎡」·「36,000회/hr」처럼 **단위가 붙어 오는 칸**이 있다 — 수만 떼어 읽는다.
_RE_NUMBER = re.compile(r"^-?[\d,]+(?:\.\d+)?")
#: 다짐 공식으로 서는 공종 — 기종은 [주] 원문 그대로.
#: ⚠ **롤러만** 든다(위 [주]③ 읽기). 넓히면 굴착기가 두 번 선다.
COMPACTION_ITEMS: dict[str, dict[str, Any]] = {
"FP-09-16-02": {
"formula": FORMULA_ROLLER,
"machine_code": "1306-0100", # 진동롤러(자주식) 10ton
"machine_note": "산림품셈 9-16-2 [주]① 「장비는 자주식 진동롤러(10ton)를 적용한다」",
"condition_note": (
"⚠ [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」 — 늘 서는 줄이 아님"
),
"pair_note": (
"⚠ [주]③ 의 「굴착기와 진동롤러 조합」은 포설(9-16-1)과 다짐(9-16-2)을 함께 쓰라는"
" 뜻으로 읽음 — 이 줄에 굴착기를 또 넣으면 포설에서 센 굴착기를 두 번 셈"
),
},
"FP-09-14-02": {
"formula": FORMULA_PLATE,
"machine_code": "1730-0015", # 플레이트 콤팩터 1.5
"machine_note": "산림품셈 9-14-2 절 제목 「다짐(플레이트 콤펙터)」 — 표에 기종 칸이 없음",
"condition_note": "구조물 되메우기 다짐에 쓰는 소형 장비 줄",
"pair_note": "",
#: ⚠ **원문이 답까지 적어 두었다** — 「∙ = 4.26 ㎥/시간」. 표 계수로 셈한 값이 이 값과
#: 어긋나면 우리가 잘못 읽은 것이므로 **시험이 둘을 맞대 본다.**
"stated_capacity": Decimal("4.26"),
},
}
def _number(cells: list[str]) -> Decimal | None:
for cell in cells:
text = str(cell or "").strip()
found = _RE_NUMBER.match(text)
if found:
return Decimal(found.group(0).replace(",", ""))
return None
def compaction_factors(
table: dict[str, Any], formula: str = FORMULA_ROLLER
) -> dict[str, Decimal] | None:
"""표에서 여섯 계수를 뽑는다. 하나라도 없으면 `None` — **채워 넣지 않는다.**"""
found: dict[str, Decimal] = {}
for row in table.get("raw_row") or []:
cells = [str(c or "").strip() for c in row]
if not cells:
continue
head = _RE_HEAD.match(cells[0].replace(" ", ""))
if not head:
continue
value = _number(cells[1:])
if value is not None:
found.setdefault(head.group(1), value)
keys = set(_KEYS[formula])
if not keys <= set(found):
return None
return {key: found[key] for key in keys}
def capacity_per_hour(factors: dict[str, Decimal], formula: str = FORMULA_ROLLER) -> Decimal:
"""시간당 작업량(㎥/시간). **식이 둘**이라 어느 식인지 함께 받는다.
롤러 `Q = 1000 × V × W × E × D × f / N` · 콤펙터 `Q = A × N × H × f × E / P`
"""
if formula == FORMULA_PLATE:
return (
factors["A"] * factors["N"] * factors["H"] * factors["f"] * factors["E"] / factors["P"]
)
return (
Decimal(1000)
* factors["V"]
* factors["W"]
* factors["E"]
* factors["D"]
* factors["f"]
/ factors["N"]
)
def compaction_rows(node: dict[str, Any]) -> list[dict[str, Any]]:
"""그 공종이 다짐 공식으로 서면 붙일 기계 줄. 아니면 빈 목록."""
entry = COMPACTION_ITEMS.get(str(node.get("work_item_code") or ""))
if entry is None:
return []
kind = str(entry.get("formula") or FORMULA_ROLLER)
for table in node.get("tables") or []:
factors = compaction_factors(table, kind)
if factors is None:
continue
divisor = factors["P"] if kind == FORMULA_PLATE else factors["N"]
if divisor == _ZERO:
continue
capacity = capacity_per_hour(factors, kind)
if capacity <= 0:
continue
if kind == FORMULA_PLATE:
formula = (
f"Q = {factors['A']:g} × {factors['N']:g} × {factors['H']:g}"
f" × {factors['f']:g} × {factors['E']:g} ÷ {factors['P']:g}"
f" = {capacity:g} ㎥/시간"
)
else:
formula = (
f"Q = 1000 × {factors['V']:g} × {factors['W']:g} × {factors['E']:g}"
f" × {factors['D']:g} × {factors['f']:g} ÷ {factors['N']:g} = {capacity:g} ㎥/시간"
)
return [
{
"machine_code": entry["machine_code"],
"capacity_per_hour": capacity,
"cell": f"{node.get('name') or '다짐'} Q(㎥/hr)",
"ratio_pct": None,
"table_id": str(table.get("pum_table_id") or ""),
"row_cells": [],
"source_text": " · ".join(
part
for part in (
formula,
entry["machine_note"],
entry.get("pair_note") or "",
entry["condition_note"],
)
if part
),
}
]
return []