229 lines
8.8 KiB
Python
229 lines
8.8 KiB
Python
"""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
|