# -*- coding: utf-8 -*- """536-3 뒷부분 보강재(네일 · 앵커) 2D 한계평형 — 힘 크기 · 능동/수동 · 탐색 한계 그대로 · 보강이 F 를 낮추지 않음 · 보강 없을 때 불변. - 손계산: 평면 쐐기 닫힌 꼴(능동 F = (cL + (W cosα + T sin(α+δ)) tanφ) / (W sinα − T cos(α+δ)) · 수동 T → T/F) · 교점 · 인발 · 벗겨짐 · 정착장 · 프리스트레스. - Slide2 Verification(Slide_SlopeStabilityVerification.pdf): #47 Amherst 네일 벽(수동 · 인발 · 평면 Janbu 0.890 · Sheahan 0.887) · #85 Duncan & Wright 그림 6.34(앵커 1단 · 능동 Bishop 1.531 · GLE 1.575 · 수동 1.324 · 1.378) · #86 그림 7.28(5단 · Bishop 1.629 · Spencer 1.620 · GLE 1.622). #85 Slide Spencer(1.884 · 1.872)는 같은 표 Bishop · GLE · 참값(1.51 · 1.32)과 25 % 넘게 어긋난 값이라 대조에서 뺌. ./venv/Scripts/python.exe -m pytest resources/tester/test_536_3_stab_supports.py -q """ 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)) sys.path.insert(0, str(Path(__file__).resolve().parent)) from test_532_2_stab_section_model import DESIGN, GROUND # noqa: E402 from E03_StabSection.E03_StabSection_Engine_Search import analyze, evaluate_specified # noqa: E402 from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402 MP = "morgenstern_price" def near(v, ref, tol): assert v is not None and abs(v - ref) <= tol * abs(ref), (v, ref) def nail(x, z, angle, L, sp, tens, bond_kn_per_m, **kw): """부착 = 길이당 힘(kN/m) 으로 줌 — bond_kPa = 그 값 / (π·D) · D 100 mm.""" return { "type": "nail", "x": x, "z": z, "angle_deg": angle, "length_m": L, "spacing_m": sp, "tensile_kN": tens, "bond_kPa": bond_kn_per_m / (math.pi * 0.1), "hole_dia_mm": 100, **kw, } # fmt: skip def anchor(x, z, angle, L, sp, tens, bond_kn_per_m, lb, pre, **kw): return {**nail(x, z, angle, L, sp, tens, bond_kn_per_m, **kw), "type": "anchor", "bond_length_m": lb, "prestress_kN": pre} # fmt: skip # ---- 손계산: 평면 쐐기 --------------------------------------------------------------- # 활동 +x · 비탈 (10, 10) → (20, 0) · 면 (4, 10) → 기슭 (20, 0) · 쐐기 넓이 30 m² WEDGE = { "materials": [{"id": "s", "gamma_t": 18.0, "c": 5.0, "phi": 30.0}], "regions": [{"material_id": "s", "polygon": [ [0, -5], [30, -5], [30, 0], [20, 0], [10, 10], [0, 10]]}], } # fmt: skip PLANE = [[4.0, 10.0], [20.0, 0.0]] HEAD, DELTA = (15.0, 5.0), 15.0 def _wedge_closed(T, passive): a, d = math.atan2(10, 16), math.radians(DELTA) W, L, c, t = 30 * 18.0, math.hypot(16, 10), 5.0, math.tan(math.radians(30)) F = 1.5 for _ in range(200): Tf = T / F if passive else T F = (c * L + (W * math.cos(a) + Tf * math.sin(a + d)) * t) / ( W * math.sin(a) - Tf * math.cos(a + d) ) return F @pytest.mark.parametrize("passive", [False, True]) def test_wedge_matches_closed_form(passive): """쐐기 하나 · 보강 한 개(인장 100 kN · 간격 2 m → 50 kN/m) — Janbu 간편 · Spencer = 닫힌 꼴(±0.1 %).""" sup = ( nail(*HEAD, DELTA, 12, 2, 100, 1e6) if passive else anchor(*HEAD, DELTA, 12, 2, 100, 1e6, 12, 100) ) r = evaluate_specified( {**WEDGE, "supports": [sup]}, polyline=PLANE, methods=("janbu", "spencer") ) F = _wedge_closed(50.0, passive) near(r["janbu"]["F0"], F, 1e-3) near(r["spencer"]["F"], F, 1e-3) f = r["janbu"]["support_forces"][0] assert f["mode"] == "tensile" and f["mobilized_kN_per_m"] == pytest.approx(50.0) assert f["force_application"] == ("passive" if passive else "active") assert f["applied_kN_per_m"] == pytest.approx(50.0 / r["janbu"]["F0"] if passive else 50.0) # 보강 효과 — 보강 없는 쐐기보다 높음 · 능동 > 수동(Slide2 와 같음) bare = evaluate_specified(WEDGE, polyline=PLANE, methods=("janbu",))["janbu"]["F0"] assert F > bare and _wedge_closed(50.0, False) > _wedge_closed(50.0, True) def _crossing_t(): """머리 (15, 5) 에서 −x · 아래 15° 로 간 거리 t 가 면 z = 10 − (x − 4)·10/16 과 만나는 곳.""" c, s, k = math.cos(math.radians(DELTA)), math.sin(math.radians(DELTA)), 10 / 16 # 5 − t·s = 10 − (15 − t·c − 4)·k return (5 - 10 + 11 * k) / (s + k * c) def test_capacity_modes_hand_calc(): """교점 거리 t · 인발(부착 × 밖 길이) · 벗겨짐(판 + 부착 × 안 길이) · 앵커 정착장 · 프리스트레스 · 안 지남.""" t = _crossing_t() L, q = 8.0, 20.0 # 길이 · 길이당 부착 kN/m sups = [ nail(*HEAD, DELTA, L, 1, 500, q), # 인발 = q·(L − t) nail(*HEAD, DELTA, L, 1, 500, q, plate_kN=10), # 벗겨짐 = 10 + q·t (< 인발) anchor(*HEAD, DELTA, L, 1, 500, q, 3.0, 0), # 교점이 자유장 — 인발 = q·3(정착장 전부) anchor(*HEAD, DELTA, L, 1, 500, q, 3.0, 40), # 능동 · 프리스트레스 40 < 한도 60 nail(*HEAD, DELTA, t - 0.5, 1, 500, q), # 면에 못 미침 nail(25.0, 0.0, 0.0, 6, 1, 500, q), # 머리가 덩이 밖 ] r = evaluate_specified({**WEDGE, "supports": sups}, polyline=PLANE, methods=("janbu",)) out = r["janbu"]["support_forces"] expect = [ ("pullout", q * (L - t)), ("stripping", 10 + q * t), ("pullout", q * 3.0), ("prestress", 40.0), ("not_crossing", 0.0), ("not_crossing", 0.0), ] assert [o["index"] for o in out] == list(range(len(sups))) for o, (mode, T) in zip(out, expect): assert o["mode"] == mode and o["mobilized_kN_per_m"] == pytest.approx(T, rel=1e-6) x = HEAD[0] - t * math.cos(math.radians(DELTA)) assert out[0]["point"] == pytest.approx([x, HEAD[1] - t * math.sin(math.radians(DELTA))]) assert out[4]["point"] is None # ---- Slide2 Verification ------------------------------------------------------------- def _p47(): return { "materials": [{"id": "a", "gamma_t": 18.9, "c": 25, "phi": 0}], "regions": [{"material_id": "a", "polygon": [ [-6, -2], [11, -2], [11, 6.1], [0, 6.1], [0, 0], [-6, 0]]}], "surcharge": [{"x0": 0, "x1": 0.02, "q": 14.6 / 0.02}], # 숏크리트 무게 14.6 kN/m "supports": [nail(0, z, 20, 4.9, 1.5, 118, 15, plate_kN=86) for z in (4.9, 3.4)], } # fmt: skip def test_slide2_47_soil_nail_wall_planar(): """수동 네일 2단 · 인발이 지배 · 평면 면 Janbu 0.890(Sheahan 0.887) · 탐색도 같은 평면.""" r = evaluate_specified( _p47(), polyline=[[0, 0], [6.279, 6.1]], direction=-1, methods=("janbu",) ) near(r["janbu"]["F"], 0.890, 0.03) near(r["janbu"]["F"], 0.887, 0.01) k = 6.1 / 6.279 c, s = math.cos(math.radians(20)), math.sin(math.radians(20)) for f, z in zip(r["janbu"]["support_forces"], (4.9, 3.4)): t = z / (s + k * c) assert f["mode"] == "pullout" assert f["mobilized_kN_per_m"] == pytest.approx(15 * (4.9 - t) / 1.5, rel=1e-6) a = analyze(_p47(), direction=-1, methods=("bishop", "janbu"), limits=[-6, 11]) near(a["methods"]["janbu"]["min"]["F"], 0.890, 0.03) def _p85(active): return { "materials": [{"id": "c", "gamma_t": 98, "c": 350, "phi": 0}], # ft · psf · pcf "regions": [{"material_id": "c", "polygon": [[15, 10], [57, 10], [57, 30], [25, 30]]}], "supports": [anchor(20, 20, 0, 37, 1, 9000, 1e7, 37, 9000 if active else 0, plate_kN=1e9)], } @pytest.mark.parametrize( "active, bishop, gle", [(True, 1.531, 1.575), (False, 1.324, 1.378)], ) def test_slide2_85_active_passive(active, bishop, gle): a = analyze(_p85(active), direction=-1, methods=("bishop", "spencer", MP)) ms = a["methods"] near(ms["bishop"]["min"]["F"], bishop, 0.03) near(ms[MP]["min"]["F"], gle, 0.03) near(ms["bishop"]["min"]["F"], 1.51 if active else 1.32, 0.03) # Duncan & Wright 참값 f = a["support_forces"][0] assert f["mode"] == "tensile" and f["mobilized_kN_per_m"] == pytest.approx(9000) assert f["force_application"] == ("active" if active else "passive") def _p86(capacity=800): return { "materials": [{"id": "f", "gamma_t": 130, "c": 0, "phi": 37}], "regions": [{"material_id": "f", "polygon": [[0, 0], [75, 0], [75, 24], [30, 24]]}], "supports": [ anchor(5 * k, 4 * k, 0, 20, 1, capacity, 1e7, 20, capacity, plate_kN=1e9) for k in range(1, 6) ], } def test_slide2_86_five_rows(): """Slide 임계 원(중심 (−3.383, 40.466) · R 40.568) 값 · 탐색(전체 붕괴만 — 얕은 면 3 m 아래 뺌).""" r = evaluate_specified(_p86(), circle=[-3.383, 40.466, 40.568], direction=-1) near(r["bishop"]["F"], 1.629, 0.03) near(r["spencer"]["F"], 1.620, 0.03) near(r[MP]["F"], 1.622, 0.03) assert all(f["mode"] == "tensile" for f in r["bishop"]["support_forces"]) a = analyze(_p86(), direction=-1, methods=("bishop", "spencer"), min_depth=3.0) near(a["methods"]["bishop"]["min"]["F"], 1.629, 0.03) near(a["methods"]["spencer"]["min"]["F"], 1.620, 0.03) def test_search_keeps_limits_with_strong_supports(): """아주 센 보강 — 탐색 한계 그대로(보강 없는 해석과 같은 면 무리) · 임계 F 가 보강 없음 이상.""" bare = {k: v for k, v in _p86().items() if k != "supports"} lim = [0, 45] a0 = analyze(bare, direction=-1, methods=("bishop",), min_depth=3.0, limits=lim) a = analyze(_p86(1e5), direction=-1, methods=("bishop",), min_depth=3.0, limits=lim) assert a0["limits"] == a["limits"] == [0, 45] assert a["support_forces_method"] == "bishop" assert a["methods"]["bishop"]["min"]["F"] >= a0["methods"]["bishop"]["min"]["F"] # ---- 보강은 F 를 낮추지 않음(요구 351 · sub2 실측 0+720 흠) --------------------------------- ALL = ("ordinary", "bishop", "janbu", "spencer", MP) #: 쐐기 비탈 (10, 10) → (20, 0) 위 면 — 얕은 · 깊은 평면 · 두 마디 · 원호(중심 · 반지름) PLANES = [[[x0, 10.0], [20.0, 0.0]] for x0 in (3.0, 5.0, 7.0, 9.0)] + [ [[2.0, 10.0], [12.0, 3.0], [20.0, 0.0]], [[4.0, 10.0], [14.0, -1.0], [22.0, 0.0]], ] CIRCLES = [[21.0, 19.45, 19.45], [19.0, 13.338, 13.422], [24.0, 27.0, 27.019]] def _sups(kind, L): heads = [(12.0, 8.0), (15.0, 5.0), (18.0, 2.0)] if kind == "nail": return [nail(x, z, 15, L, 1.5, 120, 40) for x, z in heads] return [anchor(x, z, 15, L, 1.5, 300, 60, min(4.0, L), 80) for x, z in heads] @pytest.mark.parametrize("kind", ["nail", "anchor"]) @pytest.mark.parametrize("L", [3.0, 8.0, 25.0]) def test_supports_never_lower_same_surface(kind, L): """같은 면 — 네일(수동) · 앵커(능동) · 길이 셋 · 다섯 방법 · 면 깊이 여럿: 보강 F ≥ 보강 없음 F.""" with_s = {**WEDGE, "supports": _sups(kind, L)} checked = 0 for spec in [{"polyline": p} for p in PLANES] + [{"circle": c} for c in CIRCLES]: r0 = evaluate_specified(WEDGE, methods=ALL, **spec) r1 = evaluate_specified(with_s, methods=ALL, **spec) for m in ALL: f0, f1 = r0[m]["F"], r1[m]["F"] if f0 is None or f1 is None: continue assert f1 >= f0 - 1e-9, (kind, L, spec, m, f0, f1) checked += 1 assert checked >= 40 def test_station_longer_nails_never_lower(): """측점 꼴(우 깎기) — 네일을 길게 해도 임계 F 가 보강 없음 아래로 안 감(탐색 한계 그대로).""" base = {c["side"]: c for c in _station(None)["cases"]}["right"] f0 = base["min_F"]["bishop"] prev = f0 for L in (3.0, 8.0, 15.0, 25.0): case = {c["side"]: c for c in _station([nail(-4.0, 11.48, 15, L, 1.5, 200, 30)])["cases"]} right = case["right"] assert right["result"]["limits"] == base["result"]["limits"] f = right["min_F"]["bishop"] assert f >= f0 - 1e-9 and f >= prev - 0.02 * prev, (L, f0, prev, f) prev = f # ---- 보강 없을 때 불변 · 측점 꼴 ------------------------------------------------------ def test_no_supports_unchanged(): """supports 없음 · 빈 목록 · 덩이 밖 보강재 = 같은 값 · 결과 칸 support_forces 는 보강 줄 때만.""" base = evaluate_specified(WEDGE, polyline=PLANE) empty = evaluate_specified({**WEDGE, "supports": []}, polyline=PLANE) far = evaluate_specified({**WEDGE, "supports": [nail(25, 0, 0, 3, 1, 100, 50)]}, polyline=PLANE) for m, v in base.items(): assert empty[m]["F"] == v["F"] and far[m]["F"] == pytest.approx(v["F"], abs=1e-12) assert "support_forces" not in v and far[m]["support_forces"][0]["mode"] == "not_crossing" def _station(supports): cross = {"chainage_m": 0.0, "samples": [{"offset_m": x, "elevation_m": z} for x, z in GROUND]} design = { "section_mode": "right_cut", "design_line": [{"offset_m": x, "elevation_m": z} for x, z in DESIGN], "road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}}, } return analyze_station( cross, design, conditions=("dry",), methods=("bishop",), noncircular=False, supports=supports, ) def test_station_side_and_result_slot(): """우 깎기 면 네일 — 우 비탈 경우에만 힘 · 좌 비탈은 보강 없는 값 그대로 · cases[i].result.support_forces.""" plain = _station(None) sups = [nail(-4.0, 11.48, 15, 8, 1.5, 200, 30)] res = _station(sups) by = {c["side"]: c for c in res["cases"]} old = {c["side"]: c for c in plain["cases"]} assert "support_forces" not in old["right"]["result"] assert ( by["left"]["min_F"] == old["left"]["min_F"] and by["left"]["result"]["support_forces"] == [] ) f = by["right"]["result"]["support_forces"] assert len(f) == 1 and set(f[0]) >= {"index", "mobilized_kN_per_m", "mode"} assert by["right"]["min_F"]["bishop"] > old["right"]["min_F"]["bishop"] # "side" 칸이 머리 x 보다 먼저 — 좌로 돌리면 좌 비탈에만(좌 성토 면 밖이라 not_crossing) left = _station([{**sups[0], "side": "left"}]) by_l = {c["side"]: c for c in left["cases"]} assert by_l["right"]["result"]["support_forces"] == [] assert by_l["right"]["min_F"] == old["right"]["min_F"] if __name__ == "__main__": sys.exit(pytest.main([__file__, "-q"])) def test_station_nail_length_vs_fs_monotonic(): """측점 꼴 — 네일 3행 길이 3~25 m: 임계 F 가 길이에 따라 줄지 않음(포화 뒤 평평 허용) · 항상 보강 없음 이상 · 탐색 한계 그대로(요구 351).""" base = {c["side"]: c for c in _station(None)["cases"]}["right"] f0 = base["min_F"]["bishop"] heads = [(-4.0, 11.48), (-4.6, 12.98), (-5.2, 14.48)] prev = f0 for L in (3.0, 6.0, 7.5, 9.0, 12.0, 25.0): sups = [nail(x, z, 15, L, 1.5, 121.6, 40 * math.pi * 0.1) for x, z in heads] right = {c["side"]: c for c in _station(sups)["cases"]}["right"] assert right["result"]["limits"] == base["result"]["limits"] f = right["min_F"]["bishop"] assert f >= f0 - 1e-9 and f >= prev - 0.005, (L, f0, prev, f) prev = f assert prev > f0