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