Files
Aislo/resources/tester/test_536_3_stab_supports.py
T
eomsangdonandClaude Sonnet 5.5 d9357a5f58 test(536-3): 측점 꼴 네일 길이 대 안전율 단조 · 보강 없음 이상 시험
- 네일 3행 L 3~25 m 임계 F 줄지 않음 · 탐색 한계 그대로 (요구 351 · sub4 0ef659b0d 뒤)
- 0+720 우 깎기 우기: 보강 없음 1.027 → L 3 1.049 · 7.5 1.219 · 25 1.487 · 최소 길이 7.5 m

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 04:56:42 +09:00

335 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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