"""토적표 엔진 검사 — PLAN 8-4b·8-16. 정답지는 거창 실무 토적표(오솔길 `1.BOM` 과 1:1)다. 평균단면적법이 실물과 같은 값을 내는지, 첫 측점에 체적이 없는지, 값을 자르지 않는지를 못 박는다. """ 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_EarthworkTable import ( # noqa: E402 StationArea, build_rows, build_table, totals, ) from config.config_system_design import EARTHWORK_CONVERSION_FACTORS # noqa: E402 SOIL_C = EARTHWORK_CONVERSION_FACTORS["soil"]["compacted"] def 거창_앞_네_측점() -> list[StationArea]: """거창 `1.BOM` 앞부분 — 절토 토사만 있고 측구는 단면 0.18㎡ 고정인 구간. BOM 값: 0(0.00) · 0+10(1.89) · 1(1.73) · 1+10(1.45), 거리 10m 등간. """ return [ StationArea(chainage_m=0.0, cut_soil_area_m2=0.00, ditch_area_m2=0.0), StationArea(chainage_m=10.0, cut_soil_area_m2=1.89, ditch_area_m2=0.18), StationArea(chainage_m=20.0, cut_soil_area_m2=1.73, ditch_area_m2=0.18), StationArea(chainage_m=30.0, cut_soil_area_m2=1.45, ditch_area_m2=0.18), ] def test_첫_측점은_체적이_없음() -> None: """앞 측점이 없으면 평균을 낼 수 없다 — 실무 토적표도 첫 행 체적이 비어 있다.""" rows = build_rows(거창_앞_네_측점()) assert rows[0].distance_m == 0.0 assert rows[0].cut_soil_volume_m3 == 0.0 assert rows[0].adjusted_total_m3 == 0.0 def test_평균단면적법_실무값_재현() -> None: """BOM 0+10 행: 단면적 1.89 → 체적 9.45 → 보정 8.50 (거리 10m).""" rows = build_rows(거창_앞_네_측점()) row = rows[1] assert row.distance_m == pytest.approx(10.0) assert row.cut_soil_volume_m3 == pytest.approx(9.45) # (0.00+1.89)/2 × 10 assert row.cut_soil_adjusted_m3 == pytest.approx(9.45 * SOIL_C) assert row.cut_soil_adjusted_m3 == pytest.approx(8.505) # BOM 표기 8.50 def test_평균단면적법_다음_측점() -> None: """BOM 1 행: (1.89+1.73)/2 × 10 = 18.10 → 보정 16.29.""" row = build_rows(거창_앞_네_측점())[2] assert row.cut_soil_volume_m3 == pytest.approx(18.10) assert row.cut_soil_adjusted_m3 == pytest.approx(16.29) def test_측구_단면_0_18이_체적으로() -> None: """측구 0.18㎡ 가 이어지면 10m 당 1.80㎥ — BOM 10열과 같다.""" rows = build_rows(거창_앞_네_측점()) assert rows[2].ditch_soil_volume_m3 == pytest.approx(1.80) assert rows[2].ditch_soil_adjusted_m3 == pytest.approx(1.62) def test_보정량계는_네_갈래_합() -> None: row = build_rows(거창_앞_네_측점())[2] assert row.adjusted_total_m3 == pytest.approx( row.cut_soil_adjusted_m3 + row.cut_rock_adjusted_m3 + row.ditch_soil_adjusted_m3 + row.ditch_rock_adjusted_m3 ) assert row.adjusted_total_m3 == pytest.approx(16.29 + 1.62) # BOM 15열 17.91 def test_누가토량은_차인토량의_누계() -> None: """유토곡선의 원본이 이 열이다 — 누계가 어긋나면 곡선이 통째로 틀린다.""" rows = build_rows(거창_앞_네_측점()) running = 0.0 for row in rows: running += row.balance_m3 assert row.cumulative_m3 == pytest.approx(running) def test_성토가_있으면_유용토는_작은_쪽() -> None: stations = [ StationArea(chainage_m=0.0), StationArea(chainage_m=10.0, cut_soil_area_m2=2.0, fill_area_m2=8.0), ] row = build_rows(stations)[1] assert row.adjusted_total_m3 == pytest.approx(10.0 * SOIL_C) # 절취 10㎥ → 보정 9㎥ assert row.fill_volume_m3 == pytest.approx(40.0) assert row.diverted_m3 == pytest.approx(9.0) # 둘 중 작은 쪽 assert row.balance_m3 == pytest.approx(9.0 - 40.0) def test_암은_토사와_다른_계수() -> None: """토사 0.90 · 리핑암 1.15 — 한 계수로 뭉치면 암 물량이 틀린다.""" stations = [ StationArea(chainage_m=0.0), StationArea(chainage_m=10.0, cut_rock_area_m2=2.0, cut_rock_kind="ripping_rock"), ] row = build_rows(stations)[1] assert row.cut_rock_volume_m3 == pytest.approx(10.0) assert row.cut_rock_adjusted_m3 == pytest.approx( 10.0 * EARTHWORK_CONVERSION_FACTORS["ripping_rock"]["compacted"] ) def test_설계가_가른_값을_그대로_읽음() -> None: """측구 가름의 주인은 횡단 설계다 — 토적표가 절토 비율로 다시 나누면 안 된다. 절토 비율(3:1)로 안분하면 0.30/0.10 이 되지만, 설계는 암반 경계선으로 0.05/0.35 를 냈다. 그 값이 그대로 서야 횡단도와 토적표의 숫자가 같아진다. """ stations = [ StationArea(chainage_m=0.0), StationArea( chainage_m=10.0, cut_soil_area_m2=3.0, cut_rock_area_m2=1.0, ditch_area_m2=0.4, ditch_soil_area_m2=0.05, ditch_rock_area_m2=0.35, ditch_split_basis="rock_boundary", cut_rock_kind="ripping_rock", ), ] row = build_rows(stations)[1] assert row.ditch_soil_area_m2 == pytest.approx(0.05) assert row.ditch_rock_area_m2 == pytest.approx(0.35) assert row.ditch_split_basis == "rock_boundary" assert row.notes == [] # 설계값을 읽었으므로 폴백 사유가 없다. def test_못_가른_측점은_사유와_함께_전량_암() -> None: """암 지반인데 암반 경계선이 없으면 설계가 전량 암으로 낸다 — 여기서 손보지 않는다.""" stations = [ StationArea(chainage_m=0.0), StationArea( chainage_m=10.0, cut_soil_area_m2=2.0, cut_rock_area_m2=2.0, ditch_area_m2=0.18, ditch_soil_area_m2=0.0, ditch_rock_area_m2=0.18, ditch_split_basis="rock_ground_no_boundary", cut_rock_kind="blasting_rock", ), ] row = build_rows(stations)[1] assert row.ditch_soil_area_m2 == 0.0 # 절반씩 임의로 나누지 않는다. assert row.ditch_rock_area_m2 == pytest.approx(0.18) assert row.ditch_split_basis == "rock_ground_no_boundary" def test_설계값이_없을_때만_절토_비율로_안분() -> None: """가름이 붙기 전 저장분 — 폴백으로 안분하되 사유를 남겨 설계값과 구분한다.""" stations = [ StationArea(chainage_m=0.0), StationArea( chainage_m=10.0, cut_soil_area_m2=3.0, cut_rock_area_m2=1.0, ditch_area_m2=0.4, cut_rock_kind="ripping_rock", ), ] row = build_rows(stations)[1] assert row.ditch_soil_area_m2 == pytest.approx(0.3) assert row.ditch_rock_area_m2 == pytest.approx(0.1) assert row.ditch_split_basis == "cut_area_ratio_fallback" assert row.notes and "안분" in row.notes[0] def test_값을_자르지_않을것() -> None: """품셈 1-2-2 는 표기 규칙이다 — 엔진은 전정밀로 둔다(PLAN 8-16).""" stations = [StationArea(chainage_m=0.0), StationArea(chainage_m=7.0, cut_soil_area_m2=1.0)] row = build_rows(stations)[1] assert row.cut_soil_volume_m3 == pytest.approx(3.5) assert row.cut_soil_adjusted_m3 == pytest.approx(3.15) assert repr(row.cut_soil_adjusted_m3) != "3.2" def test_합계행() -> None: rows = build_rows(거창_앞_네_측점()) total = totals(rows) assert total["distance_m"] == pytest.approx(30.0) assert total["cut_soil_volume_m3"] == pytest.approx(sum(r.cut_soil_volume_m3 for r in rows)) def test_표_모양() -> None: table = build_table(거창_앞_네_측점()) assert table["method"] == "average_end_area" assert table["station_count"] == 4 assert table["conversion_factors"] is EARTHWORK_CONVERSION_FACTORS assert len(table["rows"]) == 4 assert "cut_soil_adjusted_m3" in table["rows"][1] def test_설계결과에서_담기() -> None: """B06 설계 결과의 키 이름을 그대로 받는다 — 이름이 어긋나면 0 이 조용히 들어간다.""" design = { "cut_soil_area_m2": 1.5, "cut_rock_area_m2": 0.5, "fill_area_m2": 2.0, "ditch_area_m2": 0.18, "ditch_soil_area_m2": 0.12, "ditch_rock_area_m2": 0.06, "ditch_split_basis": "rock_boundary", "cut_rock_kind": "blasting_rock", } area = StationArea.from_design(20.0, design) assert area.chainage_m == 20.0 assert area.cut_soil_area_m2 == 1.5 assert area.cut_rock_kind == "blasting_rock" assert area.ditch_soil_area_m2 == 0.12 assert area.ditch_rock_area_m2 == 0.06 assert area.ditch_split_basis == "rock_boundary" def test_가름이_없는_저장분은_None으로_받음() -> None: """0.0 과 「설계가 안 냄」은 다르다 — None 이라야 폴백이 선다.""" area = StationArea.from_design(20.0, {"cut_soil_area_m2": 1.5, "ditch_area_m2": 0.18}) assert area.ditch_soil_area_m2 is None assert area.ditch_rock_area_m2 is None assert area.ditch_split_basis is None def test_측점_순서가_뒤죽박죽이어도_이정순으로() -> None: stations = [ StationArea(chainage_m=20.0, cut_soil_area_m2=1.0), StationArea(chainage_m=0.0), StationArea(chainage_m=10.0, cut_soil_area_m2=2.0), ] rows = build_rows(stations) assert [r.chainage_m for r in rows] == [0.0, 10.0, 20.0]