- 네일 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>
335 lines
15 KiB
Python
335 lines
15 KiB
Python
# -*- 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
|