- 모델: 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>
156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
# -*- 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"]))
|