Files
Aislo/E03_StabSection/E03_StabSection_WhatIf.py
T
eomsangdonandClaude Opus 5.5 ab3eee034d feat(536-3): E 보강 검토 앞부분 — 벽 외적 안정(활동 · 전도 · 지지력 · KDS 11 80 05) · 비탈 완화 · 소단 가정 재해석 · 최소 완화 찾기 · E03 「보강 검토」 탭 · E05 보강검토 시트(있을 때만)
- E03_StabSection_WallCheck: Coulomb · Rankine · M-O 토압 · 상재하중 · 수압 · 양압력 · Meyerhof 지지력 · 평상시 / 지진 시 판정
- E03_StabSection_WhatIf: 한 쪽 비탈 1:n(+ 소단) 다시 그림 · 원지반 만나는 가장 완만한 n · 성긴 걸음 + 이분 최소 완화(완만할수록 늘 안전하지 않음)
- E03_StabSection_Router_WhatIf: POST whatif · wallcheck · GET whatif · 저장본 _derived/stab2d_whatif(+ _view) · 원본 측점 · 요약 그대로
- E05: s8 보강검토(양식 · 자리표) · 저장본 없으면 시트 뺌 · 갱신 때 사라진 프로그램 몫 뺌
- 시험 test_536_3 12 · 534-3 양식 시험 시트 8(보강검토 빈 판 1쪽)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:59:39 +09:00

200 lines
7.6 KiB
Python

