Files
Aislo/E03_StabSection/E03_StabSection_View.py
T
eomsangdonandClaude Opus 5.5 164024ab43 feat(532-5): E03 2D 해석에 벽(옹벽 · 석축) 연결 · 횡단 없는 측점 띠(E02 · E03)
- analyze2d 가 측점마다 벽(walls_by_station · 복사 지문으로 기억) · 그려지는 설계선을 넘김 · 벽 있는 측점만 지문에 벽 · 벽 없는 측점 = 종전 지문
- 측점 보기 = 같은 벽 모형 · 수위선 = E02 고른 지하수 갈래(station_groundwater)
- 화면 — 「반영 전」 안내 걷음 · 벽 회색 빗금 · 결과 「벽 있음」 · 목록 벽 표시
- result2d · stab/ground 에 missing_stations 칸 덧붙임 · 화면 위 띠(펼침 목록)
- E03 지반 조건 = E02 화면과 같은 upgrade(옛 정본 지하수 칸 기본값)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:19:59 +09:00

264 lines
10 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 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()},
"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,
}