- 모델: 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>
635 lines
27 KiB
Python
635 lines
27 KiB
Python
"""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
|