"""E05 보고서 s11 암반검토 — E06 암반 해석 저장본(절리 · Markland · Hoek-Bray) → 시트 (PLAN 536-1). 칸 자리 = `resources/master_template/report/stab_report_slots.md` s11. 저장본이 없으면 시트를 뺌(있을 때만) — `build_program` 이 책에서 지움. """ from __future__ import annotations import copy from pathlib import Path from typing import Any from fastapi import HTTPException from E05_StabReport.E05_StabReport_Cells import _set, _verdict, mark from E06_StabRock import E06_StabRock_Model as rock_model from E06_StabRock.E06_StabRock_Router import _result CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"} SIDE_NAMES = {"left": "좌", "right": "우"} PROJECTION_NAMES = {"equal_angle": "등각(Wulff)", "equal_area": "등면적(Schmidt)"} VERDICT_NAMES = {"NG": "N.G", "check": "검토", "OK": "O.K", "none": "위험 모드 없음"} SLIDE_NAMES = {"1": "한 면({a}) 미끄러짐", "2": "한 면({b}) 미끄러짐", "12": "두 면 미끄러짐"} COLS = "ABCDEFGH" FIRST = 11 # 이 행부터 절리 · 측점 표를 다시 깔음 def read(project_root: Path) -> dict[str, Any] | None: """보고서에 쓰는 E06 결과(지금 입력 지문 것 · 없으면 가장 최근) + 낡음 표시.""" try: out = _result(Path(project_root), None, None) except HTTPException: return None return {**out["result"], "stale": out["stale"]} if out["result"] else None def stamp(project_root: Path) -> list | None: """해석 지문용 — 결과 지문 · 낡음.""" rock = read(project_root) return [rock["fingerprint"], rock["stale"]] if rock else None def _row_styles(sheet: dict[str, Any], r: int) -> dict[str, dict]: return {c: copy.deepcopy(sheet["칸"].get(f"{c}{r}") or {}) for c in COLS} def _summary(rock: dict[str, Any]) -> str: s = rock["summary"] v = s["verdicts"] text = ( f"깎기 비탈 {s['slopes']} · " + " · ".join(f"{VERDICT_NAMES[k]} {v[k]}" for k in VERDICT_NAMES if v.get(k)) + f" — 위험 모드 평면 {s['modes']['planar']} · 쐐기 {s['modes']['wedge']} · " f"전도 {s['modes']['toppling']}" ) if s.get("small_blocks"): text += f" · 소규모 블록 {s['small_blocks']} (판정에서 뺌 · 낙석 관리)" if rock["stale"]: text += " · 입력이 해석 뒤 바뀜 — E06 [해석] 뒤 [보고서 갱신]" return text def fill_rock(sheet: dict[str, Any], rock: dict[str, Any], labels: dict) -> None: """s11 — 1. 조건 · 요약(5~9 행) · 2. 절리군(한 줄씩 · 13 행 서식) · 3. 측점(17 행 서식 · 머리 줄 쪽마다).""" s = rock["settings"] d = rock_model.DEFAULT_SETTINGS lateral = (s["lateral_planar_deg"], s["lateral_toppling_deg"]) _set( sheet, "B5", mark( f"{PROJECTION_NAMES.get(s['projection'], s['projection'])} 하반구 · " f"측방 한계 평면 {lateral[0]:g}° · 전도 {lateral[1]:g}°", lateral == (d["lateral_planar_deg"], d["lateral_toppling_deg"]), ), ) _set( sheet, "B6", f"{s['gamma_r']:g} kN/m³ (사용자 입력)" if s.get("gamma_r") else "E02 측점 암 물성 γt (문헌 추정치 · 사용자 확정 전)", ) _set( sheet, "B7", mark( f"인장균열 깊이 · 활동면 높이의 {s['water_fill_wet']:g} 까지 지하수 " "(KDS 11 70 05 표 4.3-3 암반 = ½ 심도)", s["water_fill_wet"] == d["water_fill_wet"], ), ) crack = s["crack"] crack_text = { "none": "인장균열 없음", "critical": "인장균열 = 임계(최소 안전율 자리)", "depth": f"인장균열 깊이 {crack.get('z') or 0:g} m", }[crack["mode"]] bolt = s["bolt"] bolt_text = ( f"볼트 {bolt['force']:g} (평면 kN/m · 쐐기 kN) · 수평 아래 {bolt['plunge']:g}°" if bolt["force"] else "볼트 없음" ) min_block = s.get("min_block_m3", d["min_block_m3"]) _set( sheet, "B8", f"{crack_text} · {bolt_text} · " + mark( f"최소 블록 {min_block:g} m³ (이보다 작으면 판정에서 뺌)", min_block == d["min_block_m3"], ), ) _set(sheet, "B9", _summary(rock)) tpl = {r: _row_styles(sheet, r) for r in (11, 12, 13, 15, 16, 17)} merged = [m for m in sheet["병합"] if m.startswith(("A11:", "A15:"))] for a1 in [k for k in sheet["칸"] if int("".join(ch for ch in k if ch.isdigit())) >= FIRST]: del sheet["칸"][a1] sheet["병합"] = [m for m in sheet["병합"] if m not in merged] heights = {k: v for k, v in sheet["행"].items() if int(k) >= FIRST} sheet["행"] = {k: v for k, v in sheet["행"].items() if int(k) < FIRST} def put_row(r: int, src: int) -> None: for c, cell in tpl[src].items(): if cell: sheet["칸"][f"{c}{r}"] = copy.deepcopy(cell) if str(src) in heights: sheet["행"][str(r)] = dict(heights[str(src)]) def head(r: int, src: int) -> int: put_row(r, src) sheet["병합"].append(f"A{r}:H{r}") put_row(r + 1, src + 1) return r + 2 # 2. 절리군 r = head(FIRST, 11) sets = _sets(rock) for j in sets: put_row(r, 13) _set(sheet, f"A{r}", j.get("name") or j["id"]) for col, key in zip("BCDEF", ("dip_direction", "dip", "phi", "c", "persistence_m")): value = j.get(key) _set(sheet, f"{col}{r}", float(value) if value is not None else "-") _set(sheet, f"G{r}", _station_text(labels, j.get("from_m"))) _set(sheet, f"H{r}", _station_text(labels, j.get("to_m"))) r += 1 if not sets: put_row(r, 13) _set(sheet, f"A{r}", "절리군 없음") r += 1 # 3. 측점별 판정 r = head(r + 1, 15) sheet["인쇄"]["머리행"] = [r - 1, r - 1] # 측점 표 머리 줄 = 쪽마다 for entry in rock["stations"]: r = _station_rows(sheet, put_row, r, entry, labels) def _sets(rock: dict[str, Any]) -> list[dict[str, Any]]: """해석에 쓴 절리군 — 옛 저장본(sets 없음)은 측점 결과에 실린 것(지속성 · 구간 없음).""" if "sets" in rock: return rock["sets"] seen: dict[str, dict] = {} for entry in rock["stations"]: for j in entry["joints"]: seen.setdefault(j["id"], j) return list(seen.values()) def _station_text(labels: dict, chainage: Any) -> Any: if chainage is None: return "-" ch = float(chainage) return labels.get(round(ch, 3)) or f"{ch:.2f}" def _station_rows(sheet, put_row, r: int, entry: dict[str, Any], labels: dict) -> int: slope = entry["slope"] ch = float(entry["chainage_m"]) name = entry.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}" side = ( f"{SIDE_NAMES.get(slope['side'], slope['side'])} · {slope['dip']:g}° / " f"{slope['dip_direction']:g}° · 높이 {slope['height_m']:.1f} m" ) jname = {j["id"]: j.get("name") or j["id"] for j in entry["joints"]} rows: list[tuple[str, str, Any, Any, bool | None, str]] = [] for case in entry["cases"]: ids = [jname.get(i, i) for i in case["ids"]] mode = f"평면 {ids[0]}" if case["mode"] == "planar" else f"쐐기 {ids[0]} × {ids[1]}" notes = [] if not case["formed"]: notes.append("블록 안 생김") if case.get("sliding") == "lift": notes.append("들림") elif case.get("sliding") in SLIDE_NAMES: notes.append(SLIDE_NAMES[case["sliding"]].format(a=ids[0], b=ids[-1])) if case.get("small"): notes.append("소규모 · 낙석 관리") if case.get("persistence_short"): notes.append("지속성 < 활동면") cond = CONDITION_NAMES.get(case["condition"], case["condition"]) rows.append( (mode, cond, case["F"], case.get("required"), case.get("ok"), " · ".join(notes)) ) for jid in entry["kinematic"]["toppling"]: rows.append( (f"전도 {jname.get(jid, jid)}", "-", None, None, None, "기하 판정만 — 현장 확인") ) if not rows: rows.append(("없음", "-", None, None, None, "위험 모드 없음(Markland)")) for mode, cond, fs, need, ok, note in rows: put_row(r, 17) _set(sheet, f"A{r}", name) _set(sheet, f"B{r}", side) _set(sheet, f"C{r}", mode) _set(sheet, f"D{r}", cond) _set(sheet, f"E{r}", float(fs) if fs is not None else "-") _set(sheet, f"F{r}", float(need) if need is not None else "-") if ok is None: _set(sheet, f"G{r}", "검토" if mode.startswith("전도") else "-") else: _verdict(sheet, f"G{r}", ok) _set(sheet, f"H{r}", note or None) r += 1 return r