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:
2026-10-25 20:07:14 +09:00
co-authored by Claude Opus 5.5
parent eb29e753fd
commit bfd87f3e3e
6 changed files with 2013 additions and 0 deletions
@@ -0,0 +1,634 @@
"""E03 임계 활동면 탐색 — 원호(Grid & Radius · Entry & Exit · 다듬기) · 비원호(블록 + 꼭짓점 최적화).
활동 방향 +x 로 맞춘 `Prepared` 위에서 찾고 좌표를 원래 방향으로 되돌려 냄.
다듬기 · 비원호 = 차분진화(best/1/bin · 무리 전체를 한 번에 평가 · seed 고정 → 같은 입력 같은 답)
+ 비원호 마지막 국부 다듬기 = SciPy Nelder-Mead. SciPy differential_evolution 은 개체마다 파이썬 돌연변이 고리라
한 비탈 시간의 30 % 를 먹어 직접 묶음 꼴로 씀.
상용 SW 대응: Grid & Radius(SLOPE/W · Slide) · Entry & Exit(SLOPE/W) · Auto Refine(Slide) ·
Block Search + Optimize(Slide · SLOPE/W 꼭짓점 최적화).
"""
import time
import numpy as np
from scipy.optimize import minimize
from E03_StabSection import E03_StabSection_Engine_LEM as lem
METHODS = ("ordinary", "bishop", "janbu", "spencer")
_BIG = 1e3
_SURFACE_TOL = 1e-3 # 활동면이 지표면 위로 나온 높이 허용(m)
# ---- 면 → 절편 ---------------------------------------------------------------
def _crack_start(prep, xE, xX, base_fn):
"""인장균열 — 머리 쪽에서 활동면 깊이가 균열 깊이에 닿는 x (없으면 원래 진입점)."""
if prep.crack_depth <= 0:
return xE, np.zeros(len(xE))
t = np.linspace(0.0, 1.0, 201)
xs = xE[:, None] + (xX - xE)[:, None] * t
depth = prep.surface(xs) - base_fn(xs)
reach = depth >= prep.crack_depth
k = np.where(reach.any(1), np.argmax(reach, 1), 0)
k0 = np.maximum(k - 1, 0)
rows = np.arange(len(xE))
d0, d1 = depth[rows, k0], depth[rows, k]
w = np.where(d1 > d0, (prep.crack_depth - d0) / np.maximum(d1 - d0, 1e-12), 0.0)
x_new = np.where(k > 0, xs[rows, k0] + np.clip(w, 0, 1) * (xs[rows, k] - xs[rows, k0]), xE)
return x_new, np.where(k > 0, prep.crack_depth, 0.0)
def _finish_slices(prep, xe, ze, crack, min_depth):
sl = lem.slices(prep, xe, ze)
if prep.crack_depth > 0:
lem.add_crack_water(sl, crack, prep.crack_water)
height = sl["zt"] - sl["zb"]
sl["valid"] &= np.all(height > -_SURFACE_TOL, axis=1)
sl["valid"] &= np.nanmax(height, axis=1) >= max(min_depth, 1e-3)
return sl
def circle_slices(prep, xc, zc, R, xE, xX, n, min_depth=0.0):
xc, zc, R = (np.asarray(v, float) for v in (xc, zc, R))
def base(x):
return zc[:, None] - np.sqrt(np.clip(R[:, None] ** 2 - (x - xc[:, None]) ** 2, 0, None))
xE, crack = _crack_start(prep, np.asarray(xE, float), np.asarray(xX, float), base)
t = np.linspace(0.0, 1.0, n + 1)
xe = xE[:, None] + (xX - xE)[:, None] * t
sl = _finish_slices(prep, xe, base(xe), crack, min_depth)
sl["valid"] &= (xE >= xc - R) & (xX <= xc + R)
return sl
def poly_base(vx, vz, x):
"""꼭짓점(S, V) 꺾은선을 x(S, m) 에서 — 행마다 선형 보간(양 끝 밖은 끝 마디 연장)."""
z = None
for k in range(vx.shape[1] - 1):
x0, x1 = vx[:, k : k + 1], vx[:, k + 1 : k + 2]
t = (x - x0) / np.where(x1 - x0 > 1e-12, x1 - x0, 1e-12)
seg = vz[:, k : k + 1] + t * (vz[:, k + 1 : k + 2] - vz[:, k : k + 1])
z = seg if z is None else np.where(x >= x0, seg, z)
return z
def poly_slices(prep, vx, vz, n, min_depth=0.0):
vx, vz = np.asarray(vx, float), np.asarray(vz, float)
def base(x):
return poly_base(vx, vz, x)
xE, crack = _crack_start(prep, vx[:, 0], vx[:, -1], base)
# 절편 경계 = 고른 간격 + 안쪽 꼭짓점(꺾임이 절편 안에 묻히지 않게 — SLOPE/W 와 같은 생각)
t = np.linspace(0.0, 1.0, n + 1 - (vx.shape[1] - 2))
xe = xE[:, None] + (vx[:, -1] - xE)[:, None] * t
inner = np.clip(vx[:, 1:-1], xE[:, None], vx[:, -1:])
xe = np.sort(np.concatenate([xe, inner], axis=1), axis=1)
sl = _finish_slices(prep, xe, base(xe), crack, min_depth)
sl["valid"] &= np.all(np.diff(vx, axis=1) > 1e-6, axis=1)
return sl
def three_point_center(vx, vz):
"""비원호 모멘트 중심 = 진입 · 가장 깊은 꼭짓점 · 진출을 지나는 원의 중심(곧은 면은 위로 멀리)."""
p1 = np.column_stack([vx[:, 0], vz[:, 0]])
p3 = np.column_stack([vx[:, -1], vz[:, -1]])
chord_z = vz[:, :1] + (vx - vx[:, :1]) * (
(vz[:, -1:] - vz[:, :1]) / np.maximum(vx[:, -1:] - vx[:, :1], 1e-9)
)
k = np.argmax(chord_z - vz, axis=1)
p2 = np.column_stack([vx[np.arange(len(vx)), k], vz[np.arange(len(vx)), k]])
ax, ay = p1.T
bx, by = p2.T
cx, cy = p3.T
d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
ux = (
(ax**2 + ay**2) * (by - cy) + (bx**2 + by**2) * (cy - ay) + (cx**2 + cy**2) * (ay - by)
) / np.where(np.abs(d) < 1e-9, 1, d)
uy = (
(ax**2 + ay**2) * (cx - bx) + (bx**2 + by**2) * (ax - cx) + (cx**2 + cy**2) * (bx - ax)
) / np.where(np.abs(d) < 1e-9, 1, d)
far = np.abs(d) < 1e-9
span = np.hypot(cx - ax, cy - ay)
ux = np.where(far, 0.5 * (ax + cx), ux)
uy = np.where(far, np.maximum(ay, cy) + 10 * span, uy)
return np.column_stack([ux, uy])
# ---- 방법 묶음 평가 ------------------------------------------------------------
def evaluate(sl, center, methods=METHODS, scan=True) -> dict:
out = {}
base = lem.ordinary(sl, center)
seed = np.where(np.isfinite(base["F"]), base["F"], 1.5)
if "ordinary" in methods:
out["ordinary"] = base
if "bishop" in methods or "spencer" in methods:
out["bishop"] = lem.bishop(sl, center, seed)
seed = np.where(np.isfinite(out["bishop"]["F"]), out["bishop"]["F"], seed)
if "janbu" in methods:
out["janbu"] = lem.janbu(sl, seed)
if "spencer" in methods:
out["spencer"] = lem.spencer(sl, center, seed, scan)
return {m: out[m] for m in methods}
def _objective(method):
def fun(sl, center):
return evaluate(sl, center, (method,), scan=False)[method]["F"]
return fun
# ---- 원호 ---------------------------------------------------------------------
def circle_from_chord(prep, xE, xX, theta):
"""진입 · 진출 x 와 중심각 θ → 중심 · 반지름(중심은 현 위쪽 · 두 끝이 중심보다 아래여야)."""
zE, zX = prep.surface(xE), prep.surface(xX)
dx, dz = xX - xE, zX - zE
c = np.hypot(dx, dz)
R = c / (2 * np.sin(theta / 2))
d = R * np.cos(theta / 2)
nx, nz = -dz / np.maximum(c, 1e-12), dx / np.maximum(c, 1e-12)
flip = nz < 0
nx, nz = np.where(flip, -nx, nx), np.where(flip, -nz, nz)
xc = 0.5 * (xE + xX) + d * nx
zc = 0.5 * (zE + zX) + d * nz
ok = (zE <= zc) & (zX <= zc) & (dx > 0) & np.isfinite(zE) & np.isfinite(zX)
return xc, zc, R, ok
def circle_cross_surface(prep, xc, zc, R):
"""원 아래 반쪽과 지표면의 교점 중 지표면 아래로 들어간 가장 넓은 구간 → (진입, 진출)."""
sx, sz = prep.surface_x, prep.surface_z
ax, az, bx, bz = sx[:-1], sz[:-1], sx[1:], sz[1:]
dx, dz = bx - ax, bz - az
fx, fz = ax[None, :] - xc[:, None], az[None, :] - zc[:, None]
a = dx**2 + dz**2
b = 2 * (fx * dx + fz * dz)
c = fx**2 + fz**2 - R[:, None] ** 2
disc = b**2 - 4 * a * c
root = np.sqrt(np.clip(disc, 0, None))
roots = []
for sign in (-1, 1):
t = (-b + sign * root) / (2 * np.maximum(a, 1e-12))
x = ax + t * dx
z = az + t * dz
keep = (disc >= 0) & (t >= 0) & (t <= 1) & (z <= zc[:, None] + 1e-9)
roots.append(np.where(keep, x, np.nan))
xs = np.sort(np.concatenate(roots, axis=1), axis=1)
mids = 0.5 * (xs[:, :-1] + xs[:, 1:])
arc = zc[:, None] - np.sqrt(np.clip(R[:, None] ** 2 - (mids - xc[:, None]) ** 2, 0, None))
below = np.isfinite(mids) & (prep.surface(np.nan_to_num(mids)) > arc)
width = np.where(below, xs[:, 1:] - xs[:, :-1], -1.0)
k = np.argmax(width, axis=1)
rows = np.arange(len(xc))
ok = width[rows, k] > 0
return xs[rows, k], xs[rows, k + 1], ok
def entry_exit_circles(prep, limits, n_points=25, thetas_deg=(10, 20, 35, 50, 65, 80, 100, 120)):
xl, xr = limits
pts = np.linspace(xl, xr, n_points)
i, j = np.triu_indices(n_points, k=1)
xE, xX = pts[i], pts[j]
xE = np.repeat(xE, len(thetas_deg))
xX = np.repeat(xX, len(thetas_deg))
th = np.tile(np.radians(thetas_deg), len(i))
xc, zc, R, ok = circle_from_chord(prep, xE, xX, th)
return xc[ok], zc[ok], R[ok], xE[ok], xX[ok]
def grid_centers(prep, limits, nx=15, nz=15):
xl, xr = limits
z_in = prep.surface(np.linspace(xl, xr, 50))
H = max(np.nanmax(z_in) - np.nanmin(z_in), 1.0)
L = max(xr - xl, 1.0)
gx = np.linspace(xl, xr + 0.25 * L, nx)
gz = np.linspace(np.nanmax(z_in) + 0.1 * H, np.nanmax(z_in) + max(1.5 * H, 0.6 * L), nz)
return gx, gz
def grid_radius_circles(prep, limits, gx, gz, n_radii=10):
cx, cz = np.meshgrid(gx, gz)
cx, cz = cx.ravel(), cz.ravel()
sx = np.linspace(limits[0], limits[1], 80)
sz = prep.surface(sx)
dist = np.sqrt((sx[None, :] - cx[:, None]) ** 2 + (sz[None, :] - cz[:, None]) ** 2)
r0 = np.nanmin(dist, axis=1) + 0.05
r1 = np.maximum(cz - prep.bottom_z, r0 + 0.1)
frac = np.linspace(0.0, 1.0, n_radii + 1)[1:]
R = (r0[:, None] + (r1 - r0)[:, None] * frac).ravel()
owner = np.repeat(np.arange(len(cx)), n_radii)
xc, zc = cx[owner], cz[owner]
xE, xX, ok = circle_cross_surface(prep, xc, zc, R)
ok &= (xE >= limits[0] - 1e-6) & (xX <= limits[1] + 1e-6)
return xc[ok], zc[ok], R[ok], xE[ok], xX[ok], owner[ok]
def _circle_eval(prep, xc, zc, R, xE, xX, n, min_depth, methods, chunk=4000):
res = {m: np.full(len(xc), np.nan) for m in methods}
for s in range(0, len(xc), chunk):
e = slice(s, s + chunk)
sl = circle_slices(prep, xc[e], zc[e], R[e], xE[e], xX[e], n, min_depth)
center = np.column_stack([xc[e], zc[e]])
out = evaluate(sl, center, methods)
for m in methods:
res[m][e] = out[m]["F"]
return res
def refine_circle(prep, limits, method, seeds, n, min_depth, seed=0, maxiter=60):
"""원호 다듬기 — (진입 x, 진출 x, 중심각) 차분진화 · 첫 무리 = 거친 탐색의 좋은 면."""
fun = _objective(method)
lo = np.array([limits[0], limits[0], np.radians(3)])
hi = np.array([limits[1], limits[1], np.radians(170)])
def obj(p):
xc, zc, R, ok = circle_from_chord(prep, p[:, 0], p[:, 1], p[:, 2])
F = np.full(len(p), _BIG)
if ok.any():
sl = circle_slices(prep, xc[ok], zc[ok], R[ok], p[ok, 0], p[ok, 1], n, min_depth)
f = fun(sl, np.column_stack([xc[ok], zc[ok]]))
F[ok] = np.where(np.isfinite(f), f, _BIG)
return F
x, fmin, _pop, _f = _de(obj, lo, hi, _population(seeds, lo, hi, 30, seed), maxiter, seed)
xc, zc, R, _ = circle_from_chord(prep, x[0:1], x[1:2], x[2:3])
return float(fmin), (float(xc[0]), float(zc[0]), float(R[0]), float(x[0]), float(x[1]))
def _de(obj, lo, hi, init, maxiter, seed, cr=0.7, tol=1e-6, patience=25):
"""묶음 차분진화(best/1/bin · 축척 0.5~1.0 흔들기 = SciPy 기본 전략) — 최소 x · 값 · 마지막 무리 · 값."""
rng = np.random.default_rng(seed)
pop = np.clip(np.asarray(init, float), lo, hi)
f = obj(pop)
P, D = pop.shape
best_hist = f.min()
still = 0
for _ in range(maxiter):
b = pop[np.argmin(f)]
r1 = rng.integers(0, P, P)
r2 = (r1 + rng.integers(1, P, P)) % P
trial = b + rng.uniform(0.5, 1.0) * (pop[r1] - pop[r2])
cross = rng.random((P, D)) < cr
cross[np.arange(P), rng.integers(0, D, P)] = True
trial = np.clip(np.where(cross, trial, pop), lo, hi)
ft = obj(trial)
better = ft <= f
pop[better], f[better] = trial[better], ft[better]
still = still + 1 if best_hist - f.min() < tol else 0
best_hist = min(best_hist, f.min())
if still >= patience:
break
k = int(np.argmin(f))
return pop[k].copy(), float(f[k]), pop, f
def _local_search(obj, starts, lo, hi, step, maxiter, seed, lam=24, starts_keep=8):
"""여러 출발점 (1+λ) 국부 탐색 — 출발점마다 λ 개를 흔들어 나아지면 옮기고 걸음 ×1.5 · 아니면 ×0.6.
상용 SW 의 꼭짓점 이동 최적화(SLOPE/W Optimization · Slide Surface Altering)와 같은 생각.
모든 출발점 · 후보를 한 번에 평가. 돌려주는 값 = 최소 x · 값 · 출발점별 마지막 x · 값.
"""
rng = np.random.default_rng(seed)
x = np.clip(starts[:starts_keep], lo, hi)
f = obj(x)
sig = np.tile(step, (len(x), 1))
for _ in range(maxiter):
cand = x[:, None, :] + rng.normal(size=(len(x), lam, x.shape[1])) * sig[:, None, :]
cand = np.clip(cand, lo, hi).reshape(-1, x.shape[1])
fc = obj(cand).reshape(len(x), lam)
k = np.argmin(fc, axis=1)
best = fc[np.arange(len(x)), k]
up = best < f - 1e-9
x[up] = cand.reshape(len(x), lam, -1)[np.arange(len(x)), k][up]
f[up] = best[up]
sig *= np.where(up, 1.5, 0.6)[:, None]
if np.all(sig < step * 1e-3):
break
i = int(np.argmin(f))
return x[i].copy(), float(f[i]), x, f
def _population(seeds, lo, hi, size, seed, spread=0.0):
"""첫 무리 = 씨앗 + (spread > 0 이면 씨앗 둘레 흔든 것 80 % · 고른 무작위 20 %)."""
rng = np.random.default_rng(seed)
seeds = np.clip(np.asarray(seeds, float).reshape(-1, len(lo)), lo, hi)
extra = max(size - len(seeds), 5)
rand = lo + rng.random((extra, len(lo))) * (hi - lo)
if spread > 0 and len(seeds):
k = int(0.8 * extra)
base = seeds[rng.integers(0, len(seeds), k)]
rand[:k] = np.clip(base + rng.normal(0, spread, (k, len(lo))) * (hi - lo), lo, hi)
return np.vstack([seeds, rand])
# ---- 비원호(블록 + 꼭짓점 최적화) -------------------------------------------------
def _decode(prep, p, m):
"""변수 [진입 x, 진출 x, 위치 s_1..s_m, 깊이 d_1..d_m] → 꼭짓점(S, m+2)."""
xE, xX = p[:, 0], p[:, 1]
s = np.sort(p[:, 2 : 2 + m], axis=1)
d = p[:, 2 + m : 2 + 2 * m]
ix = xE[:, None] + s * (xX - xE)[:, None]
vx = np.column_stack([xE, ix, xX])
vz = np.column_stack([prep.surface(xE), prep.surface(ix) - d, prep.surface(xX)])
return vx, vz
def _convex(vx, vz, tol_deg=2.0):
ang = np.degrees(np.arctan2(vz[:, :-1] - vz[:, 1:], np.maximum(np.diff(vx, axis=1), 1e-9)))
return np.all(np.diff(ang, axis=1) <= tol_deg, axis=1)
def layer_bottom_depth(prep, x, material):
"""x 마다 그 지층(번호) 바닥 바로 위까지 깊이 — 없으면 nan(블록 씨앗: 약층 바닥을 따라 미끄러짐)."""
x = np.atleast_1d(x)
bot, top, mat = prep.columns(x[None, :])
zt = prep.surface(x)[None, :]
z = np.where((mat == material) & (top > bot), bot, np.nan)
with np.errstate(all="ignore"):
zb = np.nanmax(z, axis=-1)
return (zt - zb - 0.02)[0]
def optimize_polyline(
prep, limits, method, seeds, n, min_depth, m=4, seed=0, maxiter=60, polish=True
):
"""비원호 꼭짓점 최적화 — 차분진화 뒤 가장 좋은 면을 Nelder-Mead 로 국부 다듬기(상용 SAO · Optimize 대응)."""
fun = _objective(method)
xl, xr = limits
depth_max = max(prep.surface(np.linspace(xl, xr, 50)).max() - prep.bottom_z, 1.0)
lo = np.array([xl, xl] + [0.02] * m + [0.0] * m)
hi = np.array([xr, xr] + [0.98] * m + [depth_max] * m)
def obj(p):
p = np.atleast_2d(p)
vx, vz = _decode(prep, p, m)
ok = (p[:, 1] - p[:, 0] > 0.2) & _convex(vx, vz)
F = np.full(len(p), _BIG)
if ok.any():
sl = poly_slices(prep, vx[ok], vz[ok], n, min_depth)
f = fun(sl, three_point_center(vx[ok], vz[ok]))
F[ok] = np.where(np.isfinite(f), f, _BIG)
return F
step = np.array([0.5, 0.5] + [0.03] * m + [0.3] * m)
x, fmin, pop, f = _local_search(obj, np.asarray(seeds, float), lo, hi, step, maxiter, seed)
if polish and fmin < _BIG:
r = minimize(
lambda q: float(obj(np.clip(q, lo, hi))[0]),
x,
method="Nelder-Mead",
options={"maxfev": 200, "xatol": 1e-4, "fatol": 1e-6},
)
if r.fun < fmin:
x, fmin = np.clip(r.x, lo, hi), float(r.fun)
order = np.argsort(f)
vx, vz = _decode(prep, x[None, :], m)
return fmin, x, (vx[0], vz[0]), [x, *pop[order]]
def _best_seeds(prep, seeds, m, n, min_depth, keep):
"""씨앗을 Janbu 간편으로 한 번 셈 → 좋은 것 keep 개(최적화 첫 무리 · 나쁜 씨앗이 무리를 흐리지 않게)."""
p = np.asarray(seeds, float)
vx, vz = _decode(prep, p, m)
ok = (p[:, 1] - p[:, 0] > 0.2) & _convex(vx, vz)
F = np.full(len(p), np.inf)
if ok.any():
sl = poly_slices(prep, vx[ok], vz[ok], n, min_depth)
f = lem.janbu(sl)["F"]
F[ok] = np.where(np.isfinite(f), f, np.inf)
order = np.argsort(F)[:keep]
return [seeds[i] for i in order if np.isfinite(F[i])] or seeds[:keep]
def circle_to_params(prep, circle, m):
xc, zc, R, xE, xX = circle
s = (np.arange(1, m + 1) / (m + 1)).tolist()
ix = xE + np.array(s) * (xX - xE)
arc = zc - np.sqrt(np.clip(R**2 - (ix - xc) ** 2, 0, None))
d = np.clip(prep.surface(ix) - arc, 0, None)
return [xE, xX, *s, *d.tolist()]
def _ray_to_surface(prep, x0, z0, angle_deg, sign):
"""(x0, z0) 에서 수평과 angle 로 위로(sign = −1 왼쪽 · +1 오른쪽) 쏜 반직선이 지표와 만나는 x."""
t = np.linspace(0.0, 1.0, 400)[None, :] * 200.0
dx = sign * np.cos(np.radians(angle_deg))
dz = np.sin(np.radians(angle_deg))
xs = x0[:, None] + dx * t
above = (z0[:, None] + dz * t) >= prep.surface(xs)
k = np.argmax(above, axis=1)
hit = above[np.arange(len(x0)), k]
return np.where(hit & (k > 0), xs[np.arange(len(x0)), k], np.nan)
def block_seeds(prep, limits, m, n_points=12, angles=((55, 35), (65, 25))):
"""블록 씨앗(Slide Block Search 대응) — 지층마다 바닥선 위 두 점을 잇는 토막 + 머리 쪽 능동 쐐기 ·
끝 쪽 수동 쐐기를 지표까지 올림. 안쪽 꼭짓점 = 토막 두 끝 + 사이 (m − 2) 점."""
xl, xr = limits
xs = np.linspace(xl, xr, n_points)
i, j = np.triu_indices(n_points, k=1)
xa, xb = xs[i], xs[j]
out = []
for mat in range(len(prep.materials)):
da, db = layer_bottom_depth(prep, xa, mat), layer_bottom_depth(prep, xb, mat)
ok = np.isfinite(da) & np.isfinite(db)
if not ok.any():
continue
a, b = xa[ok], xb[ok]
za, zb = prep.surface(a) - da[ok], prep.surface(b) - db[ok]
for act, pas in angles:
xE = _ray_to_surface(prep, a, za, act, -1.0)
xX = _ray_to_surface(prep, b, zb, pas, 1.0)
good = np.isfinite(xE) & np.isfinite(xX) & (xE >= xl) & (xX <= xr)
for e, x0, x1, xx in zip(xE[good], a[good], b[good], xX[good]):
ix = np.linspace(x0, x1, m)
d = layer_bottom_depth(prep, ix, mat)
if np.isnan(d).any():
continue
span = xx - e
out.append([e, xx, *((ix - e) / span).tolist(), *np.clip(d, 0.0, None).tolist()])
return out
# ---- 결과 꾸미기 ----------------------------------------------------------------
def describe(prep, kind, geom, method, n, min_depth) -> dict:
s = prep.direction
if kind == "circle":
xc, zc, R, xE, xX = geom
sl = circle_slices(prep, [xc], [zc], [R], [xE], [xX], n, min_depth)
center = np.array([[xc, zc]])
xs = sl["xe"][0]
pts = np.column_stack([xs, sl["ze"][0]])
else:
vx, vz = geom
sl = poly_slices(prep, vx[None, :], vz[None, :], n, min_depth)
center = three_point_center(vx[None, :], vz[None, :])
pts = np.column_stack([vx, vz])
res = evaluate(sl, center, (method,))[method]
F = float(res["F"][0])
lam = float(res.get("lambda", [0.0])[0]) if method == "spencer" else 0.0
rows = lem.slice_table(sl, 0, F, lam, method) if np.isfinite(F) else []
for r in rows:
r["material"] = (
prep.materials[r["material"]]["id"] if r["material"] < len(prep.materials) else None
)
r["x_left"], r["x_right"] = sorted((s * r["x_left"], s * r["x_right"]))
out = {
"F": F if np.isfinite(F) else None,
"kind": kind,
"points": [[float(s * x), float(z)] for x, z in (pts if s > 0 else pts[::-1])],
"entry": [float(s * sl["xe"][0, 0]), float(sl["ze"][0, 0])],
"exit": [float(s * sl["xe"][0, -1]), float(sl["ze"][0, -1])],
"iterations": int(res["iterations"][0]),
"converged": bool(res["converged"][0]),
"slices": rows,
}
if kind == "circle":
out["center"] = [float(s * center[0, 0]), float(center[0, 1])]
out["radius"] = float(geom[2])
if method == "spencer":
out["lambda"] = lam
out["theta_deg"] = float(np.degrees(np.arctan(lam)))
if method == "janbu":
out["F0"] = float(res["F0"][0]) if np.isfinite(res["F0"][0]) else None
out["f0"] = float(res["f0"][0])
if prep.crack_depth > 0:
out["crack_water_force"] = float(sl["Pw"][0])
return out
# ---- 한 비탈 해석 ----------------------------------------------------------------
def analyze(
model: dict,
*,
direction: int = 1,
limits=None,
methods=METHODS,
circular=True,
noncircular=True,
n_slices=50,
coarse_slices=30,
min_depth=0.0,
grid=(15, 15, 10),
entry_exit_points=25,
polyline_vertices=4,
seed=0,
) -> dict:
"""한 비탈(활동 방향 하나)의 방법별 최소 안전율 · 임계면 · 절편표 · 등안전율 격자 · 수렴 정보."""
t0 = time.perf_counter()
prep = lem.Prepared(model, direction)
s = prep.direction
if limits is None:
lim = (float(prep.surface_x[0]), float(prep.surface_x[-1]))
else:
lim = tuple(sorted((s * float(limits[0]), s * float(limits[1]))))
results: dict = {m: {} for m in methods}
counts = {}
contour = None
best_circles: dict = {}
if circular:
a = entry_exit_circles(prep, lim, entry_exit_points)
gx, gz = grid_centers(prep, lim, grid[0], grid[1])
b = grid_radius_circles(prep, lim, gx, gz, grid[2])
coarse = tuple(np.concatenate([a[k], b[k]]) for k in range(5))
cheap = tuple(m for m in methods if m != "spencer") or ("bishop",)
F = _circle_eval(prep, *coarse, coarse_slices, min_depth, cheap)
counts["circles"] = int(len(coarse[0]))
counts["circles_valid"] = int(np.sum(np.isfinite(F[cheap[0]])))
# 등안전율 격자 — 격자 중심마다 최소(Bishop 우선)
key = "bishop" if "bishop" in F else cheap[0]
fb = F[key][len(a[0]) :]
cmin = np.full(len(gx) * len(gz), np.nan)
for o, f in zip(b[5], fb):
if np.isfinite(f) and not (f >= cmin[o]):
cmin[o] = f
contour = {
"method": key,
"x": [float(s * v) for v in gx],
"z": [float(v) for v in gz],
"F": [
[None if not np.isfinite(v) else float(v) for v in row]
for row in cmin.reshape(len(gz), len(gx))
],
}
for m in methods:
score = F[m] if m in F else F[key]
order = np.argsort(np.where(np.isfinite(score), score, np.inf))[:30]
order = order[np.isfinite(score[order])]
if not len(order):
continue
seeds = np.column_stack([coarse[3][order], coarse[4][order], _theta(coarse, order)])
fmin, geom = refine_circle(prep, lim, m, seeds, n_slices, min_depth, seed)
if fmin < _BIG:
results[m]["circular"] = describe(prep, "circle", geom, m, n_slices, min_depth)
best_circles[m] = geom
if noncircular:
mv = polyline_vertices
seeds = []
circle = (
best_circles.get("janbu")
or best_circles.get("bishop")
or next(iter(best_circles.values()), None)
)
if circle:
seeds.append(circle_to_params(prep, circle, mv))
seeds += block_seeds(prep, lim, mv)
seeds = _best_seeds(prep, seeds, mv, n_slices, min_depth, 40)
# 힘 평형 방법(Janbu) 먼저 · Spencer 는 그 마지막 무리에서 이어 찾음 · 모멘트 방법(Fellenius · Bishop)은
# 비원호 중심이 임의라 따로 최적화하지 않고 찾은 면에서 값만 셈(상용 SW 도 비원호 최적화 = Janbu · Spencer · GLE)
found = []
for m in [k for k in ("janbu", "spencer") if k in methods]:
fmin, _x, geom, top = optimize_polyline(
prep, lim, m, seeds, n_slices, min_depth, mv, seed
)
seeds = [*top, *seeds]
if fmin < _BIG:
results[m]["noncircular"] = describe(prep, "polyline", geom, m, n_slices, min_depth)
found.append(geom)
for m in [k for k in ("ordinary", "bishop") if k in methods]:
cands = [describe(prep, "polyline", g, m, n_slices, min_depth) for g in found]
cands = [c for c in cands if c["F"] is not None]
if cands:
results[m]["noncircular"] = min(cands, key=lambda c: c["F"])
for m in methods:
cands = [v for v in results[m].values() if v.get("F") is not None]
results[m]["min"] = min(cands, key=lambda v: v["F"]) if cands else None
return {
"direction": s,
"limits": sorted([s * lim[0], s * lim[1]]),
"methods": results,
"contour": contour,
"counts": counts,
"elapsed_s": round(time.perf_counter() - t0, 3),
}
def _theta(coarse, order):
xc, zc, R, xE, xX = (v[order] for v in coarse)
zE = zc - np.sqrt(np.clip(R**2 - (xE - xc) ** 2, 0, None))
zX = zc - np.sqrt(np.clip(R**2 - (xX - xc) ** 2, 0, None))
chord = np.hypot(xX - xE, zX - zE)
return 2 * np.arcsin(np.clip(chord / (2 * R), 0, 1))
def evaluate_specified(
model: dict, *, circle=None, polyline=None, direction=1, methods=METHODS, n_slices=50
):
"""지정 활동면 하나(원 [xc, zc, R] 또는 꺾은선 [[x, z], …])의 방법별 안전율 · 절편표 — 대조 · 검산용."""
prep = lem.Prepared(model, direction)
s = prep.direction
out = {}
if circle is not None:
xc, zc, R = s * float(circle[0]), float(circle[1]), float(circle[2])
xE, xX, ok = circle_cross_surface(prep, np.array([xc]), np.array([zc]), np.array([R]))
if not ok[0]:
raise ValueError("지정 원이 지표면을 지나지 않음")
geom, kind = (xc, zc, R, float(xE[0]), float(xX[0])), "circle"
else:
pts = sorted((s * float(x), float(z)) for x, z in polyline)
geom, kind = (np.array([p[0] for p in pts]), np.array([p[1] for p in pts])), "polyline"
for m in methods:
out[m] = describe(prep, kind, geom, m, n_slices, 0.0)
return out