- E03 export_report · report_row · report_representatives · REPORT_REASONS 걷음 — E05 는 E03 정본(stab2d_summary · stab2d_station)만 읽음 · representative.json = overrides 만 - GET …/stab/result2d 측점 줄에 station_key · side · fs_min · fs_by_condition · method · verdict_overall 덧붙임(E04 2D 최소) — 지배 = 조건 × 비탈 중 fs / 기준 가장 낮은 것 · 조건별 verdict 칸은 E03 화면 몫이라 그대로 - 시험 test_532_5 — 옛 사본 꼴 시험을 E05 = E03 정본(측점 수 · N.G 수 · 지배 비탈 같음)으로 바꿈 · 지배 비탈 규칙(깎기 1.22/1.2 · 쌓기 1.25/1.3 → 쌓기 N.G · E05 _row2d 같음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
294 lines
12 KiB
Python
294 lines
12 KiB
Python
"""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,
|
||
}
|