실무 대조: 오솔길 BOM 6벌 면적 셈법 ±0.11% · 거창 줄 없음 · 영월 성토면 × 50% · 원문 9-19-1 성토면·9-19-2·3 식재 조건부 배정 프로젝트: 인계 줄 +3(성토면 12,350㎡ · 절토면 토사 3,085.51 · 암 2,262.62) 외 변화 0 · 본체 합계 105,512,060 그대로 · 규격 글로 시작하는 갈래는 규격에 한 번만(도자 운반 「토사 토사」 → 「토사」 · 금액 불변) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
192 lines
8.4 KiB
Python
192 lines
8.4 KiB
Python
"""사면 4계열 면적 — 실무 토적표의 오른쪽 절반 (B08 일감 3 · PLAN 8-4b).
|
|
|
|
무엇을 내나
|
|
실무 토적표 V~AI 열에 해당한다. 계열 넷 × 성토면/절토면 2벌 = **(거리, 면적) 7쌍**
|
|
(층따기는 성토면만이라 7쌍이다).
|
|
|
|
층따기[성토면] · 면고르기[성토면·절토면] · 법면보호공[성토면·절토면] · 지장목제거[성토면·절토면]
|
|
|
|
여기서 「거리」는 그 측점의 **사면길이**이고, 면적은 토적표와 **같은 평균단면적법**으로
|
|
낸다 — 계산을 두 벌로 짜지 않는다.
|
|
|
|
법면보호공은 면고르기를 참조한다 (PLAN 8-4b)
|
|
실무 시트에서 둘의 값이 완전히 같았는데, 그것은 **엑셀에서 면고르기 열을 복사한 것**이고
|
|
오솔길 산출(`1.BOM`)에는 보호공 4열이 **0** 으로 비어 있었다. 즉 산출값이 아니라 참조다.
|
|
그래서 기본은 참조로 두되 **끊을 수 있게** 한다 — 실제 보호 대상이 면고르기 대상과
|
|
다를 수 있기 때문이다.
|
|
|
|
⚠ 반영률은 법정값이 아니다 (PLAN 8-11 · 8-10 ★)
|
|
실무 시트가 「성토면 80 % 반영」처럼 비고란에 손으로 적어 둔 값이다. **프로그램 기본은
|
|
100 %** 이고 설계자가 바꾼다. 실무 관측치(80/50/80)는 기본값 후보가 아니라 참고다.
|
|
|
|
⚠ 소단 평탄부는 사면적에 넣지 않는다
|
|
면고르기·종자파종의 대상은 「사면」이고 소단은 평평한 턱이다. `SlopeSegment` 자체가
|
|
평탄부를 빼고 나오므로 여기서 다시 거를 것이 없다. 다만 **소단이 늘수록 사면적이 줄어드는
|
|
것이 눈에 보여야** 하므로 측점마다 소단 폭을 함께 싣는다.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Iterable
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_SlopeLength import StationSlope
|
|
|
|
# 계열 이름 — 실무 토적표 머리글 그대로. `fill`/`cut` 은 성토면/절토면이다.
|
|
SERIES: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
("bench_cut", ("fill",)), # 층따기 — 성토면만(원지반이 급한 곳을 계단으로 깎는다)
|
|
("face_dressing", ("fill", "cut")), # 면고르기
|
|
("slope_protection", ("fill", "cut")), # 법면보호공(종자파종)
|
|
("tree_removal", ("fill", "cut")), # 지장목제거
|
|
)
|
|
|
|
# 법면보호공이 참조하는 계열 — 기본은 면고르기다(위 설명 참조).
|
|
PROTECTION_SOURCE = "face_dressing"
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SlopeRatios:
|
|
"""계열별 반영률(0~1). 기본 100 % — 실무 관측치는 참고일 뿐 기본값이 아니다.
|
|
|
|
⚠ TODO(미결 · PLAN 8-11) — 실무 관측 80/50/80 중 **지장목제거는 밑수가 안 맞는다**
|
|
(성토+절토 합의 80 % = 14,061 ≠ 시트값 10,782). 밑수를 못 찾았으므로 쫓지 않고
|
|
100 % 로 둔다. 근거가 나오면 이 값만 바꾼다.
|
|
"""
|
|
|
|
bench_cut: float = 1.0
|
|
face_dressing: float = 1.0
|
|
slope_protection: float = 1.0
|
|
tree_removal: float = 1.0
|
|
|
|
def of(self, series: str) -> float:
|
|
return float(getattr(self, series, 1.0))
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class SlopeAreaRow:
|
|
"""측점 하나의 사면 계열 값. `lengths` 는 거리(사면길이), `areas` 는 면적."""
|
|
|
|
chainage_m: float
|
|
distance_m: float = 0.0
|
|
berm_width_m: float = 0.0
|
|
# 성토고(m) — 수평 규준틀 개소 판정(품셈 11-3 [주]① 「성토고 5m 이상」)이 쓴다.
|
|
fill_height_m: float = 0.0
|
|
unclosed: bool = False
|
|
lengths: dict[str, float] = field(default_factory=dict)
|
|
areas: dict[str, float] = field(default_factory=dict)
|
|
|
|
|
|
def _key(series: str, face: str) -> str:
|
|
return f"{series}_{face}"
|
|
|
|
|
|
def _length_of(slope: StationSlope, series: str, face: str) -> float:
|
|
"""계열·면별 「거리」 = 그 측점의 사면길이.
|
|
|
|
법면보호공은 면고르기를 참조한다 — 같은 사면길이를 쓴다. 끊고 싶으면 이 함수만 고친다.
|
|
층따기는 성토면만 대상이다.
|
|
"""
|
|
if series == "bench_cut":
|
|
# ⚠ 층따기는 **원지반 표면**을 깎는 일이라 밑수가 성토 비탈면이 아니다
|
|
# (교본 6장 4절). B06 설계가 측점마다 내는 값을 그대로 쓴다.
|
|
# 없으면 0 — 성토 사면길이로 대신 채우면 **다른 면을 세게 된다**(2026-09-09 정정).
|
|
return slope.bench_cut_length_m if face == "fill" else 0.0
|
|
return slope.fill_length_m if face == "fill" else slope.cut_length_m
|
|
|
|
|
|
def build_rows(
|
|
slopes: Iterable[StationSlope], ratios: SlopeRatios | None = None
|
|
) -> list[SlopeAreaRow]:
|
|
"""측점별 사면길이 → 계열별 (거리, 면적). 면적은 토적표와 같은 평균단면적법."""
|
|
rates = ratios or SlopeRatios()
|
|
ordered = sorted(slopes, key=lambda s: s.chainage_m)
|
|
rows: list[SlopeAreaRow] = []
|
|
previous: SlopeAreaRow | None = None
|
|
|
|
for slope in ordered:
|
|
row = SlopeAreaRow(
|
|
chainage_m=slope.chainage_m,
|
|
berm_width_m=slope.berm_width_m,
|
|
fill_height_m=slope.fill_height_m,
|
|
unclosed=slope.unclosed,
|
|
)
|
|
for series, faces in SERIES:
|
|
for face in faces:
|
|
row.lengths[_key(series, face)] = _length_of(slope, series, face)
|
|
if previous is not None:
|
|
distance = slope.chainage_m - previous.chainage_m
|
|
row.distance_m = distance
|
|
for key, length in row.lengths.items():
|
|
series = key.rsplit("_", 1)[0]
|
|
before = previous.lengths.get(key, 0.0)
|
|
# 평균단면적법 — 토적표와 같은 식이다(체적 대신 면적을 낸다).
|
|
row.areas[key] = (before + length) / 2.0 * distance * rates.of(series)
|
|
else:
|
|
row.areas = {key: 0.0 for key in row.lengths}
|
|
rows.append(row)
|
|
previous = row
|
|
return rows
|
|
|
|
|
|
def totals(rows: list[SlopeAreaRow]) -> dict[str, float]:
|
|
"""계열별 면적 합계. 거리(사면길이)는 합이 뜻이 없어 싣지 않는다."""
|
|
keys = [_key(series, face) for series, faces in SERIES for face in faces]
|
|
return {key: sum(row.areas.get(key, 0.0) for row in rows) for key in keys}
|
|
|
|
|
|
def cut_material_totals(
|
|
slopes: Iterable[StationSlope], ratios: SlopeRatios | None = None
|
|
) -> dict[str, float]:
|
|
"""면고르기 절토면의 토사·암 몫 면적 — 같은 평균단면적법 · 같은 반영률(합 ≤ 절토면 전체)."""
|
|
rate = (ratios or SlopeRatios()).of("face_dressing")
|
|
ordered = sorted(slopes, key=lambda s: s.chainage_m)
|
|
result = {"face_dressing_cut_soil": 0.0, "face_dressing_cut_rock": 0.0}
|
|
for before, after in zip(ordered, ordered[1:]):
|
|
distance = after.chainage_m - before.chainage_m
|
|
for key, field_name in (
|
|
("face_dressing_cut_soil", "cut_soil_length_m"),
|
|
("face_dressing_cut_rock", "cut_rock_length_m"),
|
|
):
|
|
pair = getattr(before, field_name) + getattr(after, field_name)
|
|
result[key] += pair / 2.0 * distance * rate
|
|
return result
|
|
|
|
|
|
def unclosed_stations(rows: list[SlopeAreaRow]) -> list[float]:
|
|
"""사면이 원지반을 못 만나 **면적이 잘린** 측점 목록.
|
|
|
|
조용히 적게 내면 안 되는 값이라 화면이 이 목록을 그대로 보인다(PLAN 8-4b).
|
|
같은 사유로 토적표의 절·성토 면적도 잘려 있다.
|
|
"""
|
|
return [row.chainage_m for row in rows if row.unclosed]
|
|
|
|
|
|
def build_table(
|
|
slopes: Iterable[StationSlope], ratios: SlopeRatios | None = None
|
|
) -> dict[str, Any]:
|
|
"""화면·API 가 그대로 쓰는 모양."""
|
|
rates = ratios or SlopeRatios()
|
|
slopes = list(slopes)
|
|
rows = build_rows(slopes, rates)
|
|
return {
|
|
"method": "average_end_area",
|
|
"series": [{"name": name, "faces": list(faces)} for name, faces in SERIES],
|
|
"protection_source": PROTECTION_SOURCE,
|
|
"ratios": {name: rates.of(name) for name, _ in SERIES},
|
|
"rows": [
|
|
{
|
|
"chainage_m": row.chainage_m,
|
|
"distance_m": row.distance_m,
|
|
"berm_width_m": row.berm_width_m,
|
|
"fill_height_m": row.fill_height_m,
|
|
"unclosed": row.unclosed,
|
|
"lengths": row.lengths,
|
|
"areas": row.areas,
|
|
}
|
|
for row in rows
|
|
],
|
|
"totals": {**totals(rows), **cut_material_totals(slopes, rates)},
|
|
"unclosed_stations": unclosed_stations(rows),
|
|
"station_count": len(rows),
|
|
}
|