"""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