Files
Aislo/E03_StabSection/E03_StabSection_Engine_LEM.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

509 lines
22 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 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