Files
Aislo/E03_StabSection/E03_StabSection_View.py
T
eomsangdonandClaude Opus 5.5 f3cc133297 fix(E03): Spencer · M-P 해 없음 = 「수렴 안 됨」 — 넓은 λ 다시 찾기 · 수렴 정보 칸 (532-2 덧 3 · 요구 351)
- 좁은 λ 에 해 없으면 넓은 λ(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 <noreply@anthropic.com>
2026-10-26 04:00:59 +09:00

303 lines
12 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 해석 결과 → 화면 꼴 (요구 345 · 347 · PLAN 532-3) — storage 를 모르는 순수 함수.
엔진 출력 = `E03_StabSection_Engine_Station.analyze_station` 그대로(cases × methods).
판정 = 대표 방법(Bishop) 최소 안전율 ≥ 기준안전율(E02 `safety_factors[조건][깎기 · 쌓기]`).
대표 단면(보고서 몫) = 자동(쌓기 높이 ≥ 10 m · 깎기 ≥ 15 m · 최소 안전율 하위 5) + 사용자 켬/끔.
좌표 = 엔진 그대로(x = B06 offset · 좌 + · z = 표고) — 화면이 같은 좌표로 그림.
"""
from __future__ import annotations
import math
from typing import Any
PRIMARY = "bishop"
CONDITIONS = ("dry", "wet", "seismic")
REPRESENTATIVE_FILL_M = 10.0
REPRESENTATIVE_CUT_M = 15.0
REPRESENTATIVE_LOWEST = 5
#: 화면에 넘기는 면 꼭짓점 · 절편 자릿수
_ROUND = 3
def _r(value: Any, digits: int = _ROUND) -> Any:
if isinstance(value, float):
return round(value, digits) if math.isfinite(value) else None
if isinstance(value, list):
return [_r(v, digits) for v in value]
return value
def required_factor(factors: dict[str, Any], condition: str, role: str) -> float | None:
"""기준안전율 — 조건 · 비탈 역할(cut · fill). 없으면 None."""
value = (factors.get(condition) or {}).get(role)
return float(value) if isinstance(value, int | float) else None
def _governing(case: dict[str, Any]) -> dict[str, Any] | None:
"""한 비탈 · 조건의 대표 방법 최소 면(원호 · 비원호 중 작은 쪽)."""
return ((case.get("result") or {}).get("methods") or {}).get(PRIMARY, {}).get("min")
def station_row(
meta: dict[str, Any],
out: dict[str, Any] | None,
factors: dict[str, Any],
error: str | None = None,
) -> dict[str, Any]:
"""측점 목록 한 줄 — 조건 × 비탈 안전율 · 기준 · 판정 · 비탈 높이."""
cases: dict[str, dict[str, Any]] = {}
sides: dict[str, dict[str, Any]] = {}
for case in (out or {}).get("cases", []):
cond, side, role = case["condition"], case["side"], case["role"]
sides.setdefault(side, {"side": side, "role": role, "height_m": _r(case.get("height_m"))})
best = _governing(case)
fs = best.get("F") if best else None
need = required_factor(factors, cond, role)
cases.setdefault(cond, {})[side] = {
"fs": _r(fs),
"required": need,
"ok": None if fs is None or need is None else bool(fs >= need),
"kind": best.get("kind") if best else None,
"converged": best.get("converged") if best else None,
}
minimum: dict[str, float | None] = {}
verdict: dict[str, str] = {}
for cond, by_side in cases.items():
values = [v["fs"] for v in by_side.values() if v["fs"] is not None]
minimum[cond] = min(values) if values else None
oks = [v["ok"] for v in by_side.values() if v["ok"] is not None]
verdict[cond] = "none" if not oks else ("ok" if all(oks) else "ng")
return {
"key": meta["key"],
"chainage_m": meta["chainage_m"],
"label": meta.get("label"),
"section_mode": meta.get("section_mode"),
"ground_type": meta.get("ground_type"),
"center": meta.get("center"),
"fp": meta.get("fp"),
"walls": meta.get("walls") or 0,
"sides": list(sides.values()),
"cases": cases,
"min": minimum,
"verdict": verdict,
"error": error,
"elapsed_s": (out or {}).get("elapsed_s"),
}
def overall_verdict(row: dict[str, Any], condition: str | None = None) -> str:
"""측점 판정 — 조건 하나 또는 전 조건 중 가장 나쁜 것(ng > ok > none)."""
verdict = row.get("verdict") or {}
values = [verdict.get(condition, "none")] if condition else list(verdict.values())
if "ng" in values:
return "ng"
return "ok" if "ok" in values else "none"
def auto_representative(rows: list[dict[str, Any]]) -> dict[str, list[str]]:
"""자동 대표 단면 — 열쇠 → 까닭(fill_height · cut_height · lowest)."""
reasons: dict[str, list[str]] = {}
for row in rows:
for side in row["sides"]:
height = side.get("height_m") or 0.0
if side["role"] == "fill" and height >= REPRESENTATIVE_FILL_M:
reasons.setdefault(row["key"], []).append("fill_height")
if side["role"] == "cut" and height >= REPRESENTATIVE_CUT_M:
reasons.setdefault(row["key"], []).append("cut_height")
ranked = sorted(
(
(min(v for v in row["min"].values() if v is not None), row["key"])
for row in rows
if any(v is not None for v in row["min"].values())
),
)
for _, key in ranked[:REPRESENTATIVE_LOWEST]:
reasons.setdefault(key, []).append("lowest")
return {key: sorted(set(v)) for key, v in reasons.items()}
def mark_representative(rows: list[dict[str, Any]], overrides: dict[str, bool]) -> None:
"""행마다 대표 단면 칸 — auto(까닭) · user(켬/끔 · 없으면 None) · on(최종)."""
auto = auto_representative(rows)
for row in rows:
user = overrides.get(row["key"])
reasons = auto.get(row["key"], [])
row["representative"] = {
"auto": reasons,
"user": user,
"on": bool(reasons) if user is None else bool(user),
}
def governing_row(row: dict[str, Any]) -> dict[str, Any]:
"""측점 줄 덧칸(E04 · E05 같은 규칙) — 지배 = 조건 × 비탈 중 기준 대비 가장 낮은 것(fs / 기준) ·
fs_min = 그 fs · 조건별 안전율 = 그 비탈 · 판정 = 조건 하나라도 N.G 면 ng."""
roles = {s["side"]: s["role"] for s in row.get("sides") or []}
cases = row.get("cases") or {}
worst = min(
(
(c["fs"] / c["required"], side, c["fs"])
for sides in cases.values()
for side, c in sides.items()
if c.get("fs") is not None and c.get("required")
),
default=None,
)
side = worst[1] if worst else None
verdicts = set((row.get("verdict") or {}).values())
values = [v for v in (row.get("min") or {}).values() if v is not None] # 기준 없을 때
return {
"station_key": row["key"],
"chainage_m": row["chainage_m"],
"side": roles.get(side),
"fs_min": worst[2] if worst else min(values, default=None),
"fs_by_condition": {
cond: sides[side]["fs"] for cond, sides in cases.items() if side in sides
},
"method": PRIMARY,
"verdict_overall": "ng" if "ng" in verdicts else "ok" if verdicts == {"ok"} else None,
}
def summarize(rows: list[dict[str, Any]], conditions: list[str]) -> dict[str, Any]:
"""머리 요약 — 조건별 최소 안전율 · N.G 측점 수."""
out: dict[str, Any] = {"stations": len(rows), "errors": sum(1 for r in rows if r["error"])}
for cond in conditions:
values = [r["min"].get(cond) for r in rows if r["min"].get(cond) is not None]
out[cond] = {
"min_fs": min(values) if values else None,
"ng": sum(1 for r in rows if r["verdict"].get(cond) == "ng"),
}
return out
def _slices(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""절편표 + 자유물체도 힘 — N(전 수직력) = N' + u·l · U = u·l · 오른쪽 = 다음 절편의 왼쪽."""
out = []
for i, s in enumerate(rows):
nxt = rows[i + 1] if i + 1 < len(rows) else None
U = s["pore_pressure"] * s["base_length"]
out.append(
{
"i": i + 1,
**{k: _r(s.get(k)) for k in ("x_left", "x_right", "width", "height", "alpha_deg")},
**{k: _r(s.get(k)) for k in ("weight", "surcharge", "pore_pressure")},
**{k: _r(s.get(k)) for k in ("base_length", "c", "phi_deg")},
"material": s.get("material"),
"N": _r(s["normal_eff"] + U),
"N_eff": _r(s["normal_eff"]),
"U": _r(U),
"S": _r(s["shear_mobilized"]),
"E_left": _r(s["E_left"]),
"E_right": _r(nxt["E_left"]) if nxt else 0.0,
"X_left": _r(s["X_left"]),
"X_right": _r(nxt["X_left"]) if nxt else 0.0,
}
)
return out
def _surface(found: dict[str, Any] | None, method: str) -> dict[str, Any] | None:
if not found:
return None
out = {
"method": method,
"kind": found.get("kind"),
"fs": _r(found.get("F")),
"points": _r(found.get("points") or []),
"entry": _r(found.get("entry")),
"exit": _r(found.get("exit")),
"converged": found.get("converged"),
"iterations": found.get("iterations"),
}
if found.get("center") is not None:
out["center"] = _r(found["center"])
out["radius"] = _r(found.get("radius"))
for key in ("lambda", "theta_deg", "F0", "f0"):
if key in found:
out[key] = _r(found[key])
return out
def section_view(
row: dict[str, Any],
out: dict[str, Any],
model: dict[str, Any],
water: dict[str, list[list[float]] | None],
) -> dict[str, Any]:
"""측점 하나 화면 — 지층 · 지표 · 지하수선 · 하중 · 비탈 × 조건 임계면 · 후보 · 절편 · 격자."""
analyses = []
for case in out.get("cases", []):
result = case.get("result") or {}
methods = result.get("methods") or {}
best = (methods.get(PRIMARY) or {}).get("min")
candidates = []
for method, found in methods.items():
for kind in ("circular", "noncircular"):
surface = _surface(found.get(kind), method)
if surface and surface["fs"] is not None:
candidates.append(surface)
side_row = (row["cases"].get(case["condition"]) or {}).get(case["side"]) or {}
contour = result.get("contour")
analyses.append(
{
"condition": case["condition"],
"side": case["side"],
"role": case["role"],
"height_m": _r(case.get("height_m")),
"limits": _r(list(case.get("limits") or [])),
"direction": case.get("direction"),
"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 [],
"grid": (
{
"method": contour.get("method"),
"x": _r(contour["x"]),
"z": _r(contour["z"]),
"fs": _r(contour["F"]),
}
if contour
else None
),
"counts": result.get("counts"),
}
)
return {
"key": row["key"],
"chainage_m": row["chainage_m"],
"label": row.get("label"),
"materials": [
{k: m.get(k) for k in ("id", "name", "gamma_t", "gamma_sat", "c", "phi")}
for m in model["materials"]
],
"regions": [
{
"material_id": r["material_id"],
"polygon": _r(r["polygon"]),
"holes": _r(r.get("holes") or []),
}
for r in model["regions"]
],
"surface": _r(model["surface"]),
"bottom_z": _r(model.get("bottom_z")),
"surcharge": model.get("surcharge") or [],
"water": {cond: _r(line) if line else None for cond, line in water.items()},
"areas": {k: _r(v) for k, v in (out.get("areas") or {}).items()},
"analyses": analyses,
"row": row,
}