# -*- 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 _acads4_model(): # S2VM #9 (ACADS 4) — 기운 약층 · 지하수위선 · 등분포 20 kPa + 선형 20→40 kPa 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], ], }, } return m def test_acads_4_inclined_seam_water_surcharge(): # S2VM #9 — 최적화 비원호 Spencer 0.707 · Janbu 보정 0.699 (약층 좌표 = 저해상 그림 판독 — 출처 ±5 % 권함) r = analyze(_acads4_model(), 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"]))