Files
Aislo/resources/tester/test_532_2_stab_lem_benchmarks.py
T
eomsangdonandClaude Opus 5.5 bfd87f3e3e feat(E03): 2D 지층 모델 빌더 · 한계평형 솔버(Fellenius · Bishop · Janbu 보정 · Spencer) · 원호 · 비원호 탐색 (532-2)
- 모델: B06 횡단(지반선 · 설계선 · 암 경계 · 성토체 · 벽) → 지층 폴리곤 · 지하수 · 노면 하중 · 비탈별 방향 · 탐색 한계
- 솔버: 한 벌 절편 식(λ = 0 Bishop · Janbu / Spencer 모멘트 · 힘 동시) · 간극수압 none · r_u · 지하수위선 · kh · 상재하중 · 인장균열
- 탐색: Entry & Exit · Grid & Radius · 차분진화 다듬기 · 블록 씨앗 + 꼭짓점 국부 최적화 + Nelder-Mead
- 출력: 방법별 최소 안전율 · 임계면 좌표 · 절편표 · 등안전율 격자 · 수렴 정보
- 시험: Slide2 검증 예제 12 문제(ACADS 1a~4 · F&K · Loukidis · Arai & Tagyo · Yamagami & Ueta) ±3 % · 면적 = B06

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:07:14 +09:00

