# -*- coding: utf-8 -*- """567-1 E03 2D 탐색 지킴 — 수렴(기본 대 촘촘) · 비원호 다출발 · 탐색 한계 = 모형 끝 · 판정 = 원호 Bishop · 비원호 Spencer · M-P 중 작은 값 · 「면 확인」(비원호 < 원호 Bishop × 0.85). ./venv/Scripts/python.exe -m pytest resources/tester/test_567_1_stab_search.py -q -s """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(Path(__file__).resolve().parent)) from E03_StabSection import E03_StabSection_Engine_Search as search # noqa: E402 from E03_StabSection import E03_StabSection_View as view # noqa: E402 from E03_StabSection.E03_StabSection_Engine_Model import side_cases # noqa: E402 from test_532_2_stab_lem_benchmarks import ( # noqa: E402 ACADS1C_MATS, ACADS1C_REGIONS, ACADS_EXT, _acads3_model, _acads4_model, mats, ) from test_532_2_stab_section_model import DESIGN, GROUND # noqa: E402 DENSE_CIRCLE = {"grid": (30, 30, 20), "entry_exit_points": 50} CIRCLE_TOL = 0.005 NONCIRC_TOL = 0.03 def _f(res, method, kind): return (res["methods"][method].get(kind) or {}).get("F") def _diff(a, b): return abs(b / a - 1) def test_circular_default_vs_dense(): # 원호 = 판정 값 · 격자 4 배 · 진입진출 2 배에도 차 ≤ 0.5 % cases = { "1a": { "materials": mats(("s", 20, 3, 19.6)), "regions": [{"material_id": "s", "polygon": ACADS_EXT}], }, "1c": {"materials": ACADS1C_MATS, "regions": ACADS1C_REGIONS}, } for name, model in cases.items(): base = search.analyze(model, direction=-1, noncircular=False) dense = search.analyze(model, direction=-1, noncircular=False, **DENSE_CIRCLE) for m in search.METHODS: a, b = _f(base, m, "circular"), _f(dense, m, "circular") print(f"ACADS {name} {m} 원호 기본 {a:.4f} 촘촘 {b:.4f} 차 {(b / a - 1) * 100:+.2f} %") assert _diff(a, b) <= CIRCLE_TOL def test_noncircular_default_vs_dense(monkeypatch): # 비원호 = 출발 셋 기본 대 출발 여섯 · 걸음 두 배 — 차 ≤ 3 %(기록 · 3a +0.2 % · 4 −1.8 %) for name, make in (("3a", _acads3_model), ("4", _acads4_model)): base = search.analyze(make(), direction=-1, methods=("janbu", "spencer")) with monkeypatch.context() as mp: mp.setattr(search, "NONCIRC_STARTS", 2 * search.NONCIRC_STARTS) mp.setattr(search, "LS_ITER", 2 * search.LS_ITER) mp.setattr(search, "LS_ITER_SPENCER", 2 * search.LS_ITER_SPENCER) dense = search.analyze(make(), direction=-1, methods=("janbu", "spencer")) for m in ("janbu", "spencer"): a, b = _f(base, m, "noncircular"), _f(dense, m, "noncircular") print( f"ACADS {name} {m} 비원호 기본 {a:.4f} 촘촘 {b:.4f} 차 {(b / a - 1) * 100:+.2f} %" ) assert _diff(a, b) <= NONCIRC_TOL def test_multistart_takes_minimum(monkeypatch): # 출발 셋 = 출발 하나(같은 씨앗)를 안에 품음 → 같거나 낮음 model = _acads4_model() with monkeypatch.context() as mp: mp.setattr(search, "NONCIRC_STARTS", 1) one = search.analyze(model, direction=-1, methods=("janbu", "spencer")) three = search.analyze(model, direction=-1, methods=("janbu", "spencer")) assert search.NONCIRC_STARTS == 3 for m in ("janbu", "spencer"): assert _f(three, m, "noncircular") <= _f(one, m, "noncircular") + 1e-12 def test_limits_reach_model_end(): # 탐색 한계 = 노면 반대쪽 끝 ~ 모형 끝(요구 392 · Slide2 기본) design = { "section_mode": "right_cut", "road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}}, } left, right = side_cases(design, GROUND, DESIGN) assert left["limits"] == (-2.0, GROUND[-1][0]) assert right["limits"] == (GROUND[0][0], 2.0) # 설정 — 우 깎기 비탈 끝 −6 · H 3.6 → 옛 정책 −6 − max(5, 2H) · 정한 6 m → −12 assert side_cases(design, GROUND, DESIGN, "2h")[1]["limits"] == (-13.2, 2.0) assert side_cases(design, GROUND, DESIGN, 6.0)[1]["limits"] == (-12.0, 2.0) def _case(bishop_c, spencer_n=None, mp_n=None, cond="wet", side="right", role="cut", ends=None): def found(F, kind): if F is None: return None return {"F": F, "kind": kind, "converged": True, "entry": [-30.0, 20.0], "exit": [2.0, 9.0]} methods = { "bishop": {"circular": found(bishop_c, "circle"), "min": found(bishop_c, "circle")}, "spencer": {"circular": found(9.0, "circle"), "noncircular": found(spencer_n, "polyline")}, "morgenstern_price": {"noncircular": found(mp_n, "polyline")}, } result = {"methods": methods, **({"model_x": ends} if ends else {})} return {"condition": cond, "side": side, "role": role, "result": result} def _row(*cases): meta = {"key": "k", "chainage_m": 0.0} return view.station_row(meta, {"cases": list(cases)}, {"wet": {"cut": 1.2}}) def test_governing_minimum_and_surface_check(): # 비원호 Spencer 1.0 < Bishop 1.3 × 0.85 → 판정 = Spencer 1.0 N.G · 면 확인 c = _row(_case(1.3, 1.0, 1.05))["cases"]["wet"]["right"] assert (c["method"], c["kind"], c["fs"], c["ok"]) == ("spencer", "polyline", 1.0, False) assert c["fs_circular"] == 1.3 and c["surface_check"] is True # 비원호 M-P 1.15 · 15 % 안 → 판정 = M-P N.G · 면 확인 아님 c = _row(_case(1.3, 1.2, 1.15))["cases"]["wet"]["right"] assert (c["method"], c["fs"], c["ok"], c["surface_check"]) == ( "morgenstern_price", 1.15, False, False, ) # 비원호가 더 높음 → Bishop 원호 그대로(Spencer 원호 9.0 은 후보 아님) row = _row(_case(1.3, 1.4, None)) c = row["cases"]["wet"]["right"] assert (c["method"], c["kind"], c["fs"], c["ok"]) == ("bishop", "circle", 1.3, True) assert row["surface_check"] is False and row["verdict"]["wet"] == "ok" # 지배 줄 · 요약도 판정 방법 · 면 확인 수를 따름 row = _row(_case(1.3, 1.0)) assert view.governing_row(row)["method"] == "spencer" assert view.summarize([row], ["wet"])["wet"] == { "min_fs": 1.0, "ng": 1, "surface_check": 1, "edge": 0, } def test_old_result_without_noncircular_keeps_bishop(): # 옛 결과(원호 Bishop min 만) → 판정 그대로 case = { "condition": "wet", "side": "right", "role": "cut", "result": {"methods": {"bishop": {"min": {"F": 1.25, "kind": "circle"}}}}, } c = _row(case)["cases"]["wet"]["right"] assert (c["method"], c["fs"], c["ok"], c["surface_check"]) == ("bishop", 1.25, True, False) def test_edge_flag(): # 판정 면 진입 −30 · 진출 2 — 모형 끝 −30.5 면 1 m 안 = edge · −40 이면 아님 · 모형 끝 모름 = None for ends, edge in (([-30.5, 40.0], True), ([-40.0, 40.0], False), (None, None)): row = _row(_case(1.3, ends=ends)) assert row["cases"]["wet"]["right"]["edge"] is edge assert view.summarize([_row(_case(1.3, ends=[-30.5, 40.0]))], ["wet"])["wet"]["edge"] == 1 def test_section_view_critical_is_governing_surface(): # 단면 화면 임계면 = 판정 면(방법 이름도) — 후보 고리가 방법 이름을 덮지 않음 case = _case(1.3, 1.0, 1.05) out = {"cases": [case]} row = _row(case) model = {"materials": [], "regions": [], "surface": []} a = view.section_view(row, out, model, {})["analyses"][0] assert (a["critical"]["method"], a["critical"]["fs"]) == ("spencer", 1.0)