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>
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
# -*- 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"]))
|
||||
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""532-2 E03 2D 지층 모델 빌더 — 폴리곤 닫힘 · 지층 · 성토체 · 절성토 면적 = B06 · 비탈 목록 · 측점 해석 꼴.
|
||||
|
||||
- 순수 함수(`build_section_model`)는 만든 단면으로 · B06 대조는 검증_sub4 초기값 스냅샷(읽기만 · 없으면 건너뜀).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_532_2_stab_section_model.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import ( # noqa: E402
|
||||
build_section_model,
|
||||
polygon_closed,
|
||||
section_model_from_b06,
|
||||
side_cases,
|
||||
)
|
||||
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
|
||||
|
||||
SUB4 = ROOT / "storage/1/3/c725d747-50ae-46b0-87ce-4d4e56a53cf3/initial_snapshot"
|
||||
|
||||
# 오른쪽(−) 산 · 왼쪽(+) 골 · 편절편성: 노면 −2~2 · 우 절토 1:0.5 · 좌 성토 1:1.5
|
||||
GROUND = [(x / 2, 10.0 - 0.6 * (x / 2)) for x in range(-40, 41)] # 10 − 0.6x (좌로 내려감)
|
||||
DESIGN = [
|
||||
(-20.0, 22.0),
|
||||
(-6.0, 13.6),
|
||||
(-2.6, 10.0),
|
||||
(-2.0, 10.0),
|
||||
(2.0, 10.0),
|
||||
(14.0, 2.0),
|
||||
(20.0, -2.0),
|
||||
]
|
||||
|
||||
|
||||
def _trapezoid(ground, line, sign):
|
||||
"""설계선 꼭짓점 · 지반 표본 합집합 위 사다리꼴(B06 `_trapezoid_areas` 와 같은 정의) — 절토(+) / 성토(−)."""
|
||||
import numpy as np
|
||||
|
||||
gx, gz = zip(*ground)
|
||||
lx, lz = zip(*line)
|
||||
xs = sorted({*gx, *lx})
|
||||
xs = [x for x in xs if lx[0] <= x <= lx[-1]]
|
||||
d = np.interp(xs, gx, gz) - np.interp(xs, lx, lz)
|
||||
area = 0.0
|
||||
for x0, x1, d0, d1 in zip(xs, xs[1:], d, d[1:]):
|
||||
if d0 * d1 < 0:
|
||||
xm = x0 + (x1 - x0) * d0 / (d0 - d1)
|
||||
area += 0.5 * (xm - x0) * max(sign * d0, 0) + 0.5 * (x1 - xm) * max(sign * d1, 0)
|
||||
else:
|
||||
area += 0.5 * (x1 - x0) * (max(sign * d0, 0) + max(sign * d1, 0))
|
||||
return area
|
||||
|
||||
|
||||
def test_build_model_polygons_layers_fill_and_areas():
|
||||
m = build_section_model(
|
||||
GROUND,
|
||||
DESIGN,
|
||||
layers=[("weathered_soil", 0.5), ("weathered_rock", None)],
|
||||
area_range=(-20.0, 20.0),
|
||||
)
|
||||
assert m["regions"] and all(polygon_closed(r) for r in m["regions"])
|
||||
ids = {r["material_id"] for r in m["regions"]}
|
||||
assert ids == {"weathered_soil", "weathered_rock", "fill"}
|
||||
# 지층 사이 겹침 없음 · 합 = 공사 뒤 지표 아래 전체
|
||||
polys = [Polygon(r["polygon"], r.get("holes")) for r in m["regions"]]
|
||||
total = sum(p.area for p in polys)
|
||||
union = polys[0]
|
||||
for p in polys[1:]:
|
||||
union = union.union(p)
|
||||
assert abs(union.area - total) < 1e-6
|
||||
a = m["areas"]
|
||||
assert abs(a["cut_m2"] - _trapezoid(GROUND, DESIGN, 1)) < 1e-6
|
||||
assert abs(a["fill_m2"] - _trapezoid(GROUND, DESIGN, -1)) < 1e-6
|
||||
assert (
|
||||
0 < a["cut_soil_m2"] < a["cut_m2"]
|
||||
and abs(a["cut_soil_m2"] + a["cut_rock_m2"] - a["cut_m2"]) < 1e-9
|
||||
)
|
||||
|
||||
|
||||
def test_side_cases_roles_directions_limits():
|
||||
design = {
|
||||
"section_mode": "right_cut",
|
||||
"road_edges": {"left": {"offset_m": 2.0}, "right": {"offset_m": -2.0}},
|
||||
}
|
||||
left, right = side_cases(design, GROUND, DESIGN)
|
||||
assert (left["role"], left["direction"]) == ("fill", 1) # 좌 성토 → 바깥(+x)으로
|
||||
assert (right["role"], right["direction"]) == ("cut", 1) # 우 절토 → 노면(+x)으로
|
||||
assert left["limits"][0] == -2.0 and right["limits"][1] == 2.0
|
||||
assert left["height_m"] > 0 and right["height_m"] > 0
|
||||
|
||||
|
||||
def test_analyze_station_shape():
|
||||
cross = {
|
||||
"chainage_m": 0.0,
|
||||
"label": "STA.0+000",
|
||||
"samples": [{"offset_m": x, "elevation_m": z} for x, z in GROUND],
|
||||
}
|
||||
design = {
|
||||
"section_mode": "right_cut",
|
||||
"ground_type": "ripping_rock",
|
||||
"rock_boundary_offset_m": -0.5,
|
||||
"design_line": [{"offset_m": x, "elevation_m": z} for x, z in DESIGN],
|
||||
"road_edges": {
|
||||
"left": {"offset_m": 2.0, "elevation_m": 10.0},
|
||||
"right": {"offset_m": -2.0, "elevation_m": 10.0},
|
||||
},
|
||||
}
|
||||
res = analyze_station(
|
||||
cross, design, conditions=("dry",), methods=("bishop",), noncircular=False
|
||||
)
|
||||
assert len(res["cases"]) == 2
|
||||
for case in res["cases"]:
|
||||
F = case["min_F"]["bishop"]
|
||||
assert F is not None and 0.3 < F < 20
|
||||
best = case["result"]["methods"]["bishop"]["circular"]
|
||||
assert best["slices"] and best["center"] and best["radius"] > 0
|
||||
lo, hi = case["limits"]
|
||||
assert (
|
||||
lo - 1e-6 <= min(best["entry"][0], best["exit"][0])
|
||||
and max(best["entry"][0], best["exit"][0]) <= hi + 1e-6
|
||||
)
|
||||
assert res["cases"][0]["result"]["contour"]["F"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not (SUB4 / "db.json").exists(), reason="검증_sub4 스냅샷 없음")
|
||||
def test_b06_areas_match_all_stations():
|
||||
"""검증_sub4 전 측점 — 모델 절토 · 성토 · 토사 · 암 면적 = B06 저장값(반올림 4 자리 안)."""
|
||||
rows = json.loads((SUB4 / "db.json").read_text(encoding="utf-8"))["cross_sections"]
|
||||
assert rows
|
||||
for row in rows:
|
||||
design = json.loads(row["data"])["design"]
|
||||
name = Path(row["cross_section_file_path"]).name
|
||||
cross = json.loads(
|
||||
(SUB4 / "B06_Section__cross_sections" / name).read_text(encoding="utf-8")
|
||||
)
|
||||
m = section_model_from_b06(cross, design)
|
||||
assert all(polygon_closed(r) for r in m["regions"]), name
|
||||
a = m["areas"]
|
||||
assert abs(a["cut_m2"] - design["cut_area_m2"]) < 2e-3, name
|
||||
assert abs(a["fill_m2"] - design["fill_area_m2"]) < 2e-3, name
|
||||
assert abs(a["cut_soil_m2"] - design["cut_soil_area_m2"]) < 2e-3, name
|
||||
assert abs(a["cut_rock_m2"] - design["cut_rock_area_m2"]) < 2e-3, name
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-q"]))
|
||||
Reference in New Issue
Block a user