From f3cc1332975d56a6ec0fab0c1552e666eb2d51df Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 26 Oct 2026 04:00:59 +0900 Subject: [PATCH] =?UTF-8?q?fix(E03):=20Spencer=20=C2=B7=20M-P=20=ED=95=B4?= =?UTF-8?q?=20=EC=97=86=EC=9D=8C=20=3D=20=E3=80=8C=EC=88=98=EB=A0=B4=20?= =?UTF-8?q?=EC=95=88=20=EB=90=A8=E3=80=8D=20=E2=80=94=20=EB=84=93=EC=9D=80?= =?UTF-8?q?=20=CE=BB=20=EB=8B=A4=EC=8B=9C=20=EC=B0=BE=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EC=88=98=EB=A0=B4=20=EC=A0=95=EB=B3=B4=20=EC=B9=B8=20(532-2=20?= =?UTF-8?q?=EB=8D=A7=203=20=C2=B7=20=EC=9A=94=EA=B5=AC=20351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 좁은 λ 에 해 없으면 넓은 λ(Spencer −2 ~ 2 · M-P ±π · 41 점 거친 훑기 → 뉴턴) 다시 찾음 - Bishop 임계 원에서도 해 없음 + 찾은 최소 > Bishop = 그 방법 원호 「해 없음」(F None · status · reason) · 최소 F 후보에서 빠짐 - 수렴 정보 칸 residual · lambda_range · status(converged · no_solution · rejected) 덧붙임 · 해 없음 때도 F_m · F_f–λ 곡선(넓은 범위) - E03 방법 표 빈 값 = 「수렴 안 됨」(View no_solution 칸 · 옛 저장본은 없음) - 시험 test_532_2_stab_nosolution 새로 · 검증_sub4 59 측점 Bishop · Fellenius · Janbu 값 차 0 Co-Authored-By: Claude Opus 5.5 --- .../E03_StabSection_Engine_Describe.py | 39 ++++-- E03_StabSection/E03_StabSection_Engine_GLE.py | 34 ++++- E03_StabSection/E03_StabSection_Engine_LEM.py | 35 +++-- .../E03_StabSection_Engine_Search.py | 11 ++ E03_StabSection/E03_StabSection_UI_Result.ts | 6 +- E03_StabSection/E03_StabSection_View.py | 9 ++ .../logic/E03_StabSection_Logic.ts | 2 + .../tester/test_532_2_stab_nosolution.py | 122 ++++++++++++++++++ ui_template/ui_template_locale_e03.ts | 1 + 9 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 resources/tester/test_532_2_stab_nosolution.py diff --git a/E03_StabSection/E03_StabSection_Engine_Describe.py b/E03_StabSection/E03_StabSection_Engine_Describe.py index 0ca4ba4a0..6e3498b63 100644 --- a/E03_StabSection/E03_StabSection_Engine_Describe.py +++ b/E03_StabSection/E03_StabSection_Engine_Describe.py @@ -19,15 +19,17 @@ SAFETY_MAP_TOP = 50 _MAP_POINTS = 25 -def _curve(sl, center, method, f=None) -> dict: - """F_m · F_f 대 λ (화면 그래프) — Spencer = 상수 f · M-P = 반정현.""" +def _curve(sl, center, method, f=None, wide=False) -> dict: + """F_m · F_f 대 λ (화면 그래프) — Spencer = 상수 f · M-P = 반정현 · wide = 넓은 범위(해 없음이 보이게).""" if method == "spencer": - grid, fm, ff = lem.lambda_curve( - sl, center, np.linspace(*lem.LAMBDA_RANGE, gle.CURVE_POINTS) + rng, n = ( + (lem.LAMBDA_WIDE, lem.WIDE_POINTS) if wide else (lem.LAMBDA_RANGE, gle.CURVE_POINTS) ) + grid, fm, ff = lem.lambda_curve(sl, center, np.linspace(*rng, n)) fm, ff = fm[:, 0], ff[:, 0] else: - grid, fm, ff = gle.lambda_curves(sl, center, f) + grid = np.linspace(*gle.LAMBDA_WIDE, lem.WIDE_POINTS) if wide else None + grid, fm, ff = gle.lambda_curves(sl, center, f, grid) fm, ff = fm[0], ff[0] num = lambda v: [float(x) if np.isfinite(x) else None for x in v] # noqa: E731 return {"lambda": num(grid), "Fm": num(fm), "Ff": num(ff)} @@ -75,10 +77,29 @@ def describe(prep, kind, geom, method, n, min_depth, seed_F=None) -> dict: else: out["moment_center"] = [float(s * center[0, 0]), float(center[0, 1])] if method in ("spencer", "morgenstern_price"): - out["lambda"] = lam - out["theta_deg"] = float(np.degrees(np.arctan(lam))) - if np.isfinite(F): - out["lambda_curve"] = _curve(sl, center, method, f) + # 수렴 정보 — λ · 반복 수 · 잔차(max |F_m − F|, |F_f − F|) · 상태 · 해 없음 까닭 + solved = np.isfinite(F) + wide = bool(res["wide"][0]) + rng = ( + (lem.LAMBDA_WIDE if wide else lem.LAMBDA_RANGE) + if method == "spencer" + else (gle.LAMBDA_WIDE if wide else gle.LAMBDA_RANGE) + ) + out["lambda"] = lam if solved else None + out["theta_deg"] = float(np.degrees(np.arctan(lam))) if solved else None + r = float(res["residual"][0]) + out["residual"] = r if np.isfinite(r) else None + out["lambda_range"] = [float(rng[0]), float(rng[1])] + crossed = bool(res["converged"][0]) + out["status"] = "converged" if solved else ("rejected" if crossed else "no_solution") + out["reason"] = ( + None + if solved + else "해는 있으나 버림(m_α < 0.2 · 저항 없음 · F 하한)" + if crossed + else f"F_m · F_f 가 λ {rng[0]:.2f} ~ {rng[1]:.2f} 안에서 안 만남(해 없음)" + ) + out["lambda_curve"] = _curve(sl, center, method, f, wide or not solved) if method == "morgenstern_price": out["interslice_function"] = "half_sine" if method == "janbu": diff --git a/E03_StabSection/E03_StabSection_Engine_GLE.py b/E03_StabSection/E03_StabSection_Engine_GLE.py index 65f4ddf56..7cee25909 100644 --- a/E03_StabSection/E03_StabSection_Engine_GLE.py +++ b/E03_StabSection/E03_StabSection_Engine_GLE.py @@ -17,6 +17,8 @@ _TINY = 1e-9 CURVE_POINTS = 21 #: M-P λ 범위 — 반정현 평균 2/π 라 평균 X/E 가 Spencer 범위(LEM.LAMBDA_RANGE)와 같게 π/2 배 LAMBDA_RANGE = tuple(v * np.pi / 2 for v in lem.LAMBDA_RANGE) +#: 위 범위에 해가 없을 때만 다시 찾는 넓은 범위(같은 π/2 배 · 거친 훑기 → 뉴턴) — 그래도 없으면 「해 없음」 +LAMBDA_WIDE = tuple(v * np.pi / 2 for v in lem.LAMBDA_WIDE) def interslice_function(xe: np.ndarray, kind: str = "half_sine") -> np.ndarray: @@ -167,21 +169,39 @@ def _newton2(sl, center, f, F, lam, iters=40): return F, lam, it +def _solve(sl, center, f, grid, fm, ff, F0, rng): + """곡선 교점 → 뉴턴 → (F, λ, 반복, 수렴, 잔차) — 수렴 = 두 잔차 < 1e-4 · λ 범위 안 · 참고값 0.5~2 배.""" + F, lam = _crossing(grid, fm, ff) + F, lam, it = _newton2(sl, center, f, F, lam) + ok = np.isfinite(F) + safe = np.where(ok & (F > 0), F, 1.0) + rm, rff, _ = _fm_ff(sl, center, safe, np.where(ok, lam, 0.0), f) + res = np.where(ok, np.maximum(np.abs(rm - F), np.abs(rff - F)), np.nan) + conv = ok & (res < 1e-4) & (lam >= rng[0]) & (lam <= rng[1]) + if F0 is not None: + conv &= (F > 0.5 * F0) & (F < 2.0 * F0) + return F, lam, it, conv, res + + def morgenstern_price(sl: dict, center: np.ndarray, kind="half_sine", F0=None, curve=True): """M-P (F, λ) · 수렴 · (curve) F_m · F_f–λ 곡선. F0 = 출발 참고값(Spencer · Bishop) — 엉뚱한 근 거름.""" f = interslice_function(sl["xe"], kind) grid, fm, ff = lambda_curves(sl, center, f, F0=F0) - F, lam = _crossing(grid, fm, ff) - F, lam, it = _newton2(sl, center, f, F, lam) + F, lam, it, conv, res = _solve(sl, center, f, grid, fm, ff, F0, LAMBDA_RANGE) + wide = ~conv & sl["valid"] + if wide.any(): + # 좁은 범위에 해 없음 → 넓은 범위 거친 훑기 → 뉴턴(그 면만) + sub, cw, fw = lem._subset(sl, wide), center[wide], f[wide] + g2 = np.linspace(*LAMBDA_WIDE, lem.WIDE_POINTS) + F0w = None if F0 is None else np.asarray(F0, float)[wide] + _, fm2, ff2 = lambda_curves(sub, cw, fw, g2, F0w) + F2, l2, it2, c2, r2 = _solve(sub, cw, fw, g2, fm2, ff2, F0w, LAMBDA_WIDE) + F[wide], lam[wide], it[wide], conv[wide], res[wide] = F2, l2, it[wide] + it2, c2, r2 ok = np.isfinite(F) safe = np.where(ok & (F > 0), F, 1.0) rm, rff, den = _fm_ff(sl, center, safe, np.where(ok, lam, 0.0), f) p = lem._pre(sl) N = forces(sl, safe, np.where(ok, lam, 0.0), f)[0] - conv = ok & (np.abs(rm - F) < 1e-4) & (np.abs(rff - F) < 1e-4) - conv &= (lam >= LAMBDA_RANGE[0]) & (lam <= LAMBDA_RANGE[1]) - if F0 is not None: - conv &= (F > 0.5 * F0) & (F < 2.0 * F0) malpha_ok = np.all(den >= lem.MALPHA_MIN, axis=1) R = p["cu"] + N * sl["tanphi"] good = sl["valid"] & conv & (F > lem.F_MIN) & malpha_ok & (R.sum(1) > 0) @@ -191,6 +211,8 @@ def morgenstern_price(sl: dict, center: np.ndarray, kind="half_sine", F0=None, c "iterations": it, "converged": conv, "malpha_ok": malpha_ok, + "residual": res, + "wide": wide, "function": kind, } if curve: diff --git a/E03_StabSection/E03_StabSection_Engine_LEM.py b/E03_StabSection/E03_StabSection_Engine_LEM.py index ec30ffc3b..33b74db07 100644 --- a/E03_StabSection/E03_StabSection_Engine_LEM.py +++ b/E03_StabSection/E03_StabSection_Engine_LEM.py @@ -22,6 +22,9 @@ MALPHA_MIN = 0.2 #: 윗 덩이가 아랫 덩이를 내리누르면 λ < 0. 아래 한도 −1.25 = SLOPE/W GLE λ 범위 · 위는 작은 + 만 허용 #: (아랫 덩이를 들어 올리는 해 = 비현실 근 — Yamagami & Ueta 대조에서 λ ≈ +1 근이 F 를 9 % 낮췄음). LAMBDA_RANGE = (-1.25, 0.25) +#: 위 범위에 해가 없을 때만 다시 찾는 넓은 범위(거친 훑기 → 뉴턴) — 그래도 없으면 「해 없음」 +LAMBDA_WIDE = (-2.0, 2.0) +WIDE_POINTS = 41 #: 이보다 작은 F = 1/F 항이 터진 엉뚱한 근(실제 비탈 F 는 이 위) — 버림. F_MIN = 0.05 TOL = 1e-6 @@ -467,20 +470,30 @@ def spencer(sl: dict, center: np.ndarray, F0=None, scan: bool = True) -> dict: 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) + F, lam, it, conv, res = _spencer_newton(sl, center, F, lam) conv &= near(F, start) - bad = ~conv & sl["valid"] - if scan and bad.any(): + wide = np.zeros(S, bool) + for grid, rng in ( + (None, LAMBDA_RANGE), + (np.linspace(*LAMBDA_WIDE, WIDE_POINTS), LAMBDA_WIDE), + ): + bad = ~conv & sl["valid"] + if not (scan and bad.any()): + break 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]) + Fs, ls = _spencer_scan(sub, center[bad], grid) + F2, l2, it2, c2, r2 = _spencer_newton(sub, center[bad], Fs, ls, rng=rng) + F[bad], lam[bad], it[bad], res[bad] = F2, l2, it[bad] + it2, r2 + conv[bad] = c2 & near(F2, start[bad]) + wide[bad] = rng is LAMBDA_WIDE out = _finish(sl, F, it, conv, lam) out["lambda"] = np.where(np.isfinite(out["F"]), lam, np.nan) + out["residual"] = res + out["wide"] = wide return out -def _spencer_newton(sl, center, F, lam, iters=25): +def _spencer_newton(sl, center, F, lam, iters=25, rng=LAMBDA_RANGE): """(F_m − F, F_f − F) = 0 뉴턴 — 걸음 크기 제한(F 절반 · λ 0.3) · 수렴 = 두 잔차 < 1e-4 · λ 범위 안. 끝난 면은 빼고(남은 면이 절반 아래면 묶음을 줄임) · F < F_MIN 으로 빠진 면은 버림(어차피 `_finish` 가 버림). @@ -520,8 +533,8 @@ def _spencer_newton(sl, center, F, lam, iters=25): rm = _fm(sl, center, F, lam, nd=nd)[0] - F rf = _ff(sl, F, lam, nd=nd)[0] - F 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 + conv &= (lam >= rng[0]) & (lam <= rng[1]) + return F, lam, it, conv, np.maximum(np.abs(rm), np.abs(rf)) def lambda_curve(sl, center, grid=None): @@ -538,9 +551,9 @@ def lambda_curve(sl, center, grid=None): return grid, fm_all, ff_all -def _spencer_scan(sl, center): +def _spencer_scan(sl, center, grid=None): """λ 를 훑어 F_m(λ) − F_f(λ) 부호가 바뀌는 곳(λ=0 가까운 쪽)을 출발점으로.""" - grid, fm_all, ff_all = lambda_curve(sl, center) + grid, fm_all, ff_all = lambda_curve(sl, center, grid) S = len(sl["W"]) diff = fm_all - ff_all best_F, best_l = np.full(S, 1.5), np.zeros(S) diff --git a/E03_StabSection/E03_StabSection_Engine_Search.py b/E03_StabSection/E03_StabSection_Engine_Search.py index 014af338c..e47aa1e9d 100644 --- a/E03_StabSection/E03_StabSection_Engine_Search.py +++ b/E03_StabSection/E03_StabSection_Engine_Search.py @@ -487,6 +487,17 @@ def analyze( if g is not None: best_circles[m] = g results[m]["circular"] = _describe(prep, "circle", g, m, n_slices, min_depth) + # 엄밀해(Spencer · M-P)가 Bishop 임계 원에서 해 없음 · 찾은 최소가 Bishop 보다 높음 = 임계 둘레에서 + # 풀리는 원이 없어 먼 원을 고른 것 → 그 방법 원호 = 「해 없음」(Bishop 임계 원 · F None · 까닭 · F–λ 곡선) + ref = best_circles.get("bishop") if "bishop" in results else None + fb = (results.get("bishop", {}).get("circular") or {}).get("F") + for m in [k for k in ("spencer", "morgenstern_price") if k in methods and ref and fb]: + cur = results[m].get("circular") + if cur and cur["F"] is not None and cur["F"] <= fb: + continue + at = _describe(prep, "circle", ref, m, n_slices, min_depth) + if at["F"] is None: + results[m]["circular"] = at if noncircular: mv = polyline_vertices seeds = [] diff --git a/E03_StabSection/E03_StabSection_UI_Result.ts b/E03_StabSection/E03_StabSection_UI_Result.ts index d8b7fb5d0..70e35e2a4 100644 --- a/E03_StabSection/E03_StabSection_UI_Result.ts +++ b/E03_StabSection/E03_StabSection_UI_Result.ts @@ -69,7 +69,11 @@ function methodTable(analysis: Analysis): HTMLElement { const body = el("tr"); for (const m of METHODS) { head.append(el("th", { className: "is-num", text: t(`E03_Method_${m}` as LocaleKey) })); - body.append(el("td", { className: "is-num", text: fmt(analysis.min_by_method[m], 3) })); + const v = analysis.min_by_method[m]; + const unsolved = (v === null || v === undefined) && analysis.no_solution?.includes(m); + body.append( + el("td", { className: "is-num", text: unsolved ? t("E03_NoSolution") : fmt(v, 3) }), + ); } table.append(el("thead", { children: [head] }), el("tbody", { children: [body] })); return table; diff --git a/E03_StabSection/E03_StabSection_View.py b/E03_StabSection/E03_StabSection_View.py index 6e94d152e..5cf3e5296 100644 --- a/E03_StabSection/E03_StabSection_View.py +++ b/E03_StabSection/E03_StabSection_View.py @@ -251,6 +251,15 @@ def section_view( "required": side_row.get("required"), "ok": side_row.get("ok"), "min_by_method": {m: _r(v) for m, v in (case.get("min_F") or {}).items()}, + # 엄밀해(Spencer · M-P) 「해 없음」 면이 있는 방법 — 화면이 빈 값을 「수렴 안 됨」으로 + "no_solution": sorted( + m + for m, found in methods.items() + if any( + (found.get(k) or {}).get("status") == "no_solution" + for k in ("circular", "noncircular") + ) + ), "critical": _surface(best, PRIMARY), "candidates": candidates, "slices": _slices(best.get("slices") or []) if best else [], diff --git a/E03_StabSection/logic/E03_StabSection_Logic.ts b/E03_StabSection/logic/E03_StabSection_Logic.ts index bbab80681..c869cb37a 100644 --- a/E03_StabSection/logic/E03_StabSection_Logic.ts +++ b/E03_StabSection/logic/E03_StabSection_Logic.ts @@ -83,6 +83,8 @@ export interface Analysis { required: number | null; ok: boolean | null; min_by_method: Record; + /** 「해 없음」 면이 있는 방법(Spencer · M-P) — 옛 저장본은 없음 */ + no_solution?: string[]; critical: Surface | null; candidates: Surface[]; slices: SliceRow[]; diff --git a/resources/tester/test_532_2_stab_nosolution.py b/resources/tester/test_532_2_stab_nosolution.py new file mode 100644 index 000000000..cc9502b28 --- /dev/null +++ b/resources/tester/test_532_2_stab_nosolution.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +"""532-2 덧 3 엄밀해(Spencer · M-P) 해 없음 — 넓은 λ 다시 찾기 · 「수렴 안 됨」 · 수렴 정보 칸. + +- 해 없는 견본 = 검증_sub4 STA.0+340 우기 깎기 비탈을 줄인 꼴(풍화토 위 · 풍화암 · 지표 수위 · 노면 하중): + Bishop 임계 원에서 F_m · F_f 가 λ −2 ~ 2(M-P ±π)에서 안 만남 → 옛 엔진은 먼 원(F 1.82 · Bishop 1.34 의 + 136 %)을 Spencer 값으로 냈음 → 이제 Spencer · M-P 원호 = F None · status no_solution · 까닭 · 넓은 곡선. +- 해 있는 면 = 상태 converged · 잔차 < 1e-4 · λ 좁은 범위 안 · 곡선 21 점(옛 꼴 그대로). + + ./venv/Scripts/python.exe -m pytest resources/tester/test_532_2_stab_nosolution.py -q +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from test_532_2_stab_lem_benchmarks import ACADS_EXT, mats # noqa: E402 + +from E03_StabSection import E03_StabSection_Engine_GLE as gle # noqa: E402 +from E03_StabSection import E03_StabSection_Engine_LEM as lem # noqa: E402 +from E03_StabSection.E03_StabSection_Engine_Search import ( # noqa: E402 + analyze, + evaluate_specified, +) + +MP = "morgenstern_price" +SURFACE = [ + [-25.0, 876.6], [-17.0, 871.7], [-13.0, 867.7], [-11.5, 867.1], + [-3.9, 859.7], [-2.5, 856.6], [2.0, 857.0], [10.0, 850.3], +] # fmt: skip +NO_SOLUTION_MODEL = { + "materials": [ + {"id": "ws", "gamma_t": 18.0, "gamma_sat": 19.0, "c": 15.0, "phi": 30.0}, + {"id": "wr", "gamma_t": 20.0, "gamma_sat": 21.0, "c": 30.0, "phi": 33.0}, + ], + "regions": [ + {"material_id": "ws", "polygon": [ + [-24.5, 876.5], [-16.5, 871.4], [-13.0, 867.7], [-11.5, 867.1], [-8.0, 863.0], + [-5.5, 861.4], [-3.5, 858.9], [-8.0, 862.5], [-11.5, 866.6], [-14.5, 868.5], + [-16.5, 870.9], [-25.0, 876.1], + ]}, + {"material_id": "ws", "polygon": [ + [-0.2, 856.9], [3.5, 855.3], [10.0, 849.2], [3.5, 854.8], [1.0, 855.4], [-0.8, 856.9], + ]}, + {"material_id": "wr", "polygon": [ + [-3.0, 857.6], [-2.5, 856.6], [-0.8, 856.9], [1.0, 855.4], [3.5, 854.8], + [10.0, 849.2], [10.0, 801.5], [-25.0, 801.5], [-25.0, 876.1], [-16.5, 870.9], + [-5.0, 860.5], + ]}, + ], + "surface": SURFACE, + "groundwater": {"mode": "line", "line": SURFACE}, + "surcharge": [{"x0": -2.0162, "x1": 2.0, "q": 10.0}], + "bottom_z": 801.5, +} # fmt: skip + + +@pytest.fixture(scope="module") +def unsolved(): + return analyze( + NO_SOLUTION_MODEL, direction=1, limits=[-9.561, 2.0], min_depth=0.3, noncircular=False + ) + + +def test_no_solution_reported_not_a_far_circle(unsolved): + ms = unsolved["methods"] + bishop = ms["bishop"]["circular"] + assert 1.30 < bishop["F"] < 1.40 + for key, rng in (("spencer", lem.LAMBDA_WIDE), (MP, gle.LAMBDA_WIDE)): + c = ms[key]["circular"] + assert c["F"] is None and c["status"] == "no_solution" and c["converged"] is False + assert c["lambda"] is None and "해 없음" in c["reason"] + assert c["lambda_range"] == pytest.approx(list(rng)) + # 「해 없음」 면 = Bishop 임계 원 · 최소 F 후보에서 빠짐 + assert c["radius"] == pytest.approx(bishop["radius"]) and c["center"] == bishop["center"] + assert ms[key]["min"] is None + assert isinstance(c["iterations"], int) + assert c["residual"] is None or c["residual"] > 1e-4 + # F_m · F_f – λ 곡선은 넓은 범위 그대로(화면 그래프로 해 없음이 보이게) + curve = c["lambda_curve"] + assert len(curve["lambda"]) == lem.WIDE_POINTS + assert curve["lambda"][0] == pytest.approx(rng[0]) and curve["lambda"][-1] == pytest.approx( + rng[1] + ) + assert sum(v is not None for v in curve["Fm"]) > 10 + # 판정 쪽(Bishop · Fellenius · Janbu)은 그대로 + assert all(ms[k]["min"]["F"] is not None for k in ("ordinary", "bishop", "janbu")) + + +def test_spencer_curve_has_no_crossing_in_wide_range(unsolved): + curve = unsolved["methods"]["spencer"]["circular"]["lambda_curve"] + lam = np.array(curve["lambda"], float) + d = np.array(curve["Fm"], float) - np.array(curve["Ff"], float) + near0 = np.isfinite(d) & (lam <= 0.25) + assert near0.sum() > 20 and np.all(d[near0] < 0) + + +def test_solved_surface_status_and_shape_unchanged(): + m = { + "materials": mats(("s", 20, 3, 19.6)), + "regions": [{"material_id": "s", "polygon": ACADS_EXT}], + } + r = evaluate_specified( + m, circle=[29.104, 55.527, 30.527], direction=-1, methods=("spencer", MP) + ) + for key, rng in (("spencer", lem.LAMBDA_RANGE), (MP, gle.LAMBDA_RANGE)): + out = r[key] + assert out["status"] == "converged" and out["reason"] is None and out["converged"] is True + assert out["residual"] < 1e-4 and rng[0] <= out["lambda"] <= rng[1] + assert out["lambda_range"] == pytest.approx(list(rng)) + assert len(out["lambda_curve"]["lambda"]) == gle.CURVE_POINTS + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-q"])) diff --git a/ui_template/ui_template_locale_e03.ts b/ui_template/ui_template_locale_e03.ts index 3fa49b93b..3f5552b86 100644 --- a/ui_template/ui_template_locale_e03.ts +++ b/ui_template/ui_template_locale_e03.ts @@ -98,6 +98,7 @@ export const ui_locales_e03 = { E03_Converged: ["수렴", "Converged"], E03_Yes: ["예", "Yes"], E03_No: ["아니오", "No"], + E03_NoSolution: ["수렴 안 됨", "Not converged"], E03_Iter: ["회", " iter."], E03_Height: ["비탈 높이", "Slope height"], E03_Center: ["원호 중심", "Center"],