- 토적표 칸 = 사용자 견본 32칸 · 보정량계 = 절토 보정량만 · 면 계열(면고르기 · 지장목제거 · 제근 · 노면정리) 거리 = B07 횡단 수량 함수 · 면적 = 앞뒤 평균 × 측점 거리 - 토공집계표 · 운반 시트 = 거창 실무 워크북 같은 이름 시트 칸 · 식 · Q · L = 저장된 유토곡선 배분(서버 · 읽기만) - 시험 test_69_b08_earthwork_tabs.py · 보정량계 시험 정함대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
332 lines
16 KiB
Python
332 lines
16 KiB
Python
"""토적표 — 측점별 단면적을 평균단면적법으로 체적화한다 (B08 일감 2 · PLAN 8-4b).
|
|
|
|
옛 엔진 복사(PLAN 61-3) — `old_code/B08_Quantity/B08_Quantity_Engine_EarthworkTable.py` 를 그대로
|
|
옮김 · 계산식 그대로. 고친 곳은 입력부(암 종류가 없으면 지반 유형) · 암 칸을 리핑암 · 발파암으로
|
|
가를 수 있게 줄에 `cut_rock_kind` 와 합계의 종류별 칸을 더한 것뿐.
|
|
|
|
무엇을 만드나
|
|
실무 토적표의 열 구성 그대로다. 거창 실무 워크북 `토적표` 시트와 오솔길 `1.BOM` 36열이
|
|
서로 1:1 로 맞물리는 것을 확인해 열 이름을 그대로 옮겼다(PLAN 8-4b).
|
|
|
|
측점 · 거리 · 절토[토사·암 각 (단면적·입적·보정량)] · 측구터파기[토사·암 각 3칸]
|
|
· 보정량계 · 성토[단면적·입적] · 유용토 · 차인토량 · 누가토량
|
|
· 면고르기[성토면·절토면] · 지장목제거[성토·절토] · 제근 · 노면정리 각 (거리·면적)
|
|
|
|
보정량계 = 절토 보정량만 (PLAN 69 정함 · 사용자 견본 120+18 행 36.2 + 55.2 = 91.4)
|
|
측구터파기 보정량은 칸에 두되 보정량계에 넣지 않는다.
|
|
|
|
면 계열 (PLAN 69-3) — 측점 「거리」 = B07 횡단도 수량과 같은 값
|
|
면고르기 · 지장목제거 = `B07_DesignDetail_Engine_Cross_Quantity.derived_cells` 그대로 ·
|
|
노면정리 = 그 측점 노면 폭(설계 `roadbed_width_m`) · 제근 = B07 에 값이 없어 빈 칸.
|
|
면적 = (앞 거리 + 현 거리) ÷ 2 × 측점 거리 — 첫 측점은 거리만. 층따기는 토공집계표용.
|
|
|
|
평균단면적법 (신규 문서 5장 「다. 공사수량의 산출」)
|
|
체적 = (앞 측점 단면적 + 현 측점 단면적) ÷ 2 × 두 측점 사이 거리.
|
|
첫 측점은 앞이 없으므로 체적이 없다(거창 실무 토적표도 첫 행 체적이 비어 있다).
|
|
|
|
보정량 = 체적 × 토량환산계수(다짐)
|
|
절취한 흙이 다져지면 줄거나 부푼다. 성토에 쓸 수 있는 양으로 환산한 것이 보정량이다.
|
|
계수의 유일한 정의처는 `config.config_system_design.EARTHWORK_CONVERSION_FACTORS` 이며
|
|
여기서 값을 다시 적지 않는다. 프로젝트가 고른 값이 있으면 라우터가
|
|
`earthwork_conversion_factors(settings)` 로 풀어 `factors` 로 넘긴다 — 기본값은 그대로다.
|
|
|
|
측구터파기 토사·암 — 설계가 가른 값을 그대로 읽는다
|
|
B06 이 지반 유형 + 암반 경계선으로 이미 갈라 냈다(`ditch_soil_area_m2`·
|
|
`ditch_rock_area_m2` · 사유 `ditch_split_basis`). 여기서 다시 나누지 않는다 —
|
|
나누면 같은 측구가 횡단도와 토적표에서 다른 숫자로 선다. 가름이 붙기 전 저장분만
|
|
절토 면적비 안분으로 떨어지고, 그 줄에는 사유가 남는다(`_split_ditch`).
|
|
|
|
⚠ 숫자는 자르지 않는다 (PLAN 8-16)
|
|
품셈 1-2-2 의 소수 자리는 **표기 규칙**이다. 계산은 전정밀로 두고 화면·출력에서만
|
|
반올림한다. 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대이므로 섞지 말 것.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Iterable
|
|
|
|
from B07_DesignDetail.B07_DesignDetail_Engine_Cross_Quantity import derived_cells
|
|
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS
|
|
|
|
# 절토 암을 어느 환산계수로 볼지 — 측점의 `cut_rock_kind` 를 그대로 쓴다.
|
|
# 값이 없으면 리핑암으로 본다(발파암보다 보수적으로 적은 쪽).
|
|
_DEFAULT_ROCK_KIND = "ripping_rock"
|
|
|
|
# 측구 가름 근거 — 설계가 가른 값이 없어 절토 면적비로 안분했을 때만 붙는다.
|
|
# B06 가 내는 네 갈래(`no_ditch`·`soil_ground`·`rock_boundary`·`rock_ground_no_boundary`)와
|
|
# 섞이지 않게 이름을 따로 둔다. 이 값이 표에 보이면 **설계가 가른 것이 아니다.**
|
|
_FALLBACK_BASIS = "cut_area_ratio_fallback"
|
|
|
|
#: 폴백을 탄 측점 줄에 남기는 사유. 숫자만 봐서는 안분인지 설계값인지 알 수 없다.
|
|
_FALLBACK_NOTE = "측구 가름값이 설계에 없어 절토 토사:암 면적비로 안분함"
|
|
|
|
#: 절토 암 칸 갈래 — 화면 「리핑암 · 발파암」 열
|
|
ROCK_KINDS = ("ripping_rock", "blasting_rock")
|
|
|
|
|
|
#: 면 계열 칸 — 화면 차례 · `benching`(층따기)은 토공집계표에만 씀
|
|
FACE_KEYS = (
|
|
"grading_fill",
|
|
"grading_cut",
|
|
"tree_removal_fill",
|
|
"tree_removal_cut",
|
|
"grubbing",
|
|
"road_surface",
|
|
"benching",
|
|
)
|
|
|
|
#: 계수 묶음의 모양 — `{지반유형: {"compacted": C}}`.
|
|
Factors = dict[str, dict[str, float]]
|
|
|
|
|
|
def _factor(kind: str, factors: Factors) -> float:
|
|
"""지반유형 → 다짐 환산계수. 모르는 유형이면 토사로 본다."""
|
|
entry = factors.get(kind) or factors["soil"]
|
|
return float(entry["compacted"])
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class StationArea:
|
|
"""토적표 한 줄이 필요로 하는 측점 값. B06 설계 결과에서 그대로 옮겨 담는다."""
|
|
|
|
chainage_m: float
|
|
cut_soil_area_m2: float = 0.0
|
|
cut_rock_area_m2: float = 0.0
|
|
fill_area_m2: float = 0.0
|
|
ditch_area_m2: float = 0.0
|
|
# 측구 가름은 **설계가 낸 값**이다(B06 `ditch_soil_area_m2`·`ditch_rock_area_m2`).
|
|
# `None` 은 「설계가 안 냈다」는 뜻이고 0.0 과 다르다 — 0.0 은 설계가 낸 「없음」이다.
|
|
ditch_soil_area_m2: float | None = None
|
|
ditch_rock_area_m2: float | None = None
|
|
ditch_split_basis: str | None = None
|
|
cut_rock_kind: str | None = None
|
|
# 면 계열 그 측점 거리(m) — `None` = B07 에 값 없음
|
|
faces: dict[str, float | None] = field(default_factory=dict)
|
|
|
|
@classmethod
|
|
def from_design(cls, chainage_m: float, design: dict[str, Any]) -> "StationArea":
|
|
def num(key: str) -> float:
|
|
value = design.get(key)
|
|
return float(value) if isinstance(value, (int, float)) else 0.0
|
|
|
|
def optional(key: str) -> float | None:
|
|
value = design.get(key)
|
|
return float(value) if isinstance(value, (int, float)) else None
|
|
|
|
cells = derived_cells(float(chainage_m), design)
|
|
faces = {key: cells.get(key) for key in FACE_KEYS}
|
|
faces["road_surface"] = optional("roadbed_width_m")
|
|
return cls(
|
|
chainage_m=float(chainage_m),
|
|
faces=faces,
|
|
cut_soil_area_m2=num("cut_soil_area_m2"),
|
|
cut_rock_area_m2=num("cut_rock_area_m2"),
|
|
fill_area_m2=num("fill_area_m2"),
|
|
ditch_area_m2=num("ditch_area_m2"),
|
|
ditch_soil_area_m2=optional("ditch_soil_area_m2"),
|
|
ditch_rock_area_m2=optional("ditch_rock_area_m2"),
|
|
ditch_split_basis=design.get("ditch_split_basis") or None,
|
|
# 지금 B06 은 `cut_rock_kind` 를 냄 — 옛 저장분은 암 지반 유형으로(유토곡선과 같은 규칙)
|
|
cut_rock_kind=design.get("cut_rock_kind")
|
|
or (design.get("ground_type") if design.get("ground_type") in ROCK_KINDS else None),
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class EarthworkRow:
|
|
"""토적표 한 줄. 열 이름은 실무 토적표(PLAN 8-4b)를 따른다."""
|
|
|
|
chainage_m: float
|
|
distance_m: float = 0.0
|
|
cut_soil_area_m2: float = 0.0
|
|
cut_soil_volume_m3: float = 0.0
|
|
cut_soil_adjusted_m3: float = 0.0
|
|
cut_rock_area_m2: float = 0.0
|
|
cut_rock_volume_m3: float = 0.0
|
|
cut_rock_adjusted_m3: float = 0.0
|
|
ditch_soil_area_m2: float = 0.0
|
|
ditch_soil_volume_m3: float = 0.0
|
|
ditch_soil_adjusted_m3: float = 0.0
|
|
ditch_rock_area_m2: float = 0.0
|
|
ditch_rock_volume_m3: float = 0.0
|
|
ditch_rock_adjusted_m3: float = 0.0
|
|
adjusted_total_m3: float = 0.0
|
|
fill_area_m2: float = 0.0
|
|
fill_volume_m3: float = 0.0
|
|
diverted_m3: float = 0.0
|
|
balance_m3: float = 0.0
|
|
cumulative_m3: float = 0.0
|
|
# 측구를 무슨 근거로 갈랐나 — 설계가 낸 사유를 그대로 싣는다(빈 문자열은 사유 없음).
|
|
ditch_split_basis: str = ""
|
|
# 절토 암 칸이 어느 갈래(환산계수)로 셌나 — 화면이 리핑암 · 발파암 열로 가름
|
|
cut_rock_kind: str = _DEFAULT_ROCK_KIND
|
|
notes: list[str] = field(default_factory=list)
|
|
# 면 계열 — `{키: {"length_m": 거리, "area_m2": 면적}}` · 값 없으면 None
|
|
faces: dict[str, dict[str, float | None]] = field(default_factory=dict)
|
|
|
|
|
|
def _split_ditch(area: StationArea) -> tuple[float, float, str]:
|
|
"""측구터파기 단면적을 토사·암으로 가른다 — **설계가 가른 값을 그대로 읽는다.**
|
|
|
|
가름의 주인은 횡단 설계다. B06 이 절토 분리와 같은 근거(지반 유형 + 암반 경계선)로
|
|
`ditch_soil_area_m2`·`ditch_rock_area_m2` 를 내고 사유를 `ditch_split_basis` 로
|
|
함께 낸다. 토적표가 여기서 다시 나누면 **같은 측구가 횡단도와 토적표에서 다른 숫자로
|
|
선다** — 그 어긋남을 없애는 자리다.
|
|
|
|
못 가른 측점(암 지반인데 암반 경계선이 없음)은 설계가 **전량 암 + 사유**로 내므로
|
|
여기서 손보지 않는다.
|
|
|
|
⚠ 폴백은 **설계값이 아예 없을 때만** — 가름이 붙기 전 저장분이다. 그때만 그 측점의
|
|
절토 토사:암 면적비로 안분하고, 근거를 `cut_area_ratio_fallback` 으로 남겨
|
|
「설계가 가른 것이 아니다」가 표에 드러나게 한다.
|
|
"""
|
|
if area.ditch_soil_area_m2 is not None or area.ditch_rock_area_m2 is not None:
|
|
return (
|
|
float(area.ditch_soil_area_m2 or 0.0),
|
|
float(area.ditch_rock_area_m2 or 0.0),
|
|
area.ditch_split_basis or "",
|
|
)
|
|
|
|
ditch = area.ditch_area_m2
|
|
if ditch <= 0:
|
|
return 0.0, 0.0, "no_ditch"
|
|
soil, rock = area.cut_soil_area_m2, area.cut_rock_area_m2
|
|
total = soil + rock
|
|
if total <= 0:
|
|
return ditch, 0.0, _FALLBACK_BASIS # 절토가 없으면 토사로 본다.
|
|
return ditch * soil / total, ditch * rock / total, _FALLBACK_BASIS
|
|
|
|
|
|
def build_rows(
|
|
stations: Iterable[StationArea], factors: Factors | None = None
|
|
) -> list[EarthworkRow]:
|
|
"""측점 목록 → 토적표 줄 목록. 측점은 이정 순으로 정렬해 받는다.
|
|
|
|
`factors` 는 프로젝트가 고른 토량환산계수다(`earthwork_conversion_factors`).
|
|
안 주면 정본 기본값이 선다 — 설정을 안 읽는 자리(시험·되짚기)를 위한 것이다.
|
|
"""
|
|
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
|
ordered = sorted(stations, key=lambda s: s.chainage_m)
|
|
rows: list[EarthworkRow] = []
|
|
previous: StationArea | None = None
|
|
previous_ditch: tuple[float, float] = (0.0, 0.0)
|
|
cumulative = 0.0
|
|
|
|
for station in ordered:
|
|
ditch_soil, ditch_rock, ditch_basis = _split_ditch(station)
|
|
soil_factor = _factor("soil", factors)
|
|
rock_factor = _factor(station.cut_rock_kind or _DEFAULT_ROCK_KIND, factors)
|
|
row = EarthworkRow(
|
|
chainage_m=station.chainage_m,
|
|
cut_soil_area_m2=station.cut_soil_area_m2,
|
|
cut_rock_area_m2=station.cut_rock_area_m2,
|
|
ditch_soil_area_m2=ditch_soil,
|
|
ditch_rock_area_m2=ditch_rock,
|
|
fill_area_m2=station.fill_area_m2,
|
|
ditch_split_basis=ditch_basis,
|
|
cut_rock_kind=station.cut_rock_kind or _DEFAULT_ROCK_KIND,
|
|
)
|
|
if ditch_basis == _FALLBACK_BASIS:
|
|
row.notes.append(_FALLBACK_NOTE)
|
|
if previous is not None:
|
|
distance = station.chainage_m - previous.chainage_m
|
|
row.distance_m = distance
|
|
|
|
def mean_volume(before: float, now: float) -> float:
|
|
return (before + now) / 2.0 * distance
|
|
|
|
row.cut_soil_volume_m3 = mean_volume(
|
|
previous.cut_soil_area_m2, station.cut_soil_area_m2
|
|
)
|
|
row.cut_rock_volume_m3 = mean_volume(
|
|
previous.cut_rock_area_m2, station.cut_rock_area_m2
|
|
)
|
|
row.ditch_soil_volume_m3 = mean_volume(previous_ditch[0], ditch_soil)
|
|
row.ditch_rock_volume_m3 = mean_volume(previous_ditch[1], ditch_rock)
|
|
row.fill_volume_m3 = mean_volume(previous.fill_area_m2, station.fill_area_m2)
|
|
|
|
row.cut_soil_adjusted_m3 = row.cut_soil_volume_m3 * soil_factor
|
|
row.cut_rock_adjusted_m3 = row.cut_rock_volume_m3 * rock_factor
|
|
row.ditch_soil_adjusted_m3 = row.ditch_soil_volume_m3 * soil_factor
|
|
row.ditch_rock_adjusted_m3 = row.ditch_rock_volume_m3 * rock_factor
|
|
|
|
row.faces = _face_cells(previous, station)
|
|
# 보정량계 = 절토 보정량만(PLAN 69 정함) — 측구터파기는 칸에만
|
|
row.adjusted_total_m3 = row.cut_soil_adjusted_m3 + row.cut_rock_adjusted_m3
|
|
# 유용토 = 그 측점에서 절취분과 성토분이 서로 만나는 몫.
|
|
row.diverted_m3 = min(row.adjusted_total_m3, row.fill_volume_m3)
|
|
row.balance_m3 = row.adjusted_total_m3 - row.fill_volume_m3
|
|
cumulative += row.balance_m3
|
|
row.cumulative_m3 = cumulative
|
|
|
|
rows.append(row)
|
|
previous = station
|
|
previous_ditch = (ditch_soil, ditch_rock)
|
|
return rows
|
|
|
|
|
|
def _face_cells(
|
|
previous: StationArea | None, station: StationArea
|
|
) -> dict[str, dict[str, float | None]]:
|
|
"""면 계열 거리 · 면적 — 면적 = 앞뒤 평균 거리 × 측점 거리 · 첫 측점은 거리만."""
|
|
out: dict[str, dict[str, float | None]] = {}
|
|
for key in FACE_KEYS:
|
|
length = station.faces.get(key)
|
|
before = previous.faces.get(key) if previous else None
|
|
area = None
|
|
if previous is not None and (length is not None or before is not None):
|
|
distance = station.chainage_m - previous.chainage_m
|
|
area = ((before or 0.0) + (length or 0.0)) / 2.0 * distance
|
|
out[key] = {"length_m": length, "area_m2": area}
|
|
return out
|
|
|
|
|
|
def totals(rows: list[EarthworkRow]) -> dict[str, float]:
|
|
"""합계 행. 단면적은 합이 뜻이 없어 싣지 않는다(실무 토적표도 비워 둔다)."""
|
|
keys = (
|
|
"distance_m",
|
|
"cut_soil_volume_m3",
|
|
"cut_soil_adjusted_m3",
|
|
"cut_rock_volume_m3",
|
|
"cut_rock_adjusted_m3",
|
|
"ditch_soil_volume_m3",
|
|
"ditch_soil_adjusted_m3",
|
|
"ditch_rock_volume_m3",
|
|
"ditch_rock_adjusted_m3",
|
|
"adjusted_total_m3",
|
|
"fill_volume_m3",
|
|
"diverted_m3",
|
|
"balance_m3",
|
|
)
|
|
out = {key: sum(getattr(row, key) for row in rows) for key in keys}
|
|
for kind in ROCK_KINDS: # 암 칸을 갈래별로 — 화면 합계 줄의 리핑암 · 발파암
|
|
mine = [row for row in rows if row.cut_rock_kind == kind]
|
|
out[f"cut_{kind}_volume_m3"] = sum(row.cut_rock_volume_m3 for row in mine)
|
|
out[f"cut_{kind}_adjusted_m3"] = sum(row.cut_rock_adjusted_m3 for row in mine)
|
|
out[f"ditch_{kind}_volume_m3"] = sum(row.ditch_rock_volume_m3 for row in mine)
|
|
for key in FACE_KEYS:
|
|
out[f"{key}_area_m2"] = sum(row.faces.get(key, {}).get("area_m2") or 0.0 for row in rows)
|
|
return out
|
|
|
|
|
|
def build_table(stations: Iterable[StationArea], factors: Factors | None = None) -> dict[str, Any]:
|
|
"""화면·API 가 그대로 쓰는 모양. 값은 자르지 않는다(PLAN 8-16).
|
|
|
|
`conversion_factors` 로 **실제로 쓴 계수**를 되싣는다 — 프로젝트가 고른 값이면
|
|
그것이 나가야 화면이 「무엇으로 셌나」를 그대로 보인다.
|
|
"""
|
|
factors = factors or EARTHWORK_CONVERSION_FACTORS
|
|
rows = build_rows(stations, factors)
|
|
return {
|
|
"method": "average_end_area",
|
|
"conversion_factors": factors,
|
|
"rows": [row.__dict__ if not hasattr(row, "__slots__") else _as_dict(row) for row in rows],
|
|
"totals": totals(rows),
|
|
"station_count": len(rows),
|
|
}
|
|
|
|
|
|
def _as_dict(row: EarthworkRow) -> dict[str, Any]:
|
|
return {name: getattr(row, name) for name in EarthworkRow.__slots__}
|