Files
Aislo/E03_StabSection/E03_StabSection_View.py
T
eomsangdonandClaude Opus 5.5 a843aa3ad3 feat(567-1): E03 2D 비원호 출발 셋 최솟값 · 판정 = 원호 Bishop · 비원호 Spencer · M-P 중 작은 값 · 면 확인 · 모형 끝 면 · 탐색 한계 모형 끝
- 비원호 꼭짓점 최적화를 난수 씨앗 셋으로 돌려 찾은 면 모두에서 방법별 최소(NONCIRC_STARTS 3)
- 판정 면 = 원호 Bishop · 비원호 Spencer · M-P 중 작은 값 · 비원호 < 원호 Bishop × 0.85 = surface_check · 판정 면 진입 · 진출이 모형 끝 1 m 안 = edge
- 탐색 한계 바깥 끝 = 모형 끝(요구 392 · Slide2 기본) · 설정 search_reach(model_end · 2h · m)
- 새 지킴 시험 test_567_1(원호 기본 대 촘촘 ≤ 0.5 % · 비원호 ≤ 3 % · 다출발 최소 · 한계 · 판정 칸)
- 낡은 시험 고침: ACADS 3a Spencer · ACADS 4 · M-P ACADS 4 비원호 탐색 허용 ±5 % — 다출발이 원문보다 낮은 면(약층 바닥 따라감 · 3a Spencer 1.217)을 찾음

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

354 lines
15 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 · 비원호 Spencer · M-P 중 작은 값 ≥ 기준안전율(E02 `safety_factors[조건][깎기 · 쌓기]` ·
지식DB 06 해석법 조합) · 비원호가 원호 Bishop 보다 15 % 넘게 낮으면 「면 확인」(SLOPE/W 최적화 기준) ·
판정 면 진입 · 진출이 모형 끝 1 m 안이면 「모형 끝 면」(edge · 값이 모형 폭에 따라 바뀜).
대표 단면(보고서 몫) = 자동(쌓기 높이 ≥ 10 m · 깎기 ≥ 15 m · 최소 안전율 하위 5) + 사용자 켬/끔.
좌표 = 엔진 그대로(x = B06 offset · 좌 + · z = 표고) — 화면이 같은 좌표로 그림.
"""
from __future__ import annotations
import math
from typing import Any
PRIMARY = "bishop"
#: 판정 후보(방법 · 면 꼴)
GOVERNING = (
("bishop", "circular"),
("spencer", "noncircular"),
("morgenstern_price", "noncircular"),
)
#: 비원호 F < 원호 Bishop F × 이 값 = 「면 확인」
SURFACE_CHECK_RATIO = 0.85
#: 판정 면 진입 · 진출이 모형 끝에서 이 거리(m) 안 = 「모형 끝 면」
EDGE_M = 1.0
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]) -> tuple[str, dict[str, Any] | None, float | None, bool]:
"""한 비탈 · 조건의 판정 면 — (방법, 면, 원호 Bishop F, 면 확인).
후보가 없으면(옛 결과 · 원호만 셈) Bishop 최소."""
methods = (case.get("result") or {}).get("methods") or {}
circ = ((methods.get(PRIMARY) or {}).get("circular") or {}).get("F")
cands = [
(m, found)
for m, kind in GOVERNING
if (found := (methods.get(m) or {}).get(kind)) and found.get("F") is not None
]
if not cands:
return PRIMARY, (methods.get(PRIMARY) or {}).get("min"), circ, False
method, best = min(cands, key=lambda c: c[1]["F"])
check = method != PRIMARY and circ is not None and best["F"] < SURFACE_CHECK_RATIO * circ
return method, best, circ, check
def _edge(case: dict[str, Any], best: dict[str, Any] | None) -> bool | None:
"""판정 면이 모형 끝에 닿음 — 옛 결과(모형 끝 모름) · 면 없음 = None."""
ends = (case.get("result") or {}).get("model_x")
if not ends or not best or not best.get("entry") or not best.get("exit"):
return None
xs = (best["entry"][0], best["exit"][0])
return bool(min(xs) <= ends[0] + EDGE_M or max(xs) >= ends[1] - EDGE_M)
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"))})
method, best, circ, check = _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,
"method": method if best else None,
"fs_circular": _r(circ),
"surface_check": check,
"edge": _edge(case, best),
}
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,
"surface_check": any(
v["surface_check"] for sides in cases.values() for v in sides.values()
),
"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"], c.get("method") or PRIMARY)
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": worst[3] if worst else 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"),
"surface_check": sum(
1
for r in rows
if any(v.get("surface_check") for v in (r["cases"].get(cond) or {}).values())
),
"edge": sum(
1 for r in rows if any(v.get("edge") for v in (r["cases"].get(cond) or {}).values())
),
}
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 {}
method, best, _circ, _check = _governing(case)
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"),
"surface_check": side_row.get("surface_check"),
"edge": side_row.get("edge"),
"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, method),
"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,
}