feat(567-1): E05 보고서 판정 면 — 측점표 판정 방법 · 면 확인 · 모형 끝 면 칸 · 대표단면 조건별 판정 면 · 탐색 한계 한 줄

- 측점표(s4) 오른쪽에 판정 방법 · 면 확인 · 모형 끝 면 칸 셋 + 풀이 한 줄(탐색 한계 포함)
- 대표단면(s5) 조건별 방법 · 임계면 그림 · 부록 절편 = E03 판정 면(원호 Bishop · 비원호 Spencer · M-P 중 작은 값)
- 입력조건 해석 방법 글 · 탐색 한계 한 줄 갱신
- 시험 534_3 판정 면 경우 덧

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 22:13:49 +09:00
co-authored by Claude Sonnet 5.5
parent 0fca2b5af1
commit cc4330d946
4 changed files with 179 additions and 25 deletions
+75 -8
View File
@@ -33,7 +33,12 @@ from common_util.common_util_derived_store import read_derived
from E01_StabRegister import E01_StabRegister_Source as source
from E02_StabGround import E02_StabGround_Model as ground_model
from E03_StabSection import E03_StabSection_View as view2d
from E03_StabSection.E03_StabSection_Router import KIND_STATION, _latest_summary, _overrides
from E03_StabSection.E03_StabSection_Router import (
KIND_STATION,
_latest_summary,
_overrides,
search_reach_of,
)
from E04_Stab3D import E04_Stab3D_Run as run3d
from E05_StabReport import E05_StabReport_Area as area
from E05_StabReport import E05_StabReport_Figure as figure
@@ -55,8 +60,11 @@ from E05_StabReport.E05_StabReport_Cells import ( # noqa: F401 — 시험 ·
_seismic,
_set,
_verdict,
condition_list,
fig_key,
mark,
method_text,
reach_text,
)
ROOT = Path(__file__).resolve().parents[1]
@@ -110,14 +118,20 @@ def _station_result(project_root: Path, fp: str) -> tuple[dict[str, Any] | None,
return value.get("out"), (value.get("inputs") or {}).get("ground")
def _any(sides: dict[str, Any], flag: str) -> bool:
"""조건 하나 — 좌 · 우 비탈 중 하나라도 켜짐(E03 줄 · 요약과 같은 규칙)."""
return any(v.get(flag) for v in sides.values())
def _row2d(row: dict[str, Any], method: str) -> dict[str, Any]:
"""E03 요약 줄 → 보고서 줄 — 비탈 = 기준 대비 가장 낮은 쪽(fs / 기준) · 판정 = 조건 하나라도
N.G 면 ng."""
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["required"], side, c.get("method") or method)
for sides in cases.values()
for side, c in sides.items()
if c.get("fs") is not None and c.get("required")
@@ -125,6 +139,7 @@ def _row2d(row: dict[str, Any], method: str) -> dict[str, Any]:
default=None,
)
side = worst[1] if worst else None
on_side = {cond: sides[side] for cond, sides in cases.items() if side in sides}
verdicts = set((row.get("verdict") or {}).values())
values = [v for v in (row.get("min") or {}).values() if v is not None]
return {
@@ -137,7 +152,13 @@ def _row2d(row: dict[str, Any], method: str) -> dict[str, Any]:
},
"fs_min": min(values) if values else None,
"min_by_condition": row.get("min") or {},
"method": method,
"side_key": side,
"method": worst[2] if worst else method,
"method_by_condition": {c: v.get("method") or method for c, v in on_side.items()},
"surface_check_by_condition": {
c: _any(sides, "surface_check") for c, sides in cases.items()
},
"edge_by_condition": {c: _any(sides, "edge") for c, sides in cases.items()},
"verdict": "ng" if "ng" in verdicts else "ok" if verdicts == {"ok"} else None,
"representative": row.get("representative") or {},
}
@@ -151,7 +172,8 @@ def _result2d(project_root: Path) -> dict[str, Any] | None:
rows = [dict(r) for r in summary.get("stations") or []]
view2d.mark_representative(rows, _overrides(project_root))
method = summary.get("method") or view2d.PRIMARY
return {**summary, "stations": [_row2d(r, method) for r in rows]}
reach = summary.get("search_reach") or search_reach_of(project_root)
return {**summary, "search_reach": reach, "stations": [_row2d(r, method) for r in rows]}
def analysis_fingerprint(project_root: Path) -> str:
@@ -276,8 +298,17 @@ def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
ground = inputs["ground"]
base = ground_model.load_defaults()
base_sf = base["safety_factors"]
_set(sheet, "B5", "한계평형해석 — Bishop 간편법(기본) · Janbu 보정 · Fellenius 비교")
_set(sheet, "B6", "원호(격자 · 진입-탈출) + 비원호(꺾은선) · 측점마다 좌 · 우 비탈 따로")
_set(
sheet,
"B5",
"한계평형해석 — 판정 = 원호 Bishop · 비원호 Spencer · Morgenstern-Price 중 작은 값",
)
_set(
sheet,
"B6",
"원호(격자 · 진입-탈출) + 비원호(꺾은선) · 측점마다 좌 · 우 비탈 따로 · "
f"탐색 한계 = {reach_text(inputs['result2d'].get('search_reach'))}",
)
_set(
sheet,
"B7",
@@ -347,6 +378,38 @@ def fill_conditions(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
)
JUDGE_COLS = (("L", "판정 방법", 26), ("M", "면 확인", 12), ("N", "모형 끝 면", 12))
def _judge_columns(sheet: dict[str, Any], result2d: dict[str, Any]) -> None:
"""s4 오른쪽에 판정 면 칸 셋(방법 · 면 확인 · 모형 끝 면) + 3 행 풀이 한 줄 — 양식은 K 까지."""
sheet["병합"] = [m for m in sheet["병합"] if m not in ("A1:K1", "A2:K2")] + [
"A1:N1",
"A2:N2",
"A3:N3",
]
for col in "KLMN":
_set(sheet, f"{col}1", None, 1)
_set(sheet, f"{col}2", None, 2)
_set(
sheet,
"A3",
"판정 면 = 원호 Bishop · 비원호 Spencer · Morgenstern-Price 중 작은 값 · "
f"탐색 한계 = {reach_text(result2d.get('search_reach'))} · "
f"면 확인 = 좌 · 우 비탈 중 비원호가 원호 Bishop 보다 {round((1 - view2d.SURFACE_CHECK_RATIO) * 100)} % "
"넘게 낮음 · "
f"모형 끝 면 = 좌 · 우 비탈 중 판정 면이 모형 끝 {view2d.EDGE_M:g} m 안",
2,
)
sheet["행"]["3"] = {"높이": 32}
for col, title, width in JUDGE_COLS:
_set(sheet, f"{col}4", title, 6)
_set(sheet, f"{col}5", None, 6)
_set(sheet, f"{col}6", None, 7)
sheet["병합"].append(f"{col}4:{col}5")
sheet["열"][col] = {"폭": width}
def fill_stations(sheet: dict[str, Any], inputs: dict[str, Any]) -> int:
"""s4 측점표 — 측점 하나 = 한 줄(6 행부터) · N.G 수."""
ground = inputs["ground"]
@@ -354,9 +417,10 @@ def fill_stations(sheet: dict[str, Any], inputs: dict[str, Any]) -> int:
labels = _labels(inputs["stations"])
rows = sorted(inputs["result2d"]["stations"], key=lambda s: float(s["chainage_m"]))
ng = 0
_judge_columns(sheet, inputs["result2d"])
for i, st in enumerate(rows):
r = 6 + i
_copy_row(sheet, 6, r, "ABCDEFGHIJK")
_copy_row(sheet, 6, r, "ABCDEFGHIJKLMN")
side = st.get("side")
fs = st.get("fs_by_condition") or {}
_set(sheet, f"A{r}", _label_at(labels, float(st["chainage_m"])))
@@ -372,6 +436,9 @@ def fill_stations(sheet: dict[str, Any], inputs: dict[str, Any]) -> int:
ng += ok is False
_verdict(sheet, f"J{r}", ok)
_set(sheet, f"K{r}", None if ok is None else "-" if ok else "대책 검토")
_set(sheet, f"L{r}", method_text(st.get("method_by_condition") or {}))
_set(sheet, f"M{r}", condition_list(st.get("surface_check_by_condition") or {}))
_set(sheet, f"N{r}", condition_list(st.get("edge_by_condition") or {}))
return ng
+23
View File
@@ -18,7 +18,9 @@ METHOD_NAMES = {
"janbu_corrected": "Janbu 보정",
"ordinary": "Fellenius",
"spencer": "Spencer",
"morgenstern_price": "Morgenstern-Price",
}
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
REASONS = {
"fill_height": "쌓기 높이 10 m 이상",
"cut_height": "깎기 높이 15 m 이상",
@@ -43,6 +45,27 @@ def mark(text: str, default: bool) -> str:
return f"{text} ({UNCONFIRMED if default else '사용자 입력'})"
def reach_text(reach: Any) -> str:
"""E03 탐색 한계 글 — `model_end`(기본) · `2h` · 수(m)."""
if isinstance(reach, int | float):
return f"비탈 끝 + {reach:g} m"
return "비탈 끝 + 2H" if reach == "2h" else "모형 끝"
def method_text(by_condition: dict[str, str]) -> str:
"""조건별 판정 방법 글 — 모두 같으면 방법 이름 하나 · 다르면 「건기 Bishop · 우기 M-P」."""
names = {c: METHOD_NAMES.get(m, m) for c, m in by_condition.items() if m}
if len(set(names.values())) <= 1:
return next(iter(names.values()), "-")
short = {"Morgenstern-Price": "M-P"} # 좁은 칸 — 조건마다 다를 때만 줄임
return " · ".join(f"{CONDITION_NAMES.get(c, c)} {short.get(n, n)}" for c, n in names.items())
def condition_list(flags: dict[str, Any]) -> str:
"""켜진 조건 이름 줄(건기 · 우기) — 없으면 `-`."""
return " · ".join(CONDITION_NAMES.get(c, c) for c, on in flags.items() if on) or "-"
def _set(sheet: dict[str, Any], a1: str, value: Any, style: int | None = None) -> None:
"""값(None = 지움) · 서식(None = 양식 그대로)."""
cell = dict(sheet["칸"].get(a1) or {})
+34 -17
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import copy
import math
from typing import Any
from typing import Any, NamedTuple
from E03_StabSection import E03_StabSection_View as view2d
from E03_StabSection.E03_StabSection_Engine_Model import final_surface
@@ -27,6 +27,7 @@ from E05_StabReport.E05_StabReport_Cells import (
_set,
_shift,
_verdict,
condition_list,
fig_key,
)
@@ -38,20 +39,30 @@ SLICE_COLS = "ABCDEFGHIJKLM"
SLICE_TPL = 5 # 부록 절편 덩이 꼴 = 양식 1~5 행(제목 · 단위 · 머리 · 줄 · 합계)
def _case_slip(result: dict[str, Any] | None, condition: str, side: str, method: str):
"""조건 하나 — 그 비탈 역할(깎기 · 쌓기) 중 그 방법 최소 면 · 높이."""
class Pick(NamedTuple):
"""조건 하나의 판정 면 — 면 · 경우 · 판정 방법 · 면 확인 · 모형 끝 면."""
slip: dict[str, Any]
case: dict[str, Any]
method: str
check: bool
edge: bool | None
def _case_slip(result: dict[str, Any] | None, condition: str, side: str, role: str | None):
"""조건 하나 — 그 비탈(좌 · 우 열쇠 · 없으면 역할 깎기 · 쌓기)의 E03 판정 면(원호 Bishop ·
비원호 Spencer · M-P 중 작은 값 · E03 `_governing` 과 같은 규칙)."""
best = None
for case in (result or {}).get("cases") or []:
if case.get("condition") != condition:
continue
if side in ("cut", "fill") and case.get("role") not in (side, None):
if side and case.get("side") != side:
continue
methods = (case.get("result") or {}).get("methods") or {}
found = (methods.get(method) or {}).get("min") or next(
(v["min"] for v in methods.values() if v.get("min")), None
)
if found and found.get("F") is not None and (best is None or found["F"] < best[0]["F"]):
best = (found, case)
if not side and role in ("cut", "fill") and case.get("role") not in (role, None):
continue
method, found, _circ, check = view2d._governing(case)
if found and found.get("F") is not None and (best is None or found["F"] < best.slip["F"]):
best = Pick(found, case, method, check, view2d._edge(case, found))
return best
@@ -94,7 +105,7 @@ def _picks(rep: dict[str, Any], ground: dict[str, Any]) -> dict[str, Any]:
st = rep["summary"]
side, method = st.get("side"), st.get("method") or "bishop"
used = [c for c in CASES if c != "seismic" or _seismic(ground)]
picks = {c: _case_slip(rep.get("result"), c, side, method) for c in used}
picks = {c: _case_slip(rep.get("result"), c, st.get("side_key"), side) for c in used}
fs = st.get("fs_by_condition") or {}
ratios = [
(float(fs[c]) / _criterion(ground, c, side), c)
@@ -125,7 +136,7 @@ def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[s
side, method, picks = got["side"], got["method"], got["picks"]
label = _label_at(labels, float(st["chainage_m"]))
dry = picks.get("dry")
height = float(dry[1].get("height_m") or 0) if dry else 0.0
height = float(dry.case.get("height_m") or 0) if dry else 0.0
_set(
sheet,
f"A{1 + dr}",
@@ -151,11 +162,12 @@ def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[s
crit = _criterion(ground, case, side)
ok = None if value is None else float(value) >= crit - 1e-9
pick = picks.get(case)
_set(sheet, f"B{row}", METHOD_NAMES.get(method, method))
pick_method = pick.method if pick else method
_set(sheet, f"B{row}", METHOD_NAMES.get(pick_method, pick_method))
_set(sheet, f"C{row}", float(value) if value is not None else None)
_set(sheet, f"D{row}", crit)
_verdict(sheet, f"E{row}", ok)
slip = pick[0] if pick else None
slip = pick.slip if pick else None
center = (slip or {}).get("center")
circle = bool(slip and slip.get("radius") and center)
_set(sheet, f"F{row}", float(slip["radius"]) if circle else "-" if slip else None)
@@ -165,13 +177,17 @@ def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[s
slips.append({"condition": case, "F": slip.get("F"), "points": slip["points"]})
box.append({"condition": case, "F": value, "required": crit, "ok": ok})
design = (rep.get("cross") or {}).get("design") or {}
kind = (dry[0].get("kind") if dry else None) or ""
kind = (dry.slip.get("kind") if dry else None) or ""
checks = {c: bool(p and p.check) for c, p in picks.items() if c in got["used"]}
edges = {c: bool(p and p.edge) for c, p in picks.items() if c in got["used"]}
layers = rep.get("layers") or {}
note = [
{"circle": "활동면 원호", "polyline": "활동면 비원호"}.get(kind, ""),
f"지반 {GROUND_NAMES.get(design.get('ground_type'), design.get('ground_type') or '-')}",
f"토층 두께 {layers['soil_thickness_m']:g} m" if layers.get("soil_thickness_m") else "",
f"부록 {_mark(k)} 절편 계산표" if got["governing"] else "",
f"면 확인 {condition_list(checks)}" if any(checks.values()) else "",
f"모형 끝 면 {condition_list(edges)}" if any(edges.values()) else "",
]
_set(sheet, f"B{27 + dr}", " · ".join(n for n in note if n))
jobs.append((key, _section_figure(rep, slips, box, ground)))
@@ -263,7 +279,8 @@ def fill_slices(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
for k, rep in enumerate(inputs["representatives"]):
got = _picks(rep, ground)
cond = got["governing"]
slip = got["picks"][cond][0] if cond else None
pick = got["picks"][cond] if cond else None
slip = pick.slip if pick else None
rows = (slip or {}).get("slices") or []
if not rows:
continue
@@ -274,7 +291,7 @@ def fill_slices(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
sheet,
f"A{1 + dr}",
f"부록 {_mark(k)} 절편 계산표 — {label} · {SIDE_NAMES.get(got['side'], '-')} · "
f"{CONDITION_NAMES[cond]} · {METHOD_NAMES.get(got['method'], got['method'])} · "
f"{CONDITION_NAMES[cond]} · {METHOD_NAMES.get(pick.method, pick.method)} · "
f"Fs {F:.3f}",
)
kind = {"circle": "원호", "polyline": "비원호"}.get(slip.get("kind"), "")