Files
Aislo/resources/tester/test_b08_haul_summary.py
T
eomsangdonandClaude Opus 5 27e068aa12 feat(b08): ㉱ (가) 암 운반량을 구성비로 가름 — B06 리핑암은 자리표시(8-1 사용자 확정 · 브레인 판정 「구성비가 정본」)
- 운반표를 만든 한 곳(Router_Earthwork)에서 암 줄을 흙깎기와 같은 구성비(_split_by_rock)·갈래별 시공법으로 가름 → 운반거리·토공집계 운반 줄·인계·사토가 한 값
- 구성비가 비면 「암」 한 줄로 막음(깎기와 같은 사유) · 시공법 안 고른 갈래만 막음 · 무대(내역 밖) 줄은 안 막음
- 유토곡선 거리·다짐 부피는 그대로(몫대로 나눔 · 검산 차이 0) — C 까지 구성비로 맞추는 것은 (나) 차례
- 936be972: 구성비 없음 → 운반·적재·사토 암 전부 입력 사유(종전 리핑암 금액 섬) · 연암 60 리핑/보통암 40 발파 → 흙깎기 2,060.043/1,373.362 · 도자 38.123/25.416 · 덤프 112.195/74.797 · 사토 28.858/19.238 같은 몫 · 되돌림

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 20:54:28 +09:00

