Files
Aislo/resources/tester/test_b08_slope_area.py
eomsangdonandClaude Opus 5 d7cb14f416 test(B08): tmp/tests 중 tester 에 없던 47 개를 resources/tester 로 옮김
tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가
저절로 건너가도록 git 안으로 옮김. 사용자 확정.

- 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠.
- 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀.
- helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라
  복사본을 도로 뺌(시험이 .cjs 를 부름).
- resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0.

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

261 lines
10 KiB
Python

"""사면길이 유도·사면 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