Files
Aislo/E03_StabSection/E03_StabSection_Engine_Search.py
T
eomsangdonandClaude Opus 5.5 bfd87f3e3e 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>
2026-10-25 20:07:14 +09:00

635 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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