Files
Aislo/B08_Quantity/B08_Quantity_Engine_HaulInputs.py
T
eomsangdonandClaude Opus 5 659eba7faa docs(B08): 허용오차 둘을 한 자리에 모으고 왜 다른지 남김 · 축 맞춤 검산 문장 추가
- 관 허용오차 두 값이 우연히 다른 것처럼 보였음. `LENGTH_MATCH_TOLERANCE_M`(0.5m,
  「길이를 어디서 가져오나」)과 `SECTION_MATCH_TOLERANCE_M`(0.05m, 「횡단이 있나」)을
  한 자리에 이름 붙여 모으고 **맞추면 둘 다 나빠지는 까닭**을 적음
  ⚠ 두 값의 차이는 증상이고 병은 「관 자리에 측점이 없는 것」(계획서 3-14, B06 자리)
- 채집석 축 맞춤 검산을 값과 함께 보냄 — 더하는 쪽·빼는 쪽에 같은 계수가 걸려
  사토 총량이 그대로임(61.88 → 61.887)

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

147 lines
8.3 KiB
Python
Raw 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.
"""유토곡선이 받아야 할 **구조물 몫** — 채집석 공제와 구조물 잔토 (2026-09-08).
⚠⚠ **B08 은 내기만 하고 빼거나 더하지 않는다.** 두 값 다 **양수 ㎥** 로 주고,
공제(빼기)와 사토 가산(더하기)은 **유토곡선(B06)에서 한 번씩만** 일어난다.
부호를 넘기면 받는 쪽에서 두 번 뒤집힌다 — 실무 시트가 `−274.66` 으로 적혀 있어
실제로 겪은 자리다.
채집석 공제는 사토에서 한 번만 뺀다.
구조물 잔토는 사토에 한 번만 더한다.
⚠ **측점별로도 낸다.** 총량 하나만 주면 받는 쪽이 잔량 크기에 비례해 나눌 수밖에 없고,
그러면 **운반거리가 틀어진다**(한 곳에 몰리면 거리가 어긋남 — 공제 때 이미 짚은 자리).
구조물은 구간(start~end)이라 **그 가운데 측점**을 자리로 본다.
⚠ **지반 갈래도 함께 낸다** — 새 판정이 아니라 **구조물터파기가 이미 쓰는 그 값**이다
(`design.ground_type` 에서 뽑아 「암절취 · 심도 2~3m」로 세운 그것). 그 터파기에서 나온
흙이 곧 이 잔토이므로 **같은 판정을 두 곳이 그대로 쓴다** — 두 벌로 짜면 갈린다.
⚠ **섞여서 못 고른 구조물은 「모름」(`None`)으로 보낸다** — 터파기에서 다수결로 안 고른
그 규칙 그대로다. 받는 쪽이 그 몫을 「모르는 몫」으로 드러내면 된다.
⚠ **한계도 그대로 넘어간다** — 「터파기 깊이가 암반 경계선보다 깊은지」는 보지 않는다.
터파기 토질에 이미 있는 한계이고 여기서 새로 생기는 것이 아니다.
"""
from __future__ import annotations
from typing import Any
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import COLLECTED_STONE_KEY, GROUND_TYPE_LABEL
#: 유토곡선이 읽는 칸 이름 — 양쪽이 같은 낱말을 써야 인계에서 어긋나지 않는다.
STRUCTURE_SPOIL_KEY = "structure_spoil_m3"
STRUCTURE_SPOIL_POINTS_KEY = "structure_spoil_points"
#: ⚠⚠ **이 값은 자연상태(본바닥)다.** 유토곡선 잔량은 **다짐상태**라 그냥 더하면 상태가
#: 섞인다(2026-09-09 랩탑 메인 지적). 받는 쪽이 `× C` 로 맞춰 담을 수 있게 **상태를 값으로
#: 적어 보낸다** — 말로만 두면 다음 사람이 못 본다.
#: 까닭: 터파기·되메우기가 **제자리 기하 부피**(H × 폭 × 연장)이고 잔토는 그 차이다.
#: ⚠ 엄밀히는 되메움에 **쓰이는 흙**은 다짐부피 ÷ C 라 조금 더 든다. 실무 정본 시트도
#: 그냥 빼므로(터파기 1.55 − 되메우기 0.30 = 잔토 1.25) 여기서도 그대로 두되,
#: **그 사실을 적어 둔다** — 값을 임의로 보정하지 않는다.
VOLUME_BASIS_KEY = "volume_basis"
VOLUME_BASIS_NATURAL = "natural"
SPOIL_COMPONENT = "잔토처리"
COLLECTED_STONE_COMPONENT = "채집석"
#: 채집석 공제를 **갈래별로** 낸다 (2026-09-09 세 창 확정 — 축을 맞추기로).
#: ⚠ 이 값은 **벽 입적(제자리 완성 부피)**이다 — 자연·다짐 어느 축으로도 환산한 적 없다.
#: 굳이 가르면 자연 축 쪽이라 받는 쪽이 **×C 로 다짐 축에 맞춰** 뺀다.
#: ⚠⚠ **벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없다** — 캐면 부풀고(L) 벽에 쌓으면
#: 공극이 생기는데 그 값이 품셈·교본·지식DB 어디에도 없다. **실무 시트도 그냥 뺀다**
#: (울진 「채집석 −274.66」이 사토에서 바로 빠진다). 그 가정은 어느 쪽으로 가도 남으므로
#: **가정은 그대로 두고 축만 맞춘다** — 축이 다른 값을 그냥 빼는 것은 우리가 만드는 어긋남이다.
#: ⚠ 갈래를 모르는 구조물 몫은 **환산하지 않는다**(계수가 없다) — 따로 낸다.
COLLECTED_STONE_BY_GROUND_KEY = "collected_stone_by_ground_m3"
COLLECTED_STONE_UNKNOWN_KEY = "collected_stone_ground_unknown_m3"
#: ⭐ **축 맞춤의 검산** — 더하는 쪽(구조물 잔토)과 빼는 쪽(채집석 공제)에 **같은 계수**가
#: 걸리므로 사토 총량은 안 흔들린다. 실측: 잔토 126.625 ×1.15 = 145.62 담기고 공제
#: 64.75 ×1.15 = 74.46 빠져 사토(다짐) 71.16 → ÷1.15 = **61.887**, 축 맞추기 전 61.88 과 같다.
#: ⇒ **값을 안 흔들면서 축만 바로잡은 것**이다. 갈래가 갈리거나 모르는 몫이 섞이는
#: 자리에서만 값이 달라진다.
COLLECTED_STONE_AXIS_CHECK = (
"축 맞춤 검산 — 더하는 쪽과 빼는 쪽에 같은 계수가 걸려 사토 총량은 그대로"
"(잔토 ×C 담김 · 공제 ×C 빠짐 ⇒ ÷C 하면 같은 값)"
)
COLLECTED_STONE_BASIS_NOTE = (
"벽 입적(제자리 부피) — 자연 축으로 보고 받는 쪽이 ×C 로 다짐 축에 맞춰 뺌."
" ⚠ 벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없음(캐면 부풀고 쌓으면 공극이 생기나"
" 그 값이 어디에도 없음). 실무 시트는 그냥 뺌 — 우리는 축만 맞춤"
)
def _center(structure: dict[str, Any]) -> float | None:
start, end = structure.get("start_m"), structure.get("end_m")
if start is None and end is None:
return None
values = [float(v) for v in (start, end) if v is not None]
return sum(values) / len(values)
def haul_inputs(unit_quantity_table: dict[str, Any] | None) -> dict[str, Any]:
"""(채집석 공제, 구조물 잔토, 측점별 잔토). 값이 없으면 `None` — 0 으로 눅이지 않는다.
⚠ `None` 과 `0.0` 은 다르다. 「아직 안 옴」과 「없음」을 받는 쪽이 갈라 봐야 한다.
"""
if not unit_quantity_table:
return {
COLLECTED_STONE_KEY: None,
COLLECTED_STONE_BY_GROUND_KEY: {},
COLLECTED_STONE_UNKNOWN_KEY: None,
STRUCTURE_SPOIL_KEY: None,
STRUCTURE_SPOIL_POINTS_KEY: [],
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
}
total = 0.0
points: list[dict[str, Any]] = []
stone_by_ground: dict[str, float] = {}
stone_unknown = 0.0
for structure in unit_quantity_table.get("structures") or []:
amount = 0.0
stone = 0.0
for component in structure.get("components") or []:
name = str(component.get("name") or "")
if name == COLLECTED_STONE_COMPONENT:
stone += float(component.get("amount") or 0.0)
if name != SPOIL_COMPONENT:
continue
amount += float(component.get("amount") or 0.0)
# 채집석 — 그 구조물의 지반 갈래로 담는다. 갈래를 못 가른 구조물 몫은 따로 둔다.
if stone > 0:
ground = str(structure.get("ground_type") or "")
if ground:
stone_by_ground[ground] = stone_by_ground.get(ground, 0.0) + stone
else:
stone_unknown += stone
if amount <= 0:
continue
total += amount
chainage = _center(structure)
if chainage is None:
continue
# 지반 갈래 — 구조물터파기가 쓰는 그 판정을 그대로 싣는다(두 벌로 안 짠다).
ground = structure.get("ground_type") or None
points.append(
{
"chainage_m": chainage,
"spoil_m3": round(amount, 3),
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
"ground_type": ground,
"ground_label": GROUND_TYPE_LABEL.get(str(ground)) if ground else None,
"ground_basis": str(structure.get("ground_type_basis") or ""),
}
)
collected = unit_quantity_table.get(COLLECTED_STONE_KEY)
return {
COLLECTED_STONE_KEY: collected,
COLLECTED_STONE_BY_GROUND_KEY: {
key: round(value, 3) for key, value in sorted(stone_by_ground.items())
},
COLLECTED_STONE_UNKNOWN_KEY: round(stone_unknown, 3) if stone_unknown else 0.0,
"collected_stone_basis": f"{COLLECTED_STONE_BASIS_NOTE} · {COLLECTED_STONE_AXIS_CHECK}",
STRUCTURE_SPOIL_KEY: round(total, 3) if points or total else None,
STRUCTURE_SPOIL_POINTS_KEY: sorted(points, key=lambda row: row["chainage_m"]),
# 총량에도 상태를 적는다 — 측점값을 안 쓰는 쪽도 상태를 알아야 한다.
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
}