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,508 @@
"""E03 2D 한계평형해석(LEM) — 절편 · 네 방법(Fellenius · Bishop 간편 · Janbu 간편+보정 · Spencer).
좌표 x 오른쪽 · z 위 · 활동 방향 +x(왼쪽 위 → 오른쪽 아래). 반대쪽 비탈은 `Prepared(direction=-1)`
이 모형을 좌우로 뒤집어 맞춤 — 결과 좌표를 되돌리는 것은 부르는 쪽 몫(`unflip`).
식 = 지식DB `original/안정성평가/02` 절편 힘 · 모멘트 평형. 네 방법 모두 한 벌 식에서 나옴:
절편 i 연직 평형 → N = [W + λ·B0 − (c·l − u·l·tanφ)·sinα/F] / (m_α − λ·A)
m_α = cosα + sinα·tanφ/F · A = sinα − tanφ·cosα/F · B0 = kh·Ws − (c·l − u·l·tanφ)·cosα/F
λ = 절편간 힘 기울기(X = λ·E) — Bishop · Janbu 는 λ = 0 · Spencer 는 모멘트 · 힘 평형을 함께 맞추는 λ.
모멘트 F_m = Σ a·R / D (점 O 둘레) · 힘 F_f = Σ R·cosα / (Σ(N·sinα + kh·Ws) + Pw) · R = c·l + (N − u·l)·tanφ
외부 코드 없음(NumPy 만) — 상용 · 오픈소스 결과는 대조 시험에서만 씀.
"""
import numpy as np
from E03_StabSection.E03_StabSection_Types import GAMMA_W, groundwater, material, surcharges
_TINY = 1e-9
_PAD_Z = -1e9
#: m_α 가 이 값보다 작은 절편이 있으면 그 활동면을 버림(상용 SW 관례 0.2 — 진출부 급경사 발산 막음).
MALPHA_MIN = 0.2
#: Spencer 절편간 힘 기울기 λ = X/E 범위 — 여기 부호(X = 윗 절편이 아랫 절편에 주는 연직력, 위 +)로
#: 윗 덩이가 아랫 덩이를 내리누르면 λ < 0. 아래 한도 −1.25 = SLOPE/W GLE λ 범위 · 위는 작은 + 만 허용
#: (아랫 덩이를 들어 올리는 해 = 비현실 근 — Yamagami & Ueta 대조에서 λ ≈ +1 근이 F 를 9 % 낮췄음).
LAMBDA_RANGE = (-1.25, 0.25)
#: 이보다 작은 F = 1/F 항이 터진 엉뚱한 근(실제 비탈 F 는 이 위) — 버림.
F_MIN = 0.05
TOL = 1e-6
MAX_ITER = 100
class Prepared:
"""모형 → 절편 계산용 배열. 지층 = 꼭짓점 x 사이 띠마다 연직 구간(바닥 · 윗면 · 물성) — 띠 안은 선형이라 정확."""
def __init__(self, model: dict, direction: int = 1):
self.direction = 1 if direction >= 0 else -1
s = float(self.direction)
mats = [material(m) for m in model["materials"]]
index = {m["id"]: i for i, m in enumerate(mats)}
self.materials = mats
pad = len(mats) # 빈 칸 번호
self.gamma_t = np.array([m["gamma_t"] for m in mats] + [0.0])
self.gamma_sat = np.array([m["gamma_sat"] for m in mats] + [0.0])
self.c = np.array([m["c"] for m in mats] + [np.nan])
self.tanphi = np.array([np.tan(np.radians(m["phi"])) for m in mats] + [np.nan])
polys = []
for region in model["regions"]:
rings = [region["polygon"], *region.get("holes", [])]
edges = []
for ring in rings:
pts = np.asarray(ring, dtype=float) * [s, 1.0]
if np.allclose(pts[0], pts[-1]):
pts = pts[:-1]
edges.append(np.column_stack([pts, np.roll(pts, -1, axis=0)]))
polys.append((np.concatenate(edges), index[region["material_id"]]))
self._build_columns(polys, pad)
gw = groundwater(model.get("groundwater"))
self.gw_mode = gw["mode"]
self.ru = float(gw.get("ru", 0.0))
if self.gw_mode == "line":
line = sorted([s * x, z] for x, z in gw["line"])
self.water_x = np.array([p[0] for p in line])
self.water_z = np.array([p[1] for p in line])
self.kh = float(model.get("seismic_kh") or 0.0)
loads = surcharges(model.get("surcharge"))
self.loads = np.array(
[sorted((s * q["x0"], s * q["x1"])) + [q["q"]] for q in loads], dtype=float
).reshape(-1, 3)
crack = model.get("tension_crack") or {}
self.crack_depth = float(crack.get("depth") or 0.0)
self.crack_water = float(crack.get("water") or 0.0)
def _build_columns(self, polys: list, pad: int) -> None:
xs = np.unique(np.concatenate([e[:, 0] for e, _ in polys]))
self.X = xs
gap = np.diff(xs)
self.xa = xs[:-1] + 1e-7 * gap
self.xb = xs[1:] - 1e-7 * gap
q = np.concatenate([self.xa, self.xb])
bots, tops, mats = [], [], []
for edges, mat in polys:
x1, z1, x2, z2 = edges.T
lo, hi = np.minimum(x1, x2), np.maximum(x1, x2)
cross = (lo[None, :] < q[:, None]) & (q[:, None] < hi[None, :])
with np.errstate(invalid="ignore", divide="ignore"):
z = z1 + (q[:, None] - x1) * (z2 - z1) / (x2 - x1)
z = np.sort(np.where(cross, z, np.nan), axis=1)
count = int(np.max(np.sum(cross, axis=1))) if len(q) else 0
if count < 2:
continue
bots.append(z[:, 0 : count - 1 : 2])
tops.append(z[:, 1:count:2])
mats.append(np.full(bots[-1].shape, mat))
bot = np.concatenate(bots, axis=1)
top = np.concatenate(tops, axis=1)
mat = np.concatenate(mats, axis=1)
mat = np.where(np.isnan(bot), pad, mat)
half = len(self.xa)
order = np.argsort(np.where(np.isnan(bot[:half]), np.inf, bot[:half]), axis=1)
self.bot_a, self.bot_b = (np.take_along_axis(bot[k], order, 1) for k in _halves(half))
self.top_a, self.top_b = (np.take_along_axis(top[k], order, 1) for k in _halves(half))
self.mat = np.take_along_axis(mat[:half], order, 1)
# 지표면 꺾은선(정확) — 띠 양 끝으로 늘린 맨 윗면 · 연직 면은 같은 x 두 점
ta = np.nanmax(self.top_a, axis=1)
tb = np.nanmax(self.top_b, axis=1)
slope = (tb - ta) / (self.xb - self.xa)
left = ta + (xs[:-1] - self.xa) * slope
right = tb + (xs[1:] - self.xb) * slope
sx = np.column_stack([xs[:-1], xs[1:]]).ravel()
sz = np.column_stack([left, right]).ravel()
keep = np.ones(len(sx), bool)
keep[1:] = (np.abs(np.diff(sx)) > 1e-9) | (np.abs(np.diff(sz)) > 1e-6)
self.surface_x, self.surface_z = sx[keep], sz[keep]
ba = np.nanmin(self.bot_a, axis=1)
bb = np.nanmin(self.bot_b, axis=1)
self.bottom_z = float(min(np.min(ba), np.min(bb)))
# 빈 칸 = 아주 낮은 두께 0 구간(nan 처리 없이 바로 셈)
for name in ("bot_a", "bot_b", "top_a", "top_b"):
setattr(self, name, np.nan_to_num(getattr(self, name), nan=_PAD_Z))
# ---- 조회 -------------------------------------------------------------
def surface(self, x: np.ndarray) -> np.ndarray:
"""지표면 높이(연직 면은 낮은 쪽 · 모형 밖은 nan)."""
x = np.asarray(x, dtype=float)
z = np.interp(x, self.surface_x, self.surface_z)
return np.where((x < self.X[0]) | (x > self.X[-1]), np.nan, z)
def columns(self, x: np.ndarray):
j = np.clip(np.searchsorted(self.X, x, side="right") - 1, 0, len(self.xa) - 1)
t = ((x - self.xa[j]) / (self.xb[j] - self.xa[j]))[..., None]
bot = self.bot_a[j] + t * (self.bot_b[j] - self.bot_a[j])
top = self.top_a[j] + t * (self.top_b[j] - self.top_a[j])
return bot, top, self.mat[j]
def water(self, x: np.ndarray) -> np.ndarray:
return np.interp(x, self.water_x, self.water_z)
def _halves(half: int):
return (slice(0, half), slice(half, None))
def slices(prep: Prepared, xe: np.ndarray, ze: np.ndarray) -> dict:
"""활동면(절편 경계 x · 바닥 z, 꼴 (S, n+1)) → 절편 값 (S, n) · 잘못된 면은 valid False."""
xe = np.atleast_2d(np.asarray(xe, float))
ze = np.atleast_2d(np.asarray(ze, float))
b = np.diff(xe, axis=1)
xm = 0.5 * (xe[:, :-1] + xe[:, 1:])
zb = 0.5 * (ze[:, :-1] + ze[:, 1:])
alpha = np.arctan2(ze[:, :-1] - ze[:, 1:], np.maximum(b, _TINY))
bot, top, mat = prep.columns(xm)
lo = np.maximum(bot, zb[..., None])
thick = np.clip(top - lo, 0.0, None)
zt = prep.surface(xm)
if prep.gw_mode == "line":
zw = np.minimum(prep.water(xm), zt)
wet = np.clip(np.minimum(top, zw[..., None]) - lo, 0.0, None)
dry = thick - wet
u = GAMMA_W * np.clip(zw - zb, 0.0, None)
wet_mid = lo + 0.5 * wet
dry_mid = lo + wet + 0.5 * dry
g_dry, g_wet = prep.gamma_t[mat] * dry, prep.gamma_sat[mat] * wet
else:
g_dry, g_wet = prep.gamma_t[mat] * thick, 0.0 * thick
dry_mid = wet_mid = lo + 0.5 * thick
u = np.zeros_like(zb)
weight_per_b = np.sum(g_dry + g_wet, axis=-1)
moment_z = np.sum(g_dry * dry_mid + g_wet * wet_mid, axis=-1)
zg = np.where(weight_per_b > 0, moment_z / np.maximum(weight_per_b, _TINY), zb)
Ws = weight_per_b * b
if prep.gw_mode == "ru":
u = prep.ru * weight_per_b
# 바닥 물성 = 바닥 바로 위 구간(약층 바닥을 따라가는 면은 그 약층)
probe = zb[..., None] + 1e-6
inside = (bot <= probe) & (probe < top) & (top - bot > 1e-9)
base_k = np.argmax(inside, axis=-1)
base_mat = np.take_along_axis(mat, base_k[..., None], -1)[..., 0]
found = np.any(inside, axis=-1)
c = prep.c[base_mat]
tanphi = prep.tanphi[base_mat]
# 상재하중 — 절편 폭과 겹친 길이만큼 정확히 · 작용점 = 겹친 구간 가운데
Q = np.zeros_like(zb)
Mq = np.zeros_like(zb)
for x0, x1, q in prep.loads:
a = np.clip(xe[:, :-1], x0, x1)
bb = np.clip(xe[:, 1:], x0, x1)
ov = bb - a
Q += q * ov
Mq += q * ov * 0.5 * (a + bb)
W = Ws + Q
xw = np.where(W > 0, (Ws * xm + Mq) / np.maximum(W, _TINY), xm)
valid = np.all(found & (zb >= prep.bottom_z - 1e-6) & np.isfinite(zt), axis=1)
valid &= np.all(b > 0, axis=1)
return {
"xe": xe,
"ze": ze,
"b": b,
"xm": xm,
"zb": zb,
"zt": zt,
"alpha": alpha,
"sa": np.sin(alpha),
"ca": np.cos(alpha),
"l": b / np.cos(alpha),
"W": W,
"Ws": Ws,
"Q": Q,
"xw": xw,
"zg": zg,
"u": u,
"c": np.where(found, c, 0.0),
"tanphi": np.where(found, tanphi, 0.0),
"mat": base_mat,
"valid": valid,
"kh": prep.kh,
"Pw": np.zeros(len(xe)),
"zp": ze[:, 0].copy(),
}
def add_crack_water(sl: dict, depth: np.ndarray, fill: float) -> None:
"""인장균열 물 — 머리 쪽 연직 면에 수평 수압 Pw = ½·γw·(f·z_c)² · 바닥에서 f·z_c/3."""
hw = fill * np.asarray(depth, float)
sl["Pw"] = 0.5 * GAMMA_W * hw**2
sl["zp"] = sl["ze"][:, 0] + hw / 3.0
# ---- 방법 -------------------------------------------------------------------
def _parts(sl: dict, F, lam):
"""주어진 F · λ 의 절편 수직력 N · 저항 R · 분모(m_α − λA).
유효 수직력 음(인장) 절편도 그대로 둠(Slide2 와 같음 — 0 으로 자르면 ACADS · Arai & Tagyo 대조가 3~6 % 벗어남).
대신 저항 합이 0 이하인 면은 `_finish` 가 버림.
"""
F = np.asarray(F, float)[:, None]
lam = np.asarray(lam, float)[:, None]
W = sl["W"].copy()
W[:, 0] += lam[:, 0] * sl["Pw"] # 머리 경계 수압은 전단 없음 → X_0 = 0
t = sl["tanphi"]
N, den, A, B0 = _normal(sl, F, lam, W, t)
R = sl["c"] * sl["l"] + (N - sl["u"] * sl["l"]) * t
return N, R, den, A, B0
def _normal(sl, F, lam, W, t):
sa, ca = sl["sa"], sl["ca"]
cu = sl["c"] * sl["l"] - sl["u"] * sl["l"] * t
m = ca + sa * t / F
A = sa - t * ca / F
B0 = sl["kh"] * sl["Ws"] - cu * ca / F
den = m - lam * A
N = (W + lam * B0 - cu * sa / F) / np.where(np.abs(den) < _TINY, _TINY, den)
return N, den, A, B0
def _moment_terms(sl: dict, center: np.ndarray, N: np.ndarray):
xo, zo = center[:, 0:1], center[:, 1:2]
a = (sl["xm"] - xo) * sl["sa"] + (sl["zb"] - zo) * sl["ca"]
D = (
(sl["xw"] - xo) * sl["W"]
+ (sl["zg"] - zo) * sl["kh"] * sl["Ws"]
- (sl["xm"] - xo) * N * sl["ca"]
+ (sl["zb"] - zo) * N * sl["sa"]
).sum(1) + (sl["zp"] - center[:, 1]) * sl["Pw"]
return a, D
def _force_den(sl: dict, N: np.ndarray):
return (N * sl["sa"] + sl["kh"] * sl["Ws"]).sum(1) + sl["Pw"]
def _fm(sl, center, F, lam):
N, R, den, *_ = _parts(sl, F, lam)
a, D = _moment_terms(sl, center, N)
return (a * R).sum(1) / np.where(np.abs(D) < _TINY, -_TINY, D), den
def _ff(sl, F, lam):
N, R, den, *_ = _parts(sl, F, lam)
return (R * sl["ca"]).sum(1) / np.where(
np.abs(_force_den(sl, N)) < _TINY, _TINY, _force_den(sl, N)
), den
def _iterate(fun, F0):
"""F = fun(F) 풀이(묶음 전체) — 수치 미분 뉴턴(h = fun(F) − F) · 안 되면 반쯤 섞은 고정점 걸음.
단순 고정점은 급한 머리 절편에서 진동해 느림(도함수 ≈ −1) — 뉴턴은 몇 번에 끝남.
"""
F = np.asarray(F0, float).copy()
done = np.zeros(len(F), bool)
iters = np.zeros(len(F), int)
den = None
for _ in range(MAX_ITER):
f1, den = fun(F)
h1 = f1 - F
delta = 1e-6 * np.maximum(np.abs(F), 0.1)
f2, _ = fun(F + delta)
dh = ((f2 - (F + delta)) - h1) / delta
newton = F - h1 / np.where(np.abs(dh) < 1e-12, -1.0, dh)
relaxed = 0.5 * (F + f1)
use = np.isfinite(newton) & (dh < -1e-9) & (newton > 0.5 * F) & (newton < 2.0 * F)
Fn = np.where(use, newton, relaxed)
Fn = np.where(np.isfinite(Fn) & (Fn > 0), Fn, np.nan)
conv = np.abs(h1) < TOL * np.maximum(1.0, np.abs(F))
F = np.where(done | conv, F, Fn)
iters += ~done
done |= conv | ~np.isfinite(F)
if done.all():
break
f1, den = fun(np.where(np.isfinite(F), F, 1.0))
return F, iters, done & np.isfinite(F), den
def ordinary(sl: dict, center: np.ndarray) -> dict:
"""Fellenius — 절편간 힘 무시 · 바닥 수직 분해 N = W·cosα − kh·Ws·sinα · 모멘트 평형."""
N = sl["W"] * sl["ca"] - sl["kh"] * sl["Ws"] * sl["sa"]
R = sl["c"] * sl["l"] + (N - sl["u"] * sl["l"]) * sl["tanphi"]
a, D = _moment_terms(sl, center, N)
F = (a * R).sum(1) / np.where(np.abs(D) < _TINY, -_TINY, D)
ok = sl["valid"] & np.isfinite(F) & (F > F_MIN) & (R.sum(1) > 0)
# 같은 꼴 거르기 — 진출부 급경사(m_α < 0.2) 면은 Bishop 처럼 버림
m = sl["ca"] + sl["sa"] * sl["tanphi"] / np.where(ok, F, 1.0)[:, None]
ok &= np.all(m >= MALPHA_MIN, axis=1)
return {"F": np.where(ok, F, np.nan), "iterations": np.ones(len(F), int), "converged": ok}
def bishop(sl: dict, center: np.ndarray, F0=None) -> dict:
F0 = np.full(len(sl["W"]), 1.5) if F0 is None else F0
lam = np.zeros(len(F0))
F, it, conv, _ = _iterate(lambda f: _fm(sl, center, f, lam), F0)
return _finish(sl, F, it, conv, lam)
def janbu(sl: dict, F0=None) -> dict:
"""Janbu 간편(F_0) + 보정 f_0 — 지식DB 02 §2.3 · b1 = 0.31(c=0) · 0.69(φ=0) · 0.50."""
F0 = np.full(len(sl["W"]), 1.5) if F0 is None else F0
lam = np.zeros(len(F0))
F, it, conv, _ = _iterate(lambda f: _ff(sl, f, lam), F0)
out = _finish(sl, F, it, conv, lam)
out["F0"] = out["F"]
out["f0"] = janbu_f0(sl)
out["F"] = out["F0"] * out["f0"]
return out
def janbu_f0(sl: dict) -> np.ndarray:
xe, ze = sl["xe"], sl["ze"]
dx, dz = xe[:, -1:] - xe[:, :1], ze[:, -1:] - ze[:, :1]
L = np.hypot(dx, dz)[:, 0]
dist = np.abs(dz * (xe - xe[:, :1]) - dx * (ze - ze[:, :1]))[:, :] / np.maximum(
L[:, None], _TINY
)
ratio = dist.max(1) / np.maximum(L, _TINY)
c_all0 = np.all(sl["c"] <= 0, axis=1)
phi_all0 = np.all(sl["tanphi"] <= 0, axis=1)
b1 = np.where(c_all0, 0.31, np.where(phi_all0, 0.69, 0.50))
return 1.0 + b1 * (ratio - 1.4 * ratio**2)
def spencer(sl: dict, center: np.ndarray, F0=None, scan: bool = True) -> dict:
"""Spencer — 모멘트 · 힘 두 평형을 함께 맞추는 (F, λ) · 뉴턴(수치 야코비안) · 안 맞으면 λ 훑기.
scan=False — 탐색 고리 안(시험 면 대부분이 버려짐)에서는 훑기를 건너뜀(시간 대부분이 여기).
"""
S = len(sl["W"])
F = np.full(S, 1.5) if F0 is None else np.asarray(F0, float).copy()
start = F.copy()
lam = np.zeros(S)
# 엉뚱한 근(F → 0 쪽) 거르기 — 엄밀해는 출발값(Bishop)의 절반~두 배 안에 있음
near = lambda f, ref: (f > 0.5 * ref) & (f < 2.0 * ref) # noqa: E731
F, lam, it, conv = _spencer_newton(sl, center, F, lam)
conv &= near(F, start)
bad = ~conv & sl["valid"]
if scan and bad.any():
sub = _subset(sl, bad)
Fs, ls = _spencer_scan(sub, center[bad])
F2, l2, it2, c2 = _spencer_newton(sub, center[bad], Fs, ls)
F[bad], lam[bad], it[bad], conv[bad] = F2, l2, it[bad] + it2, c2 & near(F2, start[bad])
out = _finish(sl, F, it, conv, lam)
out["lambda"] = np.where(np.isfinite(out["F"]), lam, np.nan)
return out
def _residual(sl, center, F, lam):
N, R, den, *_ = _parts(sl, F, lam)
a, D = _moment_terms(sl, center, N)
fd = _force_den(sl, N)
rm = ((a * R).sum(1) - F * D) / np.maximum(np.abs(D), _TINY)
rf = ((R * sl["ca"]).sum(1) - F * fd) / np.maximum(np.abs(fd), _TINY)
return rm, rf
def _spencer_newton(sl, center, F, lam, iters=40):
F, lam = F.copy(), lam.copy()
it = np.zeros(len(F), int)
done = np.zeros(len(F), bool)
for _ in range(iters):
rm, rf = _residual(sl, center, F, lam)
hF = 1e-6 * np.maximum(F, 0.1)
hl = 1e-6
rmF, rfF = _residual(sl, center, F + hF, lam)
rml, rfl = _residual(sl, center, F, lam + hl)
j11, j12 = (rmF - rm) / hF, (rml - rm) / hl
j21, j22 = (rfF - rf) / hF, (rfl - rf) / hl
det = j11 * j22 - j12 * j21
det = np.where(np.abs(det) < 1e-14, np.nan, det)
dF = -(j22 * rm - j12 * rf) / det
dl = -(-j21 * rm + j11 * rf) / det
scale = np.minimum(
1.0,
np.minimum(
0.5 * F / np.maximum(np.abs(dF), _TINY), 0.3 / np.maximum(np.abs(dl), _TINY)
),
)
F = np.where(done, F, F + scale * dF)
lam = np.where(done, lam, lam + scale * dl)
it += ~done
small = (np.abs(dF) < TOL * np.maximum(1, np.abs(F))) & (np.abs(dl) < TOL)
done |= small | ~np.isfinite(F)
if done.all():
break
rm, rf = _residual(sl, center, F, lam)
conv = done & np.isfinite(F) & (F > 0) & (np.abs(rm) < 1e-4) & (np.abs(rf) < 1e-4)
conv &= (lam >= LAMBDA_RANGE[0]) & (lam <= LAMBDA_RANGE[1])
return F, lam, it, conv
def _spencer_scan(sl, center):
"""λ 를 훑어 F_m(λ) − F_f(λ) 부호가 바뀌는 곳(λ=0 가까운 쪽)을 출발점으로."""
grid = np.linspace(LAMBDA_RANGE[0], LAMBDA_RANGE[1], 16)
S = len(sl["W"])
diff = np.full((len(grid), S), np.nan)
fm_all = np.full((len(grid), S), np.nan)
F0 = np.full(S, 1.5)
for k, g in enumerate(grid):
lam = np.full(S, g)
fm, *_ = _iterate(lambda f: _fm(sl, center, f, lam), F0)
ff, *_ = _iterate(lambda f: _ff(sl, f, lam), F0)
diff[k], fm_all[k] = fm - ff, fm
best_F, best_l = np.full(S, 1.5), np.zeros(S)
best_gap = np.full(S, np.inf)
for k in range(len(grid) - 1):
d0, d1 = diff[k], diff[k + 1]
cross = np.isfinite(d0) & np.isfinite(d1) & (np.sign(d0) != np.sign(d1))
t = np.where(cross, d0 / np.where(d0 == d1, 1, d0 - d1), 0)
lam_c = grid[k] + t * (grid[k + 1] - grid[k])
F_c = fm_all[k] + t * (fm_all[k + 1] - fm_all[k])
better = cross & (np.abs(lam_c) < best_gap)
best_gap = np.where(better, np.abs(lam_c), best_gap)
best_F = np.where(better, F_c, best_F)
best_l = np.where(better, lam_c, best_l)
return best_F, best_l
def _subset(sl: dict, mask: np.ndarray) -> dict:
out = {
k: (v[mask] if isinstance(v, np.ndarray) and v.shape[:1] == mask.shape else v)
for k, v in sl.items()
}
return out
def _finish(sl, F, it, conv, lam):
_, R, den, *_ = _parts(sl, np.where(np.isfinite(F) & (F > 0), F, 1.0), lam)
malpha_ok = np.all(den >= MALPHA_MIN, axis=1)
ok = sl["valid"] & conv & np.isfinite(F) & (F > F_MIN) & malpha_ok & (R.sum(1) > 0)
return {
"F": np.where(ok, F, np.nan),
"iterations": it,
"converged": conv,
"malpha_ok": malpha_ok,
}
def slice_table(sl: dict, k: int, F: float, lam: float = 0.0, method: str = "bishop") -> list[dict]:
"""한 면(k 번째)의 절편표 — 폭 · 높이 · 경사 · 무게 · 수압 · 강도 · N' · 동원 전단 · (Spencer) E · X."""
one = _subset(sl, np.arange(len(sl["W"])) == k)
if method == "ordinary":
N = one["W"] * one["ca"] - one["kh"] * one["Ws"] * one["sa"]
E = np.zeros((1, N.shape[1] + 1))
else:
N, R, _den, A, B0 = _parts(one, np.array([F]), np.array([lam]))
dE = B0 + N * A
E = np.concatenate([one["Pw"][:, None], one["Pw"][:, None] + np.cumsum(dE, axis=1)], axis=1)
R = one["c"] * one["l"] + (N - one["u"] * one["l"]) * one["tanphi"]
rows = []
for i in range(N.shape[1]):
rows.append(
{
"x_left": float(one["xe"][0, i]),
"x_right": float(one["xe"][0, i + 1]),
"width": float(one["b"][0, i]),
"height": float(one["zt"][0, i] - one["zb"][0, i]),
"alpha_deg": float(np.degrees(one["alpha"][0, i])),
"weight": float(one["W"][0, i]),
"surcharge": float(one["Q"][0, i]),
"pore_pressure": float(one["u"][0, i]),
"base_length": float(one["l"][0, i]),
"material": int(one["mat"][0, i]),
"c": float(one["c"][0, i]),
"phi_deg": float(np.degrees(np.arctan(one["tanphi"][0, i]))),
"normal_eff": float(N[0, i] - one["u"][0, i] * one["l"][0, i]),
"shear_mobilized": float(R[0, i] / F),
"E_left": float(E[0, i]),
"X_left": float(lam * E[0, i]),
}
)
return rows