- 운반표를 만든 한 곳(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
310 lines
12 KiB
Python
310 lines
12 KiB
Python
"""운반 가중평균 검사 — 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)
|