"""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