diff --git a/E05_StabReport/E05_StabReport_Build.py b/E05_StabReport/E05_StabReport_Build.py index 215ed20a3..e1f130792 100644 --- a/E05_StabReport/E05_StabReport_Build.py +++ b/E05_StabReport/E05_StabReport_Build.py @@ -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 diff --git a/E05_StabReport/E05_StabReport_Cells.py b/E05_StabReport/E05_StabReport_Cells.py index 8fcf86cfb..5bad79cc1 100644 --- a/E05_StabReport/E05_StabReport_Cells.py +++ b/E05_StabReport/E05_StabReport_Cells.py @@ -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 {}) diff --git a/E05_StabReport/E05_StabReport_Section.py b/E05_StabReport/E05_StabReport_Section.py index 37ef40bf6..2fbcfaa66 100644 --- a/E05_StabReport/E05_StabReport_Section.py +++ b/E05_StabReport/E05_StabReport_Section.py @@ -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"), "") diff --git a/resources/tester/test_534_3_e05_report.py b/resources/tester/test_534_3_e05_report.py index 2d467426f..b503c3954 100644 --- a/resources/tester/test_534_3_e05_report.py +++ b/resources/tester/test_534_3_e05_report.py @@ -283,6 +283,53 @@ def test_처음_만듦_측점수_NG_그림(project): assert saved["fingerprint"] == build.analysis_fingerprint(project) +def test_567_판정_면_방법_면확인_모형끝_탐색한계(project): + """567-1 이음 — 비원호(M-P)가 판정한 측점 · 조건별 방법 · 면 확인 · 모형 끝 면 · 탐색 한계.""" + rows = [_summary(i) for i in range(N)] + dry = rows[3]["cases"]["dry"]["left"] + dry.update(method="morgenstern_price", fs_circular=1.9, surface_check=True, edge=True) + wet = rows[3]["cases"]["wet"]["left"] + wet.update(method="bishop", fs_circular=wet["fs"], surface_check=False, edge=False) + rows[3]["cases"]["wet"]["right"]["surface_check"] = True # 다른 비탈 면 확인도 조건에 잡힘 + _write_summary(project, "fp-2", rows) + value = json.loads((derived_dir(project, KIND_SUMMARY) / "fp-2.json").read_text("utf-8")) + value["value"]["search_reach"] = "2h" + (derived_dir(project, KIND_SUMMARY) / "fp-2.json").write_text(json.dumps(value), "utf-8") + doc = _station_doc(3) + poly = {**_slip(1.31), "kind": "polyline", "entry": [-29.5, 100.0], "exit": [5.0, 99.0]} + poly.pop("center"), poly.pop("radius") + for case in doc["cases"]: + if case["condition"] == "dry" and case["side"] == "left": + circ = _slip(1.9) + case["result"] = { + "model_x": [-30.0, 30.0], + "methods": { + "bishop": {"circular": circ, "min": circ}, + "morgenstern_price": {"noncircular": poly, "min": poly}, + }, + } + write_derived(project, KIND_STATION, "st3", {"out": doc, "error": None}, keep=N) + _create(project) + book, _ = store.load(project) + by = _sheets(book) + s4 = by["측점표"]["칸"] + r = 9 # 60 m = 4 번째 줄(6 행부터) + assert s4[f"D{r}"]["값"] == 1.31 # E03 화면 건기 판정 값과 같음 + assert s4[f"L{r}"]["값"] == "건기 M-P · 우기 Bishop" + assert (s4[f"M{r}"]["값"], s4[f"N{r}"]["값"]) == ("건기 · 우기", "건기") + assert (s4["L6"]["값"], s4["M6"]["값"], s4["N6"]["값"]) == ("Bishop", "-", "-") # 옛 꼴 + assert s4["L4"]["값"] == "판정 방법" and s4["M4"]["값"] == "면 확인" + assert "탐색 한계 = 비탈 끝 + 2H" in s4["A3"]["값"] and "15 %" in s4["A3"]["값"] + assert "탐색 한계 = 비탈 끝 + 2H" in by["입력조건"]["칸"]["B6"]["값"] + s5, s10 = by["대표단면"]["칸"], by["부록_절편"]["칸"] + assert (s5["B24"]["값"], s5["B25"]["값"]) == ("Morgenstern-Price", "Bishop") + assert s5["E24"]["값"] == "N.G" and s5["C24"]["값"] == 1.31 + assert s5["F24"]["값"] == "-" and s5["F25"]["값"] == 12.5 # 비원호 = 반경 없음 + assert "면 확인 건기" in s5["B27"]["값"] and "모형 끝 면 건기" in s5["B27"]["값"] + assert "Morgenstern-Price" in s10["A1"]["값"] and "Fs 1.310" in s10["A1"]["값"] + assert "비원호" in s10["A2"]["값"] + + def test_광역요약_E07_저장본(project): """E07 저장본 없음 = 「광역 해석 전」 · 생김 = 지문 바뀜 → 갱신 뒤 등급 면적 · 위험 측점 줄.""" _create(project)