feat(533-1 · 533-2): E04 3D 기둥 솔버 · 구 · 타원체 탐색 · 합성 DEM · 지층 격자

- Column: Hovland · Hungr 3D Bishop · Hungr 3D Janbu 간편 + 보정(f0) · 활동 방향 자동(지표 낙하) · r_u · 지하수위면 · kh · 노면 하중 · 못 뚫는 층에서 잘림 · 부분 기둥
- Search: Scoops3D 방식 구 격자(노드 = 지표 + h · 반지름 증분 · 부피/면적 제약 · 거친→고운) + 타원체(축 방향 ratio 배) · 덩이 나눔 · 셀별 최소 F · 임계 체적 래스터
- 잘린 면 ≥ 10 % → Janbu 보정 전환 · Bishop 이 낮으면 Bishop
- Grid: 측점 사이 선직면 보간 δ(설계 − 원지형) · 띠 밖 원지형 · 흙 두께(암 경계 · 토사 3 m) · 리핑암 뚫림 · 발파암 못 뚫음 · 노면 하중 · 1 m 정사각 재표본
- 공용 꼴 = E03_StabSection_Types import(칸 이름 그대로) · 새 의존 없음(NumPy · SciPy)
- 시험: Gens 구 1.402 · Zhang 1988 경우 1 · 2(Hungr 2.167 · 1.620) · Scoops3D 제방 1.32 · D&G 1a 1.04 · 합성 DEM 단면 · 띠 밖 · 지층

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 20:13:33 +09:00
co-authored by Claude Opus 5.5
parent 4730edba31
commit b97ddf4783
6 changed files with 1340 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
"""533-1 E04 합성 DEM · 지층 격자 — 측점 위 단면 = B06 설계선 · 띠 밖 = 원지형 · 보간 · 지층."""
import numpy as np
import pytest
from E04_Stab3D.E04_Stab3D_Engine_Grid import _segment_map, build_grid, strata_from_grid
def _dem(dx=0.5):
x = np.arange(-30, 90 + 1e-9, dx)
y = np.arange(-40, 40 + 1e-9, dx)
xx, yy = np.meshgrid(x, y)
z = 100.0 + 0.3 * yy + 0.02 * xx # 왼쪽(+y)이 오르막
return {"x": x, "y": y, "z": z, "valid_mask": np.ones(z.shape, bool)}, xx, yy
def _station(dem, cx, depth_cut, ground_type="soil", rock=None):
"""x=cx · 왼쪽 = +y · 노면 ±2 m 를 depth_cut 낮춘 설계선(사면 1:1 로 지반에 닿음)."""
offs = np.arange(-20, 20.01, 0.5)
g = 100.0 + 0.3 * offs + 0.02 * cx
design = g.copy()
road = np.abs(offs) <= 2.0
design[road] = 100.0 + 0.02 * cx - depth_cut
for k in np.flatnonzero(~road):
edge = 2.0 * np.sign(offs[k])
design[k] = (
min(g[k], 100.0 + 0.02 * cx - depth_cut + abs(offs[k] - edge))
if depth_cut > 0
else g[k]
)
return {
"chainage_m": float(cx),
"center": [float(cx), 0.0],
"left": [0.0, 1.0],
"ground": np.column_stack([offs, g]).tolist(),
"design": np.column_stack([offs, design]).tolist(),
"ground_type": ground_type,
"rock_depth_m": rock,
"road": [-2.0, 2.0],
}
def _grid():
dem, xx, yy = _dem()
sts = [
_station(dem, 0, 1.0),
_station(dem, 20, 3.0, "blasting_rock", 0.5),
_station(dem, 40, 3.0, "ripping_rock", 1.0),
]
return dem, build_grid(dem, sts, cell_m=0.5), xx, yy, sts
def test_station_section_equals_design_and_outside_unchanged():
dem, g, xx, yy, sts = _grid()
for st in sts:
col = np.isclose(g["x"], st["center"][0])
d = np.asarray(st["design"])
z_line = np.interp(g["y"], d[:, 0], d[:, 1])
inside = np.abs(g["y"]) <= 20
assert np.abs(g["z"][inside, col][:, None].ravel() - z_line[inside]).max() < 1e-9
out = ~g["in_band"]
assert np.array_equal(g["z"][out], g["z_orig"][out])
assert np.array_equal(g["z"][:, g["x"] < 0], dem["z"][:, g["x"] < 0]) # 첫 측점 앞 = 원지형
def test_between_stations_linear_delta():
_, g, xx, yy, sts = _grid()
j = np.flatnonzero(np.isclose(g["x"], 10.0))[0]
i = np.flatnonzero(np.isclose(g["y"], 0.0))[0]
# 노면 가운데 δ: 측점 0 −1 m · 측점 20 −3 m → 중간 −2 m
assert g["dz"][i, j] == pytest.approx(-2.0, abs=1e-9)
assert g["q"][i, j] == pytest.approx(10.0)
def test_curved_segment_map_round_trip():
c0, c1 = np.array([0.0, 0.0]), np.array([19.0, 6.0])
l0 = np.array([0.0, 1.0])
a = np.radians(35.0)
l1 = np.array([-np.sin(a), np.cos(a)])
t_true = np.array([0.0, 0.3, 0.75, 1.0])
o_true = np.array([-15.0, 4.0, 12.0, -7.0])
lt = l0[None] + t_true[:, None] * (l1 - l0)[None]
lt /= np.hypot(lt[:, 0], lt[:, 1])[:, None]
p = c0[None] + t_true[:, None] * (c1 - c0)[None] + o_true[:, None] * lt
t, o = _segment_map(p[:, 0], p[:, 1], c0, c1, l0, l1)
assert np.allclose(t, t_true, atol=1e-9) and np.allclose(o, o_true, atol=1e-9)
def test_rock_boundary_and_layers():
_, g, xx, yy, sts = _grid()
j0 = np.flatnonzero(np.isclose(g["x"], 20.0))[0]
jm = np.flatnonzero(np.isclose(g["x"], 30.0))[0]
i = np.flatnonzero(np.isclose(g["y"], 10.0))[0]
assert g["soil_thickness"][i, j0] == pytest.approx(0.5)
assert g["soil_thickness"][i, jm] == pytest.approx(0.75) # 0.5 → 1.0 가운데
far = np.flatnonzero(np.isclose(g["y"], 35.0))[0]
assert not g["in_band"][far, j0]
assert g["soil_thickness"][far, j0] == pytest.approx(0.5) # 띠 밖 = 가까운 측점
assert g["soil_thickness"][i, 0] == pytest.approx(3.0) # 토사 측점 = 기본 두께
st = strata_from_grid(g)
assert g["rock_kind"][i, j0] == 1 # 발파암 → 못 뚫는 층 윗면 = 암 윗면(깎기로 드러나면 지표)
assert st.hard_top[i, j0] == pytest.approx(min(g["rock_top"][i, j0], g["z"][i, j0]))
assert np.isneginf(st.hard_top[i, jm + 20]) or st.hard_top[i, jm + 20] < -1e5 # 리핑암 = 뚫림
ii = np.flatnonzero(np.isclose(g["y"], 0.0))[0]
assert st.bottoms[0, ii, j0] == pytest.approx(g["z"][ii, j0]) # 깎기 = 성토 두께 0
@@ -0,0 +1,105 @@
"""533-2 E04 3D 기둥 솔버 — 문헌 기준 문제 ±3 %.
- Gens 외(1988) · Hungr 외(1989) · Scoops3D 6.1: 2:1 평면 사면 · 구 R=1 · 중심-지표 0.5
· c=0.1 γ=1 φ=0 → 해석해 1.402 · 부피 0.654.
- Zhang(1988) 경우 1 · 2(PLoS ONE 18(7) e0287998 그림 5 · 표 2): 1:2 사면 H 12.2 m
· 타원체 중심 (6.1, 0, 21.3)
Rx=Rz=24.4 · Ry=78.2 · c 29 kPa φ 20° γ 18.8 → Hungr 3D Bishop 2.167 · Zhang 2.122 /
약층(c 0 · φ 10°, 발끝 아래 0.94 m — 그림 화소로 잼) → Hungr 1.620 · Zhang 1.533.
"""
import numpy as np
import pytest
from E04_Stab3D.E04_Stab3D_Engine_Column import Strata
from E04_Stab3D.E04_Stab3D_Engine_Search import evaluate_surface, governing
def _mat(gamma, c, phi, mid="m"):
return {"id": mid, "name": mid, "gamma_t": gamma, "gamma_sat": gamma, "c": c, "phi": phi}
def _gens(dx=0.02, angle=0.0):
"""2:1 평면 사면 — 아래로 향하는 방향을 angle(rad) 만큼 돌림."""
beta = np.arctan(0.5)
x = np.arange(-2, 2 + 1e-9, dx)
xx, yy = np.meshgrid(x, x)
down = np.array([np.cos(angle), np.sin(angle)])
z = -(xx * down[0] + yy * down[1]) * np.tan(beta)
st = Strata(x, x, z, np.zeros((0,) + z.shape), [_mat(1.0, 0.1, 0.0)])
c = np.r_[0.5 * np.sin(beta) * down, 0.5 * np.cos(beta)]
return st, c
def test_gens_sphere_analytic():
st, c = _gens()
r = evaluate_surface(st, c, 1.0)
assert r["bishop"] == pytest.approx(1.402, rel=0.01)
assert r["hovland"] == pytest.approx(1.402, rel=0.01)
assert r["volume"] == pytest.approx(np.pi * 0.25 * 2.5 / 3, rel=0.01) # 구 모자 부피 0.6545
def test_rotated_slope_same_factor():
"""Scoops3D 6.7 대칭 — 낙하 방향이 격자 축과 어긋나도 같은 F."""
f0 = evaluate_surface(*_gens(0.02), 1.0)["bishop"]
f1 = evaluate_surface(*_gens(0.02, np.radians(33.0)), 1.0)["bishop"]
assert f1 == pytest.approx(f0, rel=0.01)
def _zhang(weak: bool, dx=0.5):
x = np.arange(-15, 45 + 1e-9, dx)
y = np.arange(-90, 90 + 1e-9, dx)
xx, _ = np.meshgrid(x, y)
z = np.clip(xx * 0.5, 0, 12.2)
soil = _mat(18.8, 29.0, 20.0, "soil")
if not weak:
return Strata(x, y, z, np.zeros((0,) + z.shape), [soil])
zw = -0.94
bottoms = np.stack([np.full(z.shape, zw + 0.3), np.full(z.shape, zw)])
mats = [soil, _mat(18.8, 0.0, 10.0, "weak"), _mat(18.8, 1000.0, 45.0, "hard")]
return Strata(x, y, z, bottoms, mats, hard=[False, False, True])
def test_zhang_case1_ellipsoid():
r = evaluate_surface(_zhang(False), [6.1, 0.0, 21.3], 24.4, 78.2 / 24.4, [-1.0, 0.0])
assert r["bishop"] == pytest.approx(2.167, rel=0.03) # Hungr 외 1989
assert r["bishop"] == pytest.approx(2.122, rel=0.03) # Zhang 1988
assert r["hovland"] < r["bishop"] # Hovland 은 보수적
def test_zhang_case2_weak_layer_truncated():
r = evaluate_surface(_zhang(True), [6.1, 0.0, 21.3], 24.4, 78.2 / 24.4, [-1.0, 0.0])
assert r["bishop"] == pytest.approx(1.620, rel=0.03) # Hungr 외 1989
assert r["truncated_ratio"] > 0.1
# 잘린 면 → Janbu 보정으로 전환하되 Bishop 이 낮으면 Bishop(작은 쪽)
assert r["governing"] == pytest.approx(min(r["bishop"], r["janbu_corrected"]))
assert 1.45 < r["janbu_corrected"] < 1.80 # 문헌 폭 1.53~1.67 둘레
def test_pore_pressure_and_seismic():
st, c = _gens(0.04)
dry = evaluate_surface(st, c, 1.0)
# φ=0 이면 간극수압은 저항에 안 들어감
assert evaluate_surface(st, c, 1.0, groundwater={"mode": "ru", "ru": 0.3})[
"bishop"
] == pytest.approx(dry["bishop"])
# c−φ 흙: r_u 가 F 를 낮춤 · 깊은 지하수위는 영향 없음 · kh 는 낮춤
st2 = Strata(st.x, st.y, st.z_top, st.bottoms, [_mat(1.0, 0.05, 25.0)], z_water=st.z_top - 5.0)
base = evaluate_surface(st2, c, 1.0)["bishop"]
assert evaluate_surface(st2, c, 1.0, groundwater={"mode": "ru", "ru": 0.3})["bishop"] < base
assert evaluate_surface(st2, c, 1.0, groundwater={"mode": "line"})["bishop"] == pytest.approx(
base
)
st2.z_water = st2.z_top + 0.0
assert evaluate_surface(st2, c, 1.0, groundwater={"mode": "line"})["bishop"] < base
assert evaluate_surface(st2, c, 1.0, seismic_kh=0.1)["bishop"] < base
def test_governing_picks_bishop_when_not_truncated():
res = {
"bishop": np.array([1.5, 1.5]),
"janbu_corrected": np.array([1.4, 1.4]),
"truncated_ratio": np.array([0.0, 0.5]),
}
f, meth = governing(res)
assert f.tolist() == [1.5, 1.4] and meth.tolist() == [0, 1]
@@ -0,0 +1,60 @@
"""533-2 E04 활동면 탐색 — Scoops3D 검증 표 전역 최소 ±3 %.
Reid 외(2015) Scoops3D 표 6.8 · 6.11(CLARA-W 와 같은 구): H 10 m 제방
- 30° · λ 0.125(γ 19 · φ 20° · c 8.64 kPa) → Bishop 1.32 · 부피 1,124 m³
- Donald & Giam 1a 2:1 · γ 20 · φ 19.6° · c 3 kPa → 1.04
탐색 격자는 시험 시간 때문에 문헌 임계 중심 둘레로 좁힘.
"""
import numpy as np
import pytest
from E04_Stab3D.E04_Stab3D_Engine_Column import Strata
from E04_Stab3D.E04_Stab3D_Engine_Search import SearchParams, search
def _embankment(run, gamma, phi, c, dx=1.0):
x = np.arange(-25, run + 35 + 1e-9, dx)
y = np.arange(-40, 40 + 1e-9, dx)
xx, _ = np.meshgrid(x, y)
z = np.clip(xx / run * 10.0, 0, 10.0)
mat = {"id": "m", "name": "m", "gamma_t": gamma, "gamma_sat": gamma, "c": c, "phi": phi}
return Strata(x, y, z, np.zeros((0,) + z.shape), [mat])
def test_embankment_30deg_global_minimum():
st = _embankment(10 / np.tan(np.radians(30)), 19, 20, 8.64)
p = SearchParams(
box=(-2, 10, -1, 1),
h_min=4,
h_max=24,
spacing=1.0,
coarse=2,
max_depth=20,
v_min=50,
v_max=2e4,
ratios=(1.0,),
)
r = search(st, p)
assert r.best["governing"] == pytest.approx(1.32, rel=0.03)
assert r.best["volume"] == pytest.approx(1124, rel=0.15)
f = r.f_min
assert np.nanmin(f) == pytest.approx(r.best["governing"])
# 셀별 최소 지도: 임계 활동체 안 셀은 전역 최소 · 부피 래스터가 같이 채워짐
assert np.isfinite(r.volume[np.isfinite(f)]).all()
def test_donald_giam_1a():
st = _embankment(20, 20, 19.6, 3.0)
p = SearchParams(
box=(-4, 6, -1, 1),
h_min=16,
h_max=30,
spacing=1.0,
coarse=2,
max_depth=20,
v_min=50,
v_max=3e4,
ratios=(1.0,),
)
assert search(st, p).best["governing"] == pytest.approx(1.04, rel=0.03)