310 lines
12 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.
"""운반 가중평균 검사 — PLAN 8-3·8-7 ㉡.
실무는 (운반수단 × 지반유형)별 **가중평균 1개**를 내역에 올린다. 단순평균이 아니다.
무대는 값은 내되 **내역 줄이 되지 않는다**(품셈 1-2-7).
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_HaulSummary import ( # noqa: E402
build_table,
check_against_plan,
summary_input_rows,
)
def plan() -> dict:
"""띠 셋 + 장거리 이동 하나. 지반유형은 `HaulPlan` 이 이미 안분해 둔 값이다."""
return {
"blocks": [
{
"bands": [
{
"equipment": "free_haul",
"haul_distance_m": 12.0,
"haul_from_m": 0.0,
"haul_to_m": 12.0,
"ea_m3": 800.0,
"rr_m3": 0.0,
"br_m3": 0.0,
},
{
"equipment": "dozer",
"haul_distance_m": 40.0,
"haul_from_m": 20.0,
"haul_to_m": 60.0,
"ea_m3": 600.0,
"rr_m3": 400.0,
"br_m3": 0.0,
},
{
"equipment": "dozer",
"haul_distance_m": 50.0,
"haul_from_m": 60.0,
"haul_to_m": 110.0,
"ea_m3": 400.0,
"rr_m3": 0.0,
"br_m3": 0.0,
},
]
}
],
"transfers": [
{
"equipment": "dump_truck",
"haul_distance_m": 300.0,
"from_m": 100.0,
"to_m": 400.0,
"ea_m3": 1000.0,
"rr_m3": 0.0,
"br_m3": 500.0,
}
],
"hauled_m3": 2200.0,
"transferred_m3": 1500.0,
}
def test_가중평균이지_단순평균이_아님() -> None:
"""도자 토사 = 600㎥@40m + 400㎥@50m → (600·40+400·50)/1000 = 44m."""
rows = build_table(plan())["rows"]
dozer_soil = next(r for r in rows if r["equipment"] == "dozer" and r["ground"] == "토사")
assert dozer_soil["volume_m3"] == pytest.approx(1000.0)
assert dozer_soil["average_distance_m"] == pytest.approx(44.0)
assert dozer_soil["average_distance_m"] != pytest.approx(45.0) # 단순평균이면 45
def test_지반유형별로_갈림() -> None:
"""`HaulPlan` 이 안분해 둔 토사·리핑암·발파암을 그대로 쓴다 — 다시 판정하지 않는다."""
rows = build_table(plan())["rows"]
dozer = {r["ground"] for r in rows if r["equipment"] == "dozer"}
assert dozer == {"토사", "리핑암"}
dump = {r["ground"] for r in rows if r["equipment"] == "dump_truck"}
assert dump == {"토사", "발파암"}
def test_무대는_내역줄이_아님() -> None:
"""품셈 1-2-7 — 소운반 20m 는 품에 포함. 인력운반 10-6 도 「초과분」이다."""
table = build_table(plan())
free = next(r for r in table["rows"] if r["equipment"] == "free_haul")
assert free["in_bill"] is False
assert free["volume_m3"] == pytest.approx(800.0) # 값은 낸다
assert all(r["in_bill"] for r in table["rows"] if r["equipment"] != "free_haul")
def test_내역줄_개수는_무대를_뺀_수() -> None:
table = build_table(plan())
assert table["bill_row_count"] == len([r for r in table["rows"] if r["in_bill"]])
assert table["bill_row_count"] == 4 # 도자 토사·리핑암 · 덤프 토사·발파암
def test_근거줄이_함께_나옴() -> None:
"""어느 구간이 그 평균을 만들었는지 되짚을 수 있어야 한다."""
table = build_table(plan())
legs = table["legs"]
assert len(legs) == 6 # 무대1 + 도자3 + 덤프2
assert {leg["source"] for leg in legs} == {"band", "transfer"}
dozer_legs = [leg for leg in legs if leg["equipment"] == "dozer" and leg["ground"] == "토사"]
assert sorted(leg["distance_m"] for leg in dozer_legs) == [40.0, 50.0]
def test_줄_순서가_늘_같음() -> None:
"""내역 줄 순서가 매번 달라지면 대조를 못 한다."""
first = [(r["equipment"], r["ground"]) for r in build_table(plan())["rows"]]
second = [(r["equipment"], r["ground"]) for r in build_table(plan())["rows"]]
assert first == second
assert first[0][0] == "free_haul" # 무대가 맨 앞
def test_검산_무대를_넣어야_합이_맞음() -> None:
"""무대를 안 내면 이 대조가 죽는다 — 그래서 값은 내고 내역 줄만 뺀다."""
table = build_table(plan())
check = check_against_plan(table, plan())
assert check.hauled_total_m3 == pytest.approx(3700.0) # 800+1000+400+1000+500
assert check.plan_total_m3 == pytest.approx(3700.0)
assert check.difference_m3 == pytest.approx(0.0)
assert check.details["free_haul"] == pytest.approx(800.0)
def test_집계표_입력으로_줄임() -> None:
"""토공집계표는 근거 줄을 안 쓴다 — 내역 줄만 넘긴다.
⚠ 2026-09-09 에 칸이 **둘 늘었다** — 다짐·자연 두 상태를 함께 넘긴다
(`natural_m3`·`volume_basis`). 내역서 수량은 자연상태라(config 5-4-3) 받는 쪽이
어느 상태인지 스스로 봐야 한다. 근거 줄(`legs`)을 안 넘긴다는 계약은 그대로다.
"""
rows = summary_input_rows(build_table(plan()))
assert all(
set(row)
== {
"equipment",
"ground",
"volume_m3",
"average_distance_m",
"natural_m3",
"volume_basis",
# 쓴 계수도 함께 온다 — 받는 쪽이 되짚을 수 있어야 한다.
"conversion_c",
# 2026-09-14 ㉱ — 구성비로 가른 암 갈래(안 가른 줄은 None) · 집계표 공종 칸이 씀.
"rock_class",
}
for row in rows
)
assert len(rows) == 5 # 무대 포함(집계표에는 오른다)
def test_계획이_비면_빈_표() -> None:
table = build_table(None)
assert table["rows"] == []
assert table["legs"] == []
# ── 저장 정본 모양 (2026-09-07 실증에서 잡은 자리) ──────────────────
#
# ⚠ 정본에 저장되는 것은 유토곡선 한 벌(`mass_haul`)이고 **배분은 그 안의 `haul_plan`** 이다.
# 바깥 껍데기를 그대로 넘기면 `blocks` 를 못 찾아 **운반 표가 영영 0줄**이 된다.
# [확정] 전에는 어차피 빈 표라 화면에서 티가 안 나던 자리다.
def 저장정본() -> dict:
"""서버 계산이 실제로 내는 모양 — `mass_haul` 바깥에 `haul_plan` 이 들어 있다."""
return {
"basis": "cross",
"cut_natural_m3": 5000.0,
"haul_plan": {
"hauled_m3": 3724.0,
"blocks": [
{
"bands": [
{
"equipment": "free_haul",
"haul_distance_m": 10.0,
"ea_m3": 3.9,
"rr_m3": 0,
"br_m3": 0,
},
{
"equipment": "dozer",
"haul_distance_m": 40.0,
"ea_m3": 7.8,
"rr_m3": 0,
"br_m3": 0,
},
{
"equipment": "dump_truck",
"haul_distance_m": 288.12,
"ea_m3": 3712.29,
"rr_m3": 0,
"br_m3": 0,
},
]
}
],
"transfers": [],
},
}
def test_바깥_껍데기를_그대로_주면_빈_표가_된다() -> None:
"""⚠ 이 시험이 그 사고를 못 박는다 — 빈 표는 「계획이 없다」와 구별이 안 된다."""
assert build_table(저장정본())["rows"] == []
def test_haul_plan_을_벗겨_주면_세_줄이_선다() -> None:
table = build_table(저장정본()["haul_plan"])
equipment = [
row.equipment if hasattr(row, "equipment") else row["equipment"] for row in table["rows"]
]
assert equipment == ["free_haul", "dozer", "dump_truck"]
assert table["bill_row_count"] == 2 # 무대는 내역 줄이 아니다
# ── 암 운반 (2026-09-07 실증) ────────────────────────────────────────
#
# 앞선 실증에서는 토사만 나와 **(수단 × 지반유형) 갈래가 실물로 갈리는 것을 못 봤다**.
# 암이 유용토에 실리는 노선으로 다시 돌려 6줄로 갈리는 것과 보정계수가 물리는 것을 확인했고,
# 그 모양을 여기 못 박는다.
def 암_운반계획() -> dict:
"""서버 계산이 실제로 낸 모양(리핑암 노선) — 띠마다 지반별 물량이 함께 온다."""
return {
"hauled_m3": 7179.0,
"blocks": [
{
"bands": [
{
"equipment": "free_haul",
"haul_distance_m": 10.0,
"ea_m3": 14.16,
"rr_m3": 18.09,
"br_m3": 0.0,
},
{
"equipment": "dozer",
"haul_distance_m": 40.0,
"ea_m3": 28.32,
"rr_m3": 36.19,
"br_m3": 0.0,
},
{
"equipment": "dump_truck",
"haul_distance_m": 439.92,
"ea_m3": 988.08,
"rr_m3": 6094.15,
"br_m3": 0.0,
},
]
}
],
"transfers": [],
}
def test_수단과_지반유형으로_갈려_여섯_줄이_섬() -> None:
table = build_table(암_운반계획())
pairs = [(row["equipment"], row["ground"]) for row in table["rows"]]
assert ("dump_truck", "리핑암") in pairs
assert ("free_haul", "리핑암") in pairs
assert len(table["rows"]) == 6
def test_암도_무대는_내역에_안_섬() -> None:
"""지반이 암이어도 무대는 품에 포함이다 — 지반유형이 그 규칙을 바꾸지 않는다."""
table = build_table(암_운반계획())
free = [row for row in table["rows"] if row["equipment"] == "free_haul"]
assert len(free) == 2 # 토사·리핑암
assert all(not row["in_bill"] for row in free)
assert table["bill_row_count"] == 4
def test_지반별_합이_총_운반토량과_맞을것() -> None:
table = build_table(암_운반계획())
total = sum(row["volume_m3"] for row in table["rows"])
assert total == pytest.approx(7179.0, abs=0.02) # 띠 값 자체의 반올림 몫
def test_보정계수가_다짐환산에_물릴것() -> None:
"""⚠ 실증 산수 — 리핑암 1.15 · 발파암 1.30 이 그대로 곱해진다.
자연토량 토사 1,240 · 암 5,560 인 같은 노선에서
리핑암: 1,240×0.9 + 5,560×1.15 = 1,116 + 6,394 = 7,510
발파암: 1,240×0.9 + 5,560×1.30 = 1,116 + 7,228 = 8,344
서버 계산이 낸 값과 자릿수까지 맞았다. 계수가 바뀌면 이 시험이 깨진다.
"""
from config.config_system_design import EARTHWORK_CONVERSION_FACTORS as F
assert F["soil"]["compacted"] == 0.9
assert F["ripping_rock"]["compacted"] == 1.15
assert F["blasting_rock"]["compacted"] == 1.30
assert 1240 * 0.9 + 5560 * 1.15 == pytest.approx(7510.0)
assert 1240 * 0.9 + 5560 * 1.30 == pytest.approx(8344.0)