"""E03 보강 가정 — 비탈 기울기 완화 · 소단 덧붙임 → 설계선 다시 그림 · 최소 완화 찾기 (PLAN 536-3).
storage 를 모르는 순수 함수. 가정 = 모형 형상만 바꿈(설계 · 임도 쪽으로 안 돌려 씀 · 요구 345 E).
- `reshape_side` = 한 쪽 비탈을 비탈 시작점에서 1:n(+ 높이 h 마다 폭 w 소단)으로 원지반까지 다시 그림 ·
안쪽(노면 · 측구)은 그대로 · 2단(암 · 토사) 경사는 한 경사로 갈음.
- `slope_start` = 비탈 시작점 — 깎기 = 측구 바깥(`cut_slope_segments` 시작) · 쌓기 = 노면 끝.
- `search_ratio` = 목표 안전율을 맞추는 가장 급한 1:n(이분 · `step` 단위 올림) — 해석은 부르는 쪽 함수.
좌표 = B06 offset(좌 +) · 표고.
"""
from __future__ import annotations
import math
from typing import Any, Callable
import numpy as np
#: 최소 완화 찾기 — 가장 완만한 기울기 · 찾는 단위
MAX_RATIO = 4.0
RATIO_STEP = 0.05
def current_ratio(design: dict[str, Any], side: str, role: str) -> float:
"""지금 설계 기울기 1:n 의 n — 깎기 = 그 쪽 첫 구간 · 쌓기 = 쌓기 기울기."""
if role == "fill":
return float(design.get("fill_slope_ratio") or 1.5)
segs = [s for s in design.get("cut_slope_segments") or [] if s.get("side") == side]
if segs:
return float(min(segs, key=lambda s: abs(s["start_offset_m"]))["ratio"])
return float(design.get("cut_slope_ratio") or 1.0)
def slope_start(design: dict[str, Any], side: str, role: str) -> float:
"""비탈 시작 offset."""
edge = float(((design.get("road_edges") or {}).get(side) or {}).get("offset_m", 0.0))
if role == "cut":
segs = [s for s in design.get("cut_slope_segments") or [] if s.get("side") == side]
if segs:
return float(min(segs, key=lambda s: abs(s["start_offset_m"]))["start_offset_m"])
return edge
def _profile(
x0: float, z0: float, sign: float, up: float, ratio: float, bench_w: float, bench_h: float
) -> list[tuple[float, float]]:
"""비탈 꼭짓점(시작 → 바깥) — 원지반을 넉넉히 지나도록 길게(뒤에서 자름)."""
pts = [(x0, z0)]
x, z = x0, z0
while abs(x - x0) < 200.0:
rise = bench_h if bench_w > 0 and bench_h > 0 else 200.0
x, z = x + sign * rise * ratio, z + up * rise
pts.append((x, z))
if bench_w > 0 and bench_h > 0:
x += sign * bench_w
pts.append((x, z))
return pts
def _daylight(
pts: list[tuple[float, float]], ground: list, sign: float, up: float
) -> list[tuple[float, float]] | None:
"""비탈이 원지반과 처음 만나는 점까지 — 못 만나면 None(지형 밖). 시작부터 원지반 밖이면 시작점만."""
gx = np.array([p[0] for p in ground], dtype=float)
gz = np.array([p[1] for p in ground], dtype=float)
out = [pts[0]]
for (xa, za), (xb, zb) in zip(pts, pts[1:]):
inside = gx[(gx > min(xa, xb)) & (gx < max(xa, xb))]
xs = np.unique(np.r_[xa, xb, inside])
xs = xs[np.argsort(sign * xs)]
zl = za + (xs - xa) / (xb - xa) * (zb - za) if xb != xa else np.full(len(xs), zb)
f = up * (zl - np.interp(xs, gx, gz)) # ≥ 0 = 원지반 밖으로 나옴
if len(out) == 1 and f[0] >= 0:
return out
for k in range(1, len(xs)):
if not gx[0] <= xs[k] <= gx[-1]:
return None
if f[k] >= 0:
s = f[k - 1] / (f[k - 1] - f[k])
x = float(xs[k - 1] + s * (xs[k] - xs[k - 1]))
return out + [(x, float(zl[k - 1] + s * (zl[k] - zl[k - 1])))]
out.append((xb, zb))
return None
def reshape_side(
design_line: list,
ground: list,
*,
side: str,
role: str,
start_x: float,
ratio: float,
bench_w: float = 0.0,
bench_h: float = 0.0,
) -> list[list[float]] | None:
"""한 쪽 비탈만 다시 그린 설계선([[offset, z] …]) — 원지반을 못 만나면 None."""
line = sorted((float(x), float(z)) for x, z in design_line)
sign = 1.0 if side == "left" else -1.0
up = 1.0 if role == "cut" else -1.0
lx = np.array([p[0] for p in line])
lz = np.array([p[1] for p in line])
z0 = float(np.interp(start_x, lx, lz))
inner = [p for p in line if sign * p[0] < sign * start_x - 1e-9]
slope = _daylight(_profile(start_x, z0, sign, up, ratio, bench_w, bench_h), ground, sign, up)
if slope is None:
return None
return [list(p) for p in sorted(inner + slope)]
def design_variant(
design: dict[str, Any], ground: list, assume: dict[str, Any]
) -> dict[str, Any] | None:
"""가정 하나(side · role · ratio · bench_w · bench_h) → 설계선만 바꾼 설계 사본(None = 원지반 못 만남)."""
side, role = assume["side"], assume["role"]
pts = [(p["offset_m"], p["elevation_m"]) for p in design.get("design_line") or []]
line = reshape_side(
pts,
ground,
side=side,
role=role,
start_x=slope_start(design, side, role),
ratio=float(assume["ratio"]),
bench_w=float(assume.get("bench_w") or 0.0),
bench_h=float(assume.get("bench_h") or 0.0),
)
if line is None:
return None
return {**design, "design_line": [{"offset_m": x, "elevation_m": z} for x, z in line]}
def gentlest_ratio(
design: dict[str, Any],
ground: list,
assume: dict[str, Any],
start: float,
step: float = RATIO_STEP,
) -> float | None:
"""원지반을 만나는 가장 완만한 n(start ~ MAX_RATIO · step 단위 · 형상만 · 이분) — 없으면 None."""
def meets(n: float) -> bool:
return design_variant(design, ground, {**assume, "ratio": n}) is not None
lo = math.ceil(start / step - 1e-9) * step
hi = math.floor(MAX_RATIO / step + 1e-9) * step
if not meets(lo):
return None
if meets(hi):
return round(hi, 4)
while hi - lo > step + 1e-9: # lo = 만남 · hi = 못 만남
mid = round(round((lo + hi) / 2 / step) * step, 6)
if mid in (lo, hi):
break
lo, hi = (mid, hi) if meets(mid) else (lo, mid)
return round(lo, 4)
def search_ratio(
passes: Callable[[float], bool],
start: float,
*,
max_ratio: float = MAX_RATIO,
step: float = RATIO_STEP,
coarse: int = 5,
) -> dict[str, Any]:
"""목표를 맞추는 가장 급한 n(start ≤ n ≤ max_ratio · step 단위). passes(n) = 해석 · 판정.
완만할수록 늘 안전하지 않음(급한 자연 비탈 = 완화가 깎기 높이를 키움) — 성긴 걸음(최대 `coarse` 칸)으로
처음 맞는 칸을 찾고 그 앞 칸 사이를 이분으로 좁힘 · 끝까지 못 맞추면 found None."""
tried: list[tuple[float, bool]] = []
def test(n: float) -> bool:
ok = passes(n)
tried.append((round(n, 4), ok))
return ok
def snap(n: float) -> float:
return round(round(n / step) * step, 6)
lo = snap(math.ceil(start / step - 1e-9) * step)
top = snap(math.floor(max_ratio / step + 1e-9) * step)
if test(lo):
return {"found": lo, "tried": tried}
gap = max(2 * step, snap((top - lo) / coarse))
hi = None
n = lo
while n < top - 1e-9:
n = min(snap(n + gap), top)
if test(n):
hi = n
break
lo = n
if hi is None:
return {"found": None, "tried": tried}
while hi - lo > step + 1e-9: # lo = 못 맞춤 · hi = 맞춤
mid = snap((lo + hi) / 2)
if mid in (lo, hi):
break
lo, hi = (lo, mid) if test(mid) else (mid, hi)
return {"found": round(hi, 4), "tried": tried}