시험 고침: 534-3 양식 시험의 시트 목록 · 쪽 수 기대(목차 · 부록 더함) · E05 시험 시트 수 7 → 9 · PDF = report_pdf(목차 쪽 번호 = 실제 · 꼬리말 · 머리말) · 부록 절편 · 측점표 쪽 나눔 · 옛 작업본 갱신 시험 덧. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
14 KiB
Python
319 lines
14 KiB
Python
"""E05 보고서 s5 대표단면 · s10 부록 절편 — 단면 하나 = 한 쪽(PLAN 534-3 · 534-5).
|
||
|
||
칸 자리 = `resources/master_template/report/stab_report_slots.md` s5 · s10.
|
||
그림 = `E05_StabReport_Figure.section_svg`(값 셈 없음 · 해석 저장본 옮겨 그림).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy
|
||
import math
|
||
from typing import Any
|
||
|
||
from E03_StabSection import E03_StabSection_View as view2d
|
||
from E03_StabSection.E03_StabSection_Engine_Model import final_surface
|
||
from E05_StabReport import E05_StabReport_Figure as figure
|
||
from E05_StabReport.E05_StabReport_Cells import (
|
||
A1,
|
||
CASES,
|
||
GROUND_NAMES,
|
||
METHOD_NAMES,
|
||
REASONS,
|
||
SIDE_NAMES,
|
||
_criterion,
|
||
_label_at,
|
||
_labels,
|
||
_seismic,
|
||
_set,
|
||
_shift,
|
||
_verdict,
|
||
fig_key,
|
||
)
|
||
|
||
BLOCK = 28 # 대표단면 한 쪽 = 28 행
|
||
LOWEST_N = view2d.REPRESENTATIVE_LOWEST
|
||
CIRCLED = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳"
|
||
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
|
||
SLICE_COLS = "ABCDEFGHIJKLM"
|
||
SLICE_TPL = 5 # 부록 절편 덩이 꼴 = 양식 1~5 행(제목 · 단위 · 머리 · 줄 · 합계)
|
||
|
||
|
||
def _case_slip(result: dict[str, Any] | None, condition: str, side: str, method: str):
|
||
"""조건 하나 — 그 비탈 역할(깎기 · 쌓기) 중 그 방법 최소 면 · 높이."""
|
||
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):
|
||
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)
|
||
return best
|
||
|
||
|
||
def _block_copy(
|
||
sheet: dict[str, Any], template: dict[str, Any], dr: int, rows: int, tag: str
|
||
) -> None:
|
||
"""양식 1~rows 행 덩이를 dr 행 아래로(칸 · 병합 · 행 높이 · 조건부서식)."""
|
||
for a1, cell in template["칸"].items():
|
||
if int(A1.match(a1).group(2)) <= rows:
|
||
sheet["칸"][_shift(a1, dr)] = copy.deepcopy(cell)
|
||
for m in template.get("병합", []):
|
||
if int(A1.match(m.split(":")[0]).group(2)) <= rows:
|
||
sheet["병합"].append(":".join(_shift(p, dr) for p in m.split(":")))
|
||
for r, info in (template.get("행") or {}).items():
|
||
if int(r) <= rows:
|
||
sheet["행"][str(int(r) + dr)] = dict(info)
|
||
for rule in template.get("조건부서식", []):
|
||
moved = copy.deepcopy(rule)
|
||
moved["id"] = f"{rule['id']}_{tag}"
|
||
moved["범위"] = [{**g, "r0": g["r0"] + dr, "r1": g["r1"] + dr} for g in rule["범위"]]
|
||
sheet["조건부서식"].append(moved)
|
||
|
||
|
||
def _clear(sheet: dict[str, Any]) -> dict[str, Any]:
|
||
"""시트를 비우고 양식 사본을 돌려줌."""
|
||
template = copy.deepcopy(sheet)
|
||
sheet["칸"] = {}
|
||
sheet["병합"], sheet["행"], sheet["조건부서식"] = [], {}, []
|
||
if "그림" in sheet:
|
||
sheet["그림"] = []
|
||
return template
|
||
|
||
|
||
def _mark(k: int) -> str:
|
||
return CIRCLED[k] if k < len(CIRCLED) else str(k + 1)
|
||
|
||
|
||
def _picks(rep: dict[str, Any], ground: dict[str, Any]) -> dict[str, Any]:
|
||
"""대표 단면 하나 — 조건별 (면, 경우) · 쓰는 조건 · 지배 조건(Fs / 기준 가장 낮음)."""
|
||
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}
|
||
fs = st.get("fs_by_condition") or {}
|
||
ratios = [
|
||
(float(fs[c]) / _criterion(ground, c, side), c)
|
||
for c in used
|
||
if fs.get(c) is not None and picks.get(c)
|
||
]
|
||
governing = min(ratios)[1] if ratios else None
|
||
return {"side": side, "method": method, "used": used, "picks": picks, "governing": governing}
|
||
|
||
|
||
def fill_sections(sheet: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[str, str]]:
|
||
"""s5 대표단면 — 단면 하나 = 28 행 덩이 = 한 쪽 · 그림 일감 [(열쇠, SVG)]."""
|
||
reps = inputs["representatives"]
|
||
ground = inputs["ground"]
|
||
labels = _labels(inputs["stations"])
|
||
template = _clear(sheet)
|
||
if not reps:
|
||
_block_copy(sheet, template, 0, BLOCK, "0")
|
||
_set(sheet, "A1", "대표 단면 — 해당 없음")
|
||
sheet["인쇄"]["쪽나눔"] = []
|
||
return []
|
||
jobs: list[tuple[str, str]] = []
|
||
for k, rep in enumerate(reps):
|
||
dr = BLOCK * k
|
||
_block_copy(sheet, template, dr, BLOCK, str(k))
|
||
st = rep["summary"]
|
||
got = _picks(rep, ground)
|
||
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
|
||
_set(
|
||
sheet,
|
||
f"A{1 + dr}",
|
||
f"대표 단면 {_mark(k)} — {label} · {SIDE_NAMES.get(side, side or '-')} · "
|
||
f"H {height:.1f} m",
|
||
)
|
||
reason = REASONS.get(rep.get("reason"), rep.get("reason") or "")
|
||
if rep.get("reason") == "lowest":
|
||
reason = f"{reason} {LOWEST_N}"
|
||
_set(sheet, f"B{2 + dr}", reason)
|
||
key = fig_key(rep["station_key"])
|
||
sheet["그림"].append(
|
||
{"열쇠": key, "닻": f"A{3 + dr}:H{22 + dr}", "이름": f"대표 단면 {label}"}
|
||
)
|
||
fs = st.get("fs_by_condition") or {}
|
||
slips, box = [], []
|
||
for r, case in zip((24, 25, 26), CASES):
|
||
row = r + dr
|
||
if case not in got["used"]:
|
||
_set(sheet, f"B{row}", "검토 안 함")
|
||
continue
|
||
value = fs.get(case)
|
||
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))
|
||
_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
|
||
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)
|
||
_set(sheet, f"G{row}", float(center[0]) if circle else "-" if slip else None)
|
||
_set(sheet, f"H{row}", float(center[1]) if circle else "-" if slip else None)
|
||
if slip and slip.get("points"):
|
||
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 ""
|
||
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 "",
|
||
]
|
||
_set(sheet, f"B{27 + dr}", " · ".join(n for n in note if n))
|
||
jobs.append((key, _section_figure(rep, slips, box, ground)))
|
||
sheet["인쇄"]["쪽나눔"] = [BLOCK * (k + 1) for k in range(len(reps))]
|
||
return jobs
|
||
|
||
|
||
def _strata(rep: dict[str, Any], ground: dict[str, Any], fill: bool) -> list[dict[str, str]]:
|
||
"""지층 범례 — 성토(공사 뒤 지표가 원지반 위인 곳이 있으면) · 위 지층 · 아래 지층."""
|
||
names = {m["id"]: m for m in ground.get("materials") or []}
|
||
layers = rep.get("layers") or {}
|
||
out = []
|
||
picks = [
|
||
("fill", (ground.get("label_map") or {}).get("fill") or "fill", fill),
|
||
("soil", layers.get("upper"), True),
|
||
("rock", layers.get("lower"), bool(layers.get("soil_thickness_m"))),
|
||
]
|
||
for kind, mid, show in picks:
|
||
m = names.get(mid)
|
||
if show and m:
|
||
text = f"{m['name']} γt {m['gamma_t']:g} · c {m['c']:g} · φ {m['phi']:g}"
|
||
out.append({"kind": kind, "text": text})
|
||
return out
|
||
|
||
|
||
def _section_figure(
|
||
rep: dict[str, Any], slips: list[dict[str, Any]], box: list[dict[str, Any]], soil: dict
|
||
) -> str:
|
||
cross = rep.get("cross") or {}
|
||
design = cross.get("design") or {}
|
||
ground = sorted( # 해석에 쓴 넓힌 지반선(임계면이 횡단 표본 밖까지 감) · 없으면 횡단 표본
|
||
(float(x), float(z)) for x, z in rep.get("ground") or []
|
||
) or sorted(
|
||
(float(s["offset_m"]), float(s["z"]))
|
||
for s in cross.get("samples") or []
|
||
if s.get("valid", True) and s.get("z") is not None
|
||
)
|
||
line = sorted(
|
||
(float(p["offset_m"]), float(p["elevation_m"])) for p in design.get("design_line") or []
|
||
)
|
||
surface = [tuple(p) for p in final_surface(ground, line)] if ground and line else ground
|
||
thickness = (rep.get("layers") or {}).get("soil_thickness_m")
|
||
rock = [(x, z - float(thickness)) for x, z in ground] if thickness else None
|
||
edges = design.get("road_edges") or {}
|
||
road = None
|
||
if edges.get("left") and edges.get("right"):
|
||
road = (float(edges["right"]["offset_m"]), float(edges["left"]["offset_m"]))
|
||
heights = []
|
||
if road and line:
|
||
for case in (rep.get("result") or {}).get("cases") or []:
|
||
if case.get("condition") != "dry" or not case.get("height_m"):
|
||
continue
|
||
left = case.get("side") == "left"
|
||
edge = road[1] if left else road[0]
|
||
day = line[-1][0] if left else line[0][0]
|
||
z_edge, z1 = figure._interp(surface, edge), figure._interp(surface, day)
|
||
if z_edge is not None and z1 is not None: # 높이 = 엔진 값(비탈 끝에서 길 쪽으로)
|
||
h = float(case["height_m"])
|
||
z0 = z1 + h if z_edge > z1 else z1 - h
|
||
heights.append({"x": day, "z0": z0, "z1": z1, "label": f"H {h:.2f} m"})
|
||
filled = any(
|
||
(z_g := figure._interp(ground, x)) is not None and z - z_g > 0.05 for x, z in surface
|
||
)
|
||
return figure.section_svg(
|
||
title="",
|
||
ground=ground,
|
||
surface=surface,
|
||
rock=rock,
|
||
slips=slips,
|
||
road=road,
|
||
heights=heights,
|
||
wet=any(s["condition"] == "wet" for s in slips),
|
||
strata=_strata(rep, soil, filled),
|
||
fs_box=box,
|
||
)
|
||
|
||
|
||
# ── s10 부록 절편 ────────────────────────────────────────────────────────────
|
||
def fill_slices(sheet: dict[str, Any], inputs: dict[str, Any]) -> None:
|
||
"""s10 부록 절편 — 대표 단면마다 지배 조건(Fs / 기준 가장 낮음) 임계면 절편표 한 쪽."""
|
||
ground = inputs["ground"]
|
||
labels = _labels(inputs["stations"])
|
||
template = _clear(sheet)
|
||
data_tpl = {c: dict(template["칸"].get(f"{c}4") or {}) for c in SLICE_COLS}
|
||
sum_tpl = {c: dict(template["칸"].get(f"{c}5") or {}) for c in SLICE_COLS}
|
||
data_h = (template.get("행") or {}).get("4")
|
||
sum_h = (template.get("행") or {}).get("5")
|
||
dr, breaks = 0, []
|
||
for k, rep in enumerate(inputs["representatives"]):
|
||
got = _picks(rep, ground)
|
||
cond = got["governing"]
|
||
slip = got["picks"][cond][0] if cond else None
|
||
rows = (slip or {}).get("slices") or []
|
||
if not rows:
|
||
continue
|
||
_block_copy(sheet, template, dr, 3, str(k))
|
||
label = _label_at(labels, float(rep["summary"]["chainage_m"]))
|
||
F = float(slip["F"])
|
||
_set(
|
||
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"Fs {F:.3f}",
|
||
)
|
||
kind = {"circle": "원호", "polyline": "비원호"}.get(slip.get("kind"), "")
|
||
_set(
|
||
sheet,
|
||
f"A{2 + dr}",
|
||
f"대표 단면 {_mark(k)} 의 {CONDITION_NAMES[cond]} 임계 활동면({kind}) · "
|
||
f"{template['칸']['A2']['값']}",
|
||
)
|
||
sums = [0.0, 0.0, 0.0, 0.0]
|
||
for i, s in enumerate(rows):
|
||
r = 4 + dr + i
|
||
for c, cell in data_tpl.items():
|
||
sheet["칸"][f"{c}{r}"] = dict(cell)
|
||
if data_h:
|
||
sheet["행"][str(r)] = dict(data_h)
|
||
drive = (s["weight"] + s["surcharge"]) * math.sin(math.radians(s["alpha_deg"]))
|
||
resist = s["shear_mobilized"] * F
|
||
values = (
|
||
i + 1, s["width"], s["height"], s["alpha_deg"], s["weight"], s["surcharge"],
|
||
s["pore_pressure"], s["base_length"], s["c"], s["phi_deg"], s["normal_eff"],
|
||
drive, resist,
|
||
) # fmt: skip
|
||
for c, v in zip(SLICE_COLS, values):
|
||
_set(sheet, f"{c}{r}", float(v) if c != "A" else v)
|
||
for j, v in enumerate((s["weight"], s["surcharge"], drive, resist)):
|
||
sums[j] += v
|
||
r = 4 + dr + len(rows)
|
||
for c, cell in sum_tpl.items():
|
||
sheet["칸"][f"{c}{r}"] = dict(cell)
|
||
if sum_h:
|
||
sheet["행"][str(r)] = dict(sum_h)
|
||
_set(sheet, f"A{r}", "합계")
|
||
for c, v in zip("EFLM", sums):
|
||
_set(sheet, f"{c}{r}", v)
|
||
dr = r
|
||
breaks.append(r)
|
||
if not breaks:
|
||
_block_copy(sheet, template, 0, 2, "0")
|
||
_set(sheet, "A1", "부록 — 절편 계산표 없음(대표 단면 없음)")
|
||
sheet["인쇄"]["쪽나눔"] = breaks
|