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