"""사면길이 유도·사면 4계열 면적 검사 — PLAN 8-4b·8-11. 사면길이는 저장분에서 **유도**하는 값이라 판정 규칙이 곧 정확성이다. 그래서 실측 설계선 모양을 그대로 넣어 사면과 원지반이 갈리는지를 못 박는다. """ from __future__ import annotations import math 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_SlopeArea import ( # noqa: E402 SlopeRatios, build_rows, build_table, totals, unclosed_stations, ) from B08_Quantity.B08_Quantity_Engine_SlopeLength import ( # noqa: E402 StationSlope, station_slope, ) def line(points: list[tuple[float, float]]) -> list[dict[str, float]]: return [{"offset_m": o, "elevation_m": z} for o, z in points] def 절토_설계선() -> dict: """측점 20.0 **실측 모양 그대로** — 노면·측구·2단 절토 사면·원지반 (우측). 실측 구간 기울기: 노면 n=33.3 · 측구 벽 n=1.0 · 절토(암) n=0.4 · 절토(토사) n=1.0 · 원지반 n=1.63. 측구 바닥은 노면보다 **낮다**(깊이 0.3). ⚠ 측구 벽이 토사 절토비(1.0)와 같은 경사라, 이 모양이 곧 「측구를 사면으로 세지 않는가」 를 재는 시험이다. """ return { # 실측 표고를 그대로 세운 것 — 노면(-2.00) 870.83 을 기준으로 바깥으로 쌓았다. "design_line": line( [ (-5.00, 873.73), # 원지반 (n=1.63) (-4.50, 873.32), # 절토 토사 n=1.0 (-4.00, 872.82), # 절토 토사 n≈1.03 (-3.50, 872.33), # 절토 암 n=0.4 (-3.00, 871.08), # 절토 암 n=0.4 (-2.90, 870.83), (-2.60, 870.53), # 측구 바깥 벽 n=1.0 — 사면이 아니다 (-2.30, 870.53), # 측구 바닥 (노면보다 0.3 낮다) (-2.00, 870.83), # 노체 끝 (0.00, 870.89), # 노면 ] ), "road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}}, "ditch_enabled": True, "ditch_side": "right", "ditch": {"type": "standard", "top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3}, "cut_slope_ratio": 0.4, "soil_cut_slope_ratio": 1.0, "fill_slope_ratio": 1.2, "two_stage_slope": True, } def 성토_설계선() -> dict: """성토 사면 n=1.2 가 이어지다 원지반(n=2.5)에서 끊긴다.""" return { "design_line": line( [ (2.0, 870.00), # 노체 끝 (4.4, 868.00), # 성토 사면 n=1.2 (6.8, 866.00), (9.8, 864.80), # 원지반 n=2.5 ] ), "road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}}, "cut_slope_ratio": 0.4, "soil_cut_slope_ratio": 1.0, "fill_slope_ratio": 1.2, } # ── 사면길이 유도 ──────────────────────────────────────────────────── def test_절토_사면만_잡고_원지반은_끊음() -> None: slope = station_slope(20.0, 절토_설계선()) # 암 두 조각(0.10/0.25 · 0.50/1.25) + 토사 두 조각(0.50/0.49 · 0.50/0.50). # 원지반(-4.50~-5.00, n=1.22)은 경사비가 안 맞아 안 든다. expected = ( math.hypot(0.10, 0.25) + math.hypot(0.50, 1.25) + math.hypot(0.50, 0.49) + math.hypot(0.50, 0.50) ) assert slope.cut_length_m == pytest.approx(expected, rel=0.02) def test_절토_2단이_암토사로_갈림() -> None: """8-11 의 암 5분류는 설계자 % 입력 몫 — 여기서는 2단 경계까지만 본다.""" slope = station_slope(20.0, 절토_설계선()) materials = {segment.material for segment in slope.segments if segment.role == "cut"} assert materials == {"rock", "soil"} def test_측구는_사면이_아님() -> None: """측구 벽도 n=1.0 이라 토사 절토비와 같다 — 노체 안쪽이므로 세면 안 된다.""" slope = station_slope(20.0, 절토_설계선()) for segment in slope.segments: assert segment.from_offset_m <= -2.9 + 1e-6, f"측구 구간이 섞였다: {segment}" def test_성토_사면도_유도됨() -> None: """원지반선 없이도 경사비 불일치로 끊긴다 — 이 길이 3번의 갈림길이었다.""" slope = station_slope(40.0, 성토_설계선()) assert slope.fill_length_m == pytest.approx(math.hypot(4.8, 4.0), rel=0.02) assert slope.cut_length_m == 0.0 def 성토_실측_설계선() -> dict: """route 150 측점 20.0 **좌측 실측 좌표** — 성토 사면이 원지반을 만나 끊기는 자리. 2.00~6.95 가 n=1.2 로 일정하고 6.95 에서 n=2.0 으로 꺾인다. 그 꺾임이 곧 원지반이다. ⚠ 이 시험이 있는 까닭 — 「성토 사면적이 실무의 2.8배」라는 의심이 들어 손으로 따라간 자리다. 결과는 **엔진이 맞았고** 삼각형 근사(평지 가정)로 견준 쪽이 틀렸다. 지반이 기울어 있으면 같은 면적이라도 사면이 훨씬 길어진다. """ return { "design_line": line( [ (2.00, 870.323), (2.50, 869.906), (3.00, 869.490), (3.50, 869.073), (4.00, 868.656), (4.50, 868.240), (5.00, 867.823), (5.50, 867.406), (6.00, 866.990), (6.50, 866.573), (6.95, 866.202), # 여기까지가 사면 — 낙차 4.121m (7.00, 866.177), # 원지반 n=2.0 (7.50, 865.921), (8.00, 865.716), ] ), "road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}}, "ditch_enabled": True, "ditch_side": "right", # 좌측에는 측구가 없다 "ditch": {"top_width_m": 0.9}, "fill_slope_ratio": 1.2, "cut_slope_ratio": 0.4, "soil_cut_slope_ratio": 1.0, } def test_성토_사면이_원지반에서_정확히_끊김() -> None: """실측 좌표로 잰다 — 사면길이 6.44m(수평 4.95 · 낙차 4.121).""" slope = station_slope(20.0, 성토_실측_설계선()) assert slope.fill_length_m == pytest.approx(math.hypot(4.95, 4.121), rel=0.005) assert slope.fill_length_m == pytest.approx(6.44, abs=0.01) # 원지반(7.00 바깥)은 한 조각도 안 든다. assert max(s.to_offset_m for s in slope.segments) == pytest.approx(6.95) def test_잘린_측점은_표시가_따라옴() -> None: design = dict(성토_설계선(), slope_unclosed=True) assert station_slope(40.0, design).unclosed is True def test_설계선이_없으면_0() -> None: assert station_slope(0.0, {}).cut_length_m == 0.0 # ── 사면 4계열 면적 ────────────────────────────────────────────────── def 사면들() -> list[StationSlope]: return [ StationSlope(chainage_m=0.0, cut_length_m=0.0, fill_length_m=0.0), StationSlope(chainage_m=20.0, cut_length_m=4.0, fill_length_m=6.0), StationSlope(chainage_m=40.0, cut_length_m=6.0, fill_length_m=2.0), ] def test_면적은_평균단면적법() -> None: """토적표와 같은 식이다 — 체적 대신 면적이 나올 뿐 계산을 두 벌로 짜지 않는다.""" rows = build_rows(사면들()) assert rows[1].areas["face_dressing_cut"] == pytest.approx((0.0 + 4.0) / 2 * 20) assert rows[2].areas["face_dressing_cut"] == pytest.approx((4.0 + 6.0) / 2 * 20) assert rows[2].areas["face_dressing_fill"] == pytest.approx((6.0 + 2.0) / 2 * 20) def test_첫_측점은_면적이_없음() -> None: assert all(value == 0.0 for value in build_rows(사면들())[0].areas.values()) def test_층따기는_성토면만() -> None: rows = build_rows(사면들()) assert "bench_cut_fill" in rows[1].areas assert "bench_cut_cut" not in rows[1].areas def test_법면보호공은_면고르기와_같은_값() -> None: """기본은 참조다 — 실무 시트가 그러했고 오솔길 산출에는 보호공이 비어 있었다.""" row = build_rows(사면들())[2] assert row.areas["slope_protection_cut"] == pytest.approx(row.areas["face_dressing_cut"]) assert row.areas["slope_protection_fill"] == pytest.approx(row.areas["face_dressing_fill"]) def test_반영률_기본은_100퍼센트() -> None: """실무 관측 80/50/80 은 참고일 뿐 기본값이 아니다 — 법대로 방침(8-10 ★).""" ratios = SlopeRatios() assert (ratios.bench_cut, ratios.face_dressing, ratios.slope_protection, ratios.tree_removal) == ( 1.0, 1.0, 1.0, 1.0, ) def test_반영률을_주면_곱해짐() -> None: plain = build_rows(사면들())[2].areas["face_dressing_cut"] scaled = build_rows(사면들(), SlopeRatios(face_dressing=0.8))[2].areas["face_dressing_cut"] assert scaled == pytest.approx(plain * 0.8) def test_잘린_측점은_목록으로_드러남() -> None: """조용히 적게 내면 안 된다 — 화면이 이 목록을 그대로 보인다.""" slopes = 사면들() slopes[1].unclosed = True rows = build_rows(slopes) assert unclosed_stations(rows) == [20.0] def test_합계() -> None: rows = build_rows(사면들()) total = totals(rows) assert total["face_dressing_cut"] == pytest.approx( sum(row.areas["face_dressing_cut"] for row in rows) ) def test_표_모양() -> None: table = build_table(사면들()) assert table["method"] == "average_end_area" assert table["protection_source"] == "face_dressing" assert table["station_count"] == 3 assert set(table["ratios"]) == { "bench_cut", "face_dressing", "slope_protection", "tree_removal", } assert len(table["rows"]) == 3