- 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>
200 lines
7.6 KiB
Python
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}
|