349 lines
13 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 -*-
"""532-2 E03 2D 한계평형 솔버 — 공개 검증 예제 대조(±3 %).
출처(기대값 = 원문 그대로):
- S2VM = Rocscience Slide2 Slope Stability Verification Manual (2022) — 지식DB
`original/안정성평가/참고문헌/Rocscience_Slide2_SlopeStabilityVerification.pdf` · 문제 번호 = 그 책 번호.
- ACADS = Giam & Donald (1989) ACADS 사면 시험문제(S2VM #1~#9 에 실림).
- FK = Fredlund & Krahn (1977) Can. Geotech. J. 14(3) (S2VM #21).
- AT = Arai & Tagyo (1985) · YU = Yamagami & Ueta (1988) · LOUK = Loukidis et al. (2003) (S2VM #14~#17 · #62).
- 지정 활동면(원 · 꺾은선)은 방법 대조 · 탐색 문제는 상용 SW 탐색 결과 대조.
- Ordinary(Fellenius) 간극수압 = F&K 식 N = W·cosα − u·l (지식DB 02 §2.1) — Slide 는 (W − u·b)·cosα 라 r_u 문제 값이 다름.
./venv/Scripts/python.exe -m pytest resources/tester/test_532_2_stab_lem_benchmarks.py -q
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from shapely.geometry import Polygon, box
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from E03_StabSection.E03_StabSection_Engine_Search import analyze, evaluate_specified # noqa: E402
TOL = 0.03
def mats(*rows):
return [{"id": i, "name": i, "gamma_t": g, "c": c, "phi": p} for i, g, c, p in rows]
def near(got, expected, tol=TOL):
assert got is not None, "안전율 없음"
assert abs(got / expected - 1) <= tol, (
f"{got:.4f} vs {expected} ({(got / expected - 1) * 100:+.2f} %)"
)
def circ(res, method):
return (res["methods"][method].get("circular") or {}).get("F")
def nonc(res, method):
return (res["methods"][method].get("noncircular") or {}).get("F")
ACADS_EXT = [[20, 20], [70, 20], [70, 35], [50, 35], [30, 25], [20, 25]]
ACADS1C_REGIONS = [
{
"material_id": "soil1",
"polygon": [[30, 25], [50, 35], [70, 35], [70, 31], [54, 31], [50, 29], [40, 27]],
},
{
"material_id": "soil2",
"polygon": [[40, 27], [50, 29], [54, 31], [70, 31], [70, 24], [52, 24]],
},
{
"material_id": "soil3",
"polygon": [[20, 20], [70, 20], [70, 24], [52, 24], [40, 27], [30, 25], [20, 25]],
},
]
ACADS1C_MATS = mats(("soil1", 19.5, 0, 38), ("soil2", 19.5, 5.3, 23), ("soil3", 19.5, 7.2, 20))
def test_acads_1a_homogeneous_circular_search():
# S2VM #1 (ACADS 1a) — Bishop 0.987 · Spencer 0.986 · Janbu 보정 0.990 · 심판값 1.00
m = {
"materials": mats(("s", 20, 3, 19.6)),
"regions": [{"material_id": "s", "polygon": ACADS_EXT}],
}
r = analyze(m, direction=-1, noncircular=False)
near(circ(r, "bishop"), 0.987)
near(circ(r, "spencer"), 0.986)
near(circ(r, "janbu"), 0.990)
def test_acads_1b_tension_crack_full_of_water():
# S2VM #2 (ACADS 1b) — 균열 깊이 4 m · 물 가득 — Bishop 1.596 · Spencer 1.592 · Janbu 보정 1.489
m = {
"materials": mats(("s", 20, 32, 10)),
"regions": [{"material_id": "s", "polygon": ACADS_EXT}],
"tension_crack": {"depth": 4.0, "water": 1.0},
}
r = analyze(m, direction=-1, noncircular=False)
near(circ(r, "bishop"), 1.596)
near(circ(r, "spencer"), 1.592)
near(circ(r, "janbu"), 1.489)
def test_acads_1c_three_layers():
# S2VM #3 (ACADS 1c) — Bishop 1.405 · Spencer 1.375 · Janbu 보정 1.357
r = analyze(
{"materials": ACADS1C_MATS, "regions": ACADS1C_REGIONS}, direction=-1, noncircular=False
)
near(circ(r, "bishop"), 1.405)
near(circ(r, "spencer"), 1.375)
near(circ(r, "janbu"), 1.357)
def test_acads_1d_three_layers_seismic():
# S2VM #4 (ACADS 1d) — kh 0.15 — Bishop 1.016 · Spencer 0.991 · Janbu 보정 0.965
m = {"materials": ACADS1C_MATS, "regions": ACADS1C_REGIONS, "seismic_kh": 0.15}
r = analyze(m, direction=-1, noncircular=False)
near(circ(r, "bishop"), 1.016)
near(circ(r, "spencer"), 0.991)
near(circ(r, "janbu"), 0.965)
def test_acads_2b_embankment_dam_specified_circle():
# S2VM #6 (ACADS 2b Talbingo 댐 · 성토) — 지정 원 (100.3, 291.0, R 278.8)
# Bishop 2.208 · Spencer 2.292 · Janbu 보정 2.073 · 심판값 2.29
ext = [
[0, 0], [315.5, 162], [319.5, 162], [321.6, 162], [327.6, 162], [386.9, 130.6], [394.1, 130.6],
[453.4, 97.9], [460.6, 97.9], [515, 65.3], [521.1, 65.3], [577.9, 31.4], [585.1, 31.4], [648, 0],
] # fmt: skip
core = [
[200.7, 0],
[311.9, 130.6],
[319.5, 162],
[321.6, 162],
[328.8, 146.1],
[331.3, 130.6],
[307.1, 0],
]
rest = Polygon(ext).difference(Polygon(core))
regions = [{"material_id": "core", "polygon": core}] + [
{"material_id": "rockfill", "polygon": list(p.exterior.coords)}
for p in getattr(rest, "geoms", [rest])
]
m = {"materials": mats(("rockfill", 20.4, 0, 45), ("core", 18.1, 85, 23)), "regions": regions}
r = evaluate_specified(m, circle=[100.3, 291.0, 278.8], direction=-1)
near(r["bishop"]["F"], 2.208)
near(r["spencer"]["F"], 2.292)
near(r["janbu"]["F"], 2.073)
def _acads3_model():
ext = Polygon([[20, 20], [84, 20], [84, 40], [67.5, 40], [43, 27.75], [20, 27.75]])
regions = [
{
"material_id": "soil",
"polygon": list(ext.intersection(box(0, 27, 100, 100)).exterior.coords),
},
{
"material_id": "seam",
"polygon": list(ext.intersection(box(0, 26.5, 100, 27)).exterior.coords),
},
{
"material_id": "soil",
"polygon": list(ext.intersection(box(0, 0, 100, 26.5)).exterior.coords),
},
]
return {
"materials": mats(("soil", 18.84, 28.5, 20), ("seam", 18.84, 0, 10)),
"regions": regions,
"groundwater": {"mode": "line", "line": [[20, 26.5], [84, 26.5]]},
}
def test_acads_3b_weak_seam_specified_polyline():
# S2VM #8 (ACADS 3b) — 약층 + 지하수위 · 지정 비원호 — Spencer 1.277 · Janbu 보정 1.294
r = evaluate_specified(
_acads3_model(),
polyline=[[41.85, 27.75], [44.0, 26.5], [63.5, 27.0], [73.31, 40.0]],
direction=-1,
methods=("janbu", "spencer"),
)
near(r["spencer"]["F"], 1.277)
near(r["janbu"]["F"], 1.294)
def test_acads_3a_weak_seam_noncircular_search():
# S2VM #7 (ACADS 3a) — 블록 탐색 Janbu 보정 1.275 · Spencer 1.258(심판값 1.24~1.27)
r = analyze(_acads3_model(), direction=-1, methods=("janbu", "spencer"))
near(nonc(r, "janbu"), 1.275)
near(nonc(r, "spencer"), 1.258)
def test_acads_4_inclined_seam_water_surcharge():
# S2VM #9 (ACADS 4) — 기운 약층 · 지하수위선 · 등분포 20 kPa + 선형 20→40 kPa
# 최적화 비원호 Spencer 0.707 · Janbu 보정 0.699 (약층 좌표 = 저해상 그림 판독 — 출처 ±5 % 권함)
ext = Polygon([[20, 15], [84, 15], [84, 40], [67.5, 40], [43, 27.75], [20, 27.75]])
seam = Polygon([[20, 18.26], [84, 36.2], [84, 36.8], [20, 18.88]])
regions = [{"material_id": "seam", "polygon": list(ext.intersection(seam).exterior.coords)}] + [
{"material_id": "soil", "polygon": list(p.exterior.coords)}
for p in ext.difference(seam).geoms
]
linear = [{"x0": 70 + i, "x1": 71 + i, "q": 20 + 20 * (i + 0.5) / 10} for i in range(10)]
m = {
"materials": mats(("soil", 18.84, 28.5, 20), ("seam", 18.84, 0, 10)),
"regions": regions,
"surcharge": [{"x0": 23, "x1": 43, "q": 20}] + linear,
"groundwater": {
"mode": "line",
"line": [
[20, 27.75],
[43, 27.75],
[49, 29.8],
[60, 34],
[66, 35.8],
[74, 37.6],
[80, 38.4],
[84, 38.4],
],
},
}
r = analyze(m, direction=-1, methods=("janbu", "spencer"))
near(nonc(r, "spencer"), 0.707)
near(nonc(r, "janbu"), 0.699)
FK_EXT = [[0, 0], [180, 0], [180, 20], [140, 20], [60, 60], [0, 60]]
def test_fredlund_krahn_specified_circle_dry_and_ru():
# S2VM #21 (F&K 1977, ft · lbf) — 지정 원 (120, 90, R 80)
# 건기 Ordinary 1.928 · Bishop 2.080 · Spencer 2.073 / r_u 0.25 Ordinary 1.607 · Bishop 1.766 · Spencer 1.761
m = {
"materials": mats(("s", 120, 600, 20)),
"regions": [{"material_id": "s", "polygon": FK_EXT}],
}
r = evaluate_specified(
m, circle=[120, 90, 80], direction=1, methods=("ordinary", "bishop", "spencer")
)
near(r["ordinary"]["F"], 1.928)
near(r["bishop"]["F"], 2.080)
near(r["spencer"]["F"], 2.073)
m["groundwater"] = {"mode": "ru", "ru": 0.25}
r = evaluate_specified(
m, circle=[120, 90, 80], direction=1, methods=("ordinary", "bishop", "spencer")
)
near(r["ordinary"]["F"], 1.607)
near(r["bishop"]["F"], 1.766)
near(r["spencer"]["F"], 1.761)
def test_loukidis_seismic_with_ru():
# S2VM #62 — 건기 kh 0.432: Bishop 0.991 · Spencer 1.001 / r_u 0.5 + kh 0.132: Bishop 0.987 · Spencer 1.001
ext = [[-50, -25], [150, -25], [150, 25], [75, 25], [0, 0], [-50, 0]]
m = {
"materials": mats(("c", 20, 25, 30)),
"regions": [{"material_id": "c", "polygon": ext}],
"seismic_kh": 0.432,
}
r = analyze(m, direction=-1, noncircular=False, methods=("bishop", "spencer"))
near(circ(r, "bishop"), 0.991)
near(circ(r, "spencer"), 1.001)
m.update(seismic_kh=0.132, groundwater={"mode": "ru", "ru": 0.5})
r = analyze(m, direction=-1, noncircular=False, methods=("bishop", "spencer"))
near(circ(r, "bishop"), 0.987)
near(circ(r, "spencer"), 1.001)
AT_EXT = [[0, 0], [66, 0], [66, 35], [48, 35], [18, 15], [0, 15]]
def test_arai_tagyo_3_water_table():
# S2VM #16 — 지하수위선 · 원호 Bishop 1.118 · Spencer 1.118 · Janbu 보정 1.131 /
# 비원호 Janbu 보정 1.050 · Spencer 1.094
m = {
"materials": mats(("s", 18.82, 41.65, 15)),
"regions": [{"material_id": "s", "polygon": AT_EXT}],
"groundwater": {"mode": "line", "line": [[0, 15], [18, 15], [30, 23], [48, 29], [66, 32]]},
}
r = analyze(m, direction=-1)
near(circ(r, "bishop"), 1.118)
near(circ(r, "spencer"), 1.118)
near(circ(r, "janbu"), 1.131)
near(nonc(r, "janbu"), 1.050)
near(nonc(r, "spencer"), 1.094)
def test_arai_tagyo_2_weak_middle_layer():
# S2VM #15 — 약한 가운데 층 · Bishop 0.420 · Spencer 0.409 · Janbu 보정 0.423
regions = [
{"material_id": "upper", "polygon": [[24, 19], [48, 35], [72, 35]]},
{
"material_id": "middle",
"polygon": [[0, 3], [96, 35], [72, 35], [24, 19], [18, 15], [0, 15]],
},
{"material_id": "lower", "polygon": [[0, 3], [96, 3], [96, 35]]},
]
m = {
"materials": mats(
("upper", 18.82, 29.4, 12), ("middle", 18.82, 9.8, 5), ("lower", 18.82, 294, 40)
),
"regions": regions,
}
r = analyze(m, direction=-1, noncircular=False)
near(circ(r, "bishop"), 0.420)
near(circ(r, "spencer"), 0.409)
near(circ(r, "janbu"), 0.423)
def test_yamagami_ueta_ordinary_vs_bishop():
# S2VM #17 — Bishop 1.344 · Ordinary 1.278 / 비원호 Spencer 1.325
ext = [[0, 0], [25, 0], [25, 10], [15, 10], [5, 5], [0, 5]]
m = {
"materials": mats(("s", 17.64, 9.8, 10)),
"regions": [{"material_id": "s", "polygon": ext}],
}
r = analyze(m, direction=-1, methods=("ordinary", "bishop", "spencer"))
near(circ(r, "bishop"), 1.344)
near(circ(r, "ordinary"), 1.278)
near(nonc(r, "spencer"), 1.325)
def test_mirror_direction_same_factor():
# 좌우 뒤집은 같은 비탈 = 같은 안전율 · 임계면 좌표도 거울
m = {
"materials": mats(("s", 20, 3, 19.6)),
"regions": [{"material_id": "s", "polygon": ACADS_EXT}],
}
mirrored = {**m, "regions": [{"material_id": "s", "polygon": [[-x, z] for x, z in ACADS_EXT]}]}
a = evaluate_specified(m, circle=[29.104, 55.527, 30.527], direction=-1)
b = evaluate_specified(mirrored, circle=[-29.104, 55.527, 30.527], direction=1)
for k in ("ordinary", "bishop", "janbu", "spencer"):
assert abs(a[k]["F"] - b[k]["F"]) < 1e-9
assert abs(a["bishop"]["entry"][0] + b["bishop"]["entry"][0]) < 1e-9
near(a["bishop"]["F"], 0.987) # S2VM #1 Bishop 임계 원 그대로
def test_slice_table_and_convergence_info():
m = {
"materials": mats(("s", 20, 3, 19.6)),
"regions": [{"material_id": "s", "polygon": ACADS_EXT}],
}
r = evaluate_specified(m, circle=[29.104, 55.527, 30.527], direction=-1, methods=("spencer",))[
"spencer"
]
assert r["converged"] and r["iterations"] > 0 and -1.25 <= r["lambda"] <= 0.25
rows = r["slices"]
assert len(rows) == 50 and all(row["width"] > 0 for row in rows)
# 절편간 힘 E — 머리(0) 에서 시작해 발끝에서 다시 0 근처(힘 평형이 닫힘)
weights = sum(row["weight"] for row in rows)
assert abs(rows[0]["E_left"]) < 1e-9
assert {"alpha_deg", "pore_pressure", "normal_eff", "shear_mobilized", "material"} <= rows[
0
].keys()
assert weights > 0
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-q"]))