"""㉳ (가) 미교차 측점 지반 샘플을 지표면 끝까지 넓힘 — 2026-09-14 브레인 판정. 반폭 20m 끝에서 성토 비탈이 원지반과 안 만나면 면적이 잘려 성토가 작게 섰음(936be972 12곳 중 11곳). ⇒ 열린 쪽만 +5m 씩 확정 지표면에서 다시 떠 계산 · 닫히면 멈춤 · 지표면 밖(무효 샘플)이면 멈추고 미교차 그대로. 상한 숫자 없음(지표면이 실제로 있는 끝) · 716 「미교차만 +5m 한 번」 갈음. """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pytest ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from B06_Section.B06_Section_Engine_SampleExtend import extend_unclosed # noqa: E402 from common_util.common_util_surface_sampler import CallableSurfaceSampler # noqa: E402 FRAME = {"origin": {"x": 0.0, "y": 0.0}, "left_xy": [1.0, 0.0]} def _ground(x: np.ndarray) -> np.ndarray: """좌(+)는 30m 까지 1:1 로 떨어지다 완만 · 우(−)는 오르막(절토가 금방 닫힘).""" x = np.asarray(x, dtype=float) left = np.where(x < 30.0, 100.0 - x, 70.0 - 0.1 * (x - 30.0)) return np.where(x >= 0, left, 100.0 - 0.8 * x) def _samples(half: float = 20.0) -> list[dict]: offsets = np.arange(-half, half + 0.25, 0.5) return [ {"offset_m": float(o), "elevation_m": float(z), "valid": True} for o, z in zip(offsets, _ground(offsets)) ] def _compute(samples: list[dict]) -> dict: return compute_cross_design(samples, 100.0, ground_type="soil", section_mode="right_cut") def _sampler(limit: float | None = None) -> CallableSurfaceSampler: def function(xy: np.ndarray) -> np.ndarray: values = _ground(xy[:, 0]) if limit is not None: values = np.where(xy[:, 0] > limit, np.nan, values) return values return CallableSurfaceSampler(function) def test_열린_쪽만_5m_씩_넓혀_닫히면_멈춤() -> None: before = _compute(_samples()) assert before["slope_unclosed"], "기준 단면이 20m 에서 안 닫혀야 시험이 뜻이 있음" samples, info = extend_unclosed(_samples(), FRAME, _compute, _sampler()) after = _compute(samples) assert not after["slope_unclosed"] assert info["left_m"] > 0 and info["left_m"] % 5 == 0 and info["right_m"] == 0 assert info["surface_end"] is False assert max(s["offset_m"] for s in samples) == pytest.approx(20.0 + info["left_m"]) assert min(s["offset_m"] for s in samples) == pytest.approx(-20.0) # 닫힌 쪽은 안 넓힘 assert after["fill_area_m2"] > before["fill_area_m2"] # 잘린 성토가 제 크기로 def test_지표면_끝이면_멈추고_미교차_그대로() -> None: samples, info = extend_unclosed(_samples(), FRAME, _compute, _sampler(limit=32.0)) assert info["surface_end"] is True assert max(s["offset_m"] for s in samples) == pytest.approx(30.0) # 무효가 섞인 걸음은 안 붙임 assert _compute(samples)["slope_unclosed"] def test_닫힌_측점은_그대로() -> None: closed = _samples(60.0) assert extend_unclosed(closed, FRAME, _compute, _sampler()) is None def test_서버_재계산이_부름() -> None: source = (ROOT / "B06_Section" / "B06_Section_Server_Calc_Prebuild.py").read_text("utf-8") assert "extend_unclosed_sections(" in source