Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_3' and 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -18,21 +18,31 @@ from __future__ import annotations
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import multiprocessing
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
import numpy as np
|
||||
import shapely
|
||||
from shapely.geometry import LineString, MultiLineString
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Grade import (
|
||||
legal_grade_limit_pct,
|
||||
legal_plan_radius_min_m,
|
||||
resolve_design_speed,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial import (
|
||||
_POOLS,
|
||||
_TERRAINS,
|
||||
generate_initial_route,
|
||||
load_terrain_and_streams,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Store import request_fingerprint
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_surface_confirmation import SHEET_ANALYSIS_FILTER
|
||||
from config.config_system_terrain import PLAN_ANALYZE_WORKERS
|
||||
from D03_PlanAnalysis.D03_PlanAnalysis_Terrain import PLAN_CORRIDOR_M, models_dir_of
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -43,6 +53,9 @@ SELECTION = {"source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth":
|
||||
# D 등록에 지형 구분 칸 없음 — B `_road_settings` 못 읽을 때 폴백 그대로(가장 완화)
|
||||
TERRAIN_TYPE = "special"
|
||||
ALGO = 1
|
||||
# 계류 거르기 칸 · 지형 상자 여유(m) — `near_streams`
|
||||
STREAM_TILE_M = 200.0
|
||||
STREAM_PAD_M = 100.0
|
||||
|
||||
Progress = Callable[[int, str], None]
|
||||
Loader = Callable[..., tuple[Any, list]]
|
||||
@@ -90,6 +103,38 @@ def _route_view(result: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: result.get(key) for key in keep}
|
||||
|
||||
|
||||
def _engine_inputs(criteria: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"weights": None,
|
||||
"max_grade": criteria["max_grade"],
|
||||
"min_radius_m": criteria["min_radius_m"],
|
||||
"stream_offset_m": None,
|
||||
"grade_smooth_m": None,
|
||||
"target_grade": None,
|
||||
"with_profile": False,
|
||||
"design_speed": criteria["design_speed"],
|
||||
}
|
||||
|
||||
|
||||
def segment_fingerprint(
|
||||
seg: dict[str, Any],
|
||||
terrain: Any,
|
||||
streams: list,
|
||||
criteria: dict[str, Any],
|
||||
corridor_m: float = PLAN_CORRIDOR_M,
|
||||
) -> str:
|
||||
"""구간 입력 지문 — 갈래 넷 B 저장 키 · 복도 · 판."""
|
||||
expected = [_point(seg["start"]), _point(seg["end"])]
|
||||
inputs = _engine_inputs(criteria)
|
||||
keys = [
|
||||
request_fingerprint(expected, terrain, streams, mode=mode, **inputs)[0]
|
||||
for mode in PLAN_MODES
|
||||
]
|
||||
return hashlib.blake2b(
|
||||
repr((keys, float(corridor_m), ALGO)).encode(), digest_size=16
|
||||
).hexdigest()
|
||||
|
||||
|
||||
def analyze_segment(
|
||||
project_root: Path,
|
||||
seg: dict[str, Any],
|
||||
@@ -100,23 +145,8 @@ def analyze_segment(
|
||||
) -> dict[str, Any]:
|
||||
"""연도 구간 하나 · 갈래 넷 — 입력 지문이 저장본과 같으면 그대로(`stored` True)."""
|
||||
expected = [_point(seg["start"]), _point(seg["end"])]
|
||||
inputs = {
|
||||
"weights": None,
|
||||
"max_grade": criteria["max_grade"],
|
||||
"min_radius_m": criteria["min_radius_m"],
|
||||
"stream_offset_m": None,
|
||||
"grade_smooth_m": None,
|
||||
"target_grade": None,
|
||||
"with_profile": False,
|
||||
"design_speed": criteria["design_speed"],
|
||||
}
|
||||
keys = [
|
||||
request_fingerprint(expected, terrain, streams, mode=mode, **inputs)[0]
|
||||
for mode in PLAN_MODES
|
||||
]
|
||||
fingerprint = hashlib.blake2b(
|
||||
repr((keys, float(corridor_m), ALGO)).encode(), digest_size=16
|
||||
).hexdigest()
|
||||
inputs = _engine_inputs(criteria)
|
||||
fingerprint = segment_fingerprint(seg, terrain, streams, criteria, corridor_m)
|
||||
stored = read_segment(project_root, seg)
|
||||
if stored is not None and stored.get("fingerprint") == fingerprint:
|
||||
return {**stored, "stored": True}
|
||||
@@ -148,6 +178,60 @@ def analyze_segment(
|
||||
return {**result, "stored": False}
|
||||
|
||||
|
||||
def near_streams(terrain: Any, streams: list) -> list:
|
||||
"""지형 상자 안 어느 점에서든 가장 가까운 계류일 수 있는 선만 — 나머지는 엔진 값에 안 닿음.
|
||||
|
||||
칸마다 닿는 거리 = 칸 가운데 계류 거리 + 칸 반대각(거리는 1-립시츠) — 그 안에 드는 선만 남기면
|
||||
상자 안 점의 계류 거리 · 건너기 · 격자 거리가 통째와 같음(통째 거리 = 추천 시간의 큰 몫).
|
||||
"""
|
||||
if not streams:
|
||||
return streams
|
||||
x0, x1 = float(np.min(terrain.x)) - STREAM_PAD_M, float(np.max(terrain.x)) + STREAM_PAD_M
|
||||
y0, y1 = float(np.min(terrain.y)) - STREAM_PAD_M, float(np.max(terrain.y)) + STREAM_PAD_M
|
||||
xs = np.linspace(x0, x1, max(1, math.ceil((x1 - x0) / STREAM_TILE_M)) + 1)
|
||||
ys = np.linspace(y0, y1, max(1, math.ceil((y1 - y0) / STREAM_TILE_M)) + 1)
|
||||
lo_x, lo_y = np.meshgrid(xs[:-1], ys[:-1])
|
||||
hi_x, hi_y = np.meshgrid(xs[1:], ys[1:])
|
||||
tiles = shapely.box(lo_x.ravel(), lo_y.ravel(), hi_x.ravel(), hi_y.ravel())
|
||||
reach = shapely.distance(shapely.centroid(tiles), MultiLineString(streams)) + 0.5 * math.hypot(
|
||||
xs[1] - xs[0], ys[1] - ys[0]
|
||||
)
|
||||
lines = np.array([LineString(line) for line in streams], dtype=object)
|
||||
keep = (shapely.distance(lines[:, None], tiles[None, :]) <= reach[None, :]).any(axis=1)
|
||||
return [line for line, kept in zip(streams, keep) if kept]
|
||||
|
||||
|
||||
def _load(project_root: Path, year: int, loader: Loader) -> tuple[Any, list]:
|
||||
terrain, streams = loader(
|
||||
project_root,
|
||||
SELECTION,
|
||||
models_dir_of(project_root, year),
|
||||
derived_dir(project_root) / f"cost_{year}",
|
||||
)
|
||||
return terrain, near_streams(terrain, streams)
|
||||
|
||||
|
||||
def _segment_job(
|
||||
project_root: Path,
|
||||
seg: dict[str, Any],
|
||||
criteria: dict[str, Any],
|
||||
corridor_m: float,
|
||||
loader: Loader,
|
||||
) -> dict[str, Any]:
|
||||
"""연도 구간 하나 — 지형 읽기부터(제 프로세스면 끝날 때 비용면 · 묶음 메모리를 놓음)."""
|
||||
terrain, streams = _load(project_root, int(seg["year"]), loader)
|
||||
return analyze_segment(project_root, seg, terrain, streams, criteria, corridor_m)
|
||||
|
||||
|
||||
def _worker_job(*args: Any) -> dict[str, Any]:
|
||||
"""나란히 셈 프로세스 — 구간 하나 뒤 B 기억(비용면 · 묶음)을 비워 다음 구간 메모리를 놓음."""
|
||||
try:
|
||||
return _segment_job(*args)
|
||||
finally:
|
||||
_POOLS.clear()
|
||||
_TERRAINS.clear()
|
||||
|
||||
|
||||
def analyze_plan(
|
||||
project_root: Path,
|
||||
segments: list[dict[str, Any]],
|
||||
@@ -156,23 +240,52 @@ def analyze_plan(
|
||||
loader: Loader = load_terrain_and_streams,
|
||||
corridor_m: float = PLAN_CORRIDOR_M,
|
||||
on_progress: Progress | None = None,
|
||||
workers: int = PLAN_ANALYZE_WORKERS,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""연도 구간마다 지형(연도 DEM · 연도 비용면 캐시)을 읽어 `analyze_segment`."""
|
||||
results = []
|
||||
for i, seg in enumerate(segments):
|
||||
year = int(seg["year"])
|
||||
"""연도 구간마다(연도 DEM · 연도 비용면 캐시) `analyze_segment`.
|
||||
|
||||
다시 셀 구간이 둘 이상이면 프로세스 `workers` 개로 나란히 — 구간끼리 입력 · 저장본이 따로라
|
||||
결과 같음. `workers` 1 = 이 프로세스(가짜 지형 시험)."""
|
||||
|
||||
def report(percent: int, message: str) -> None:
|
||||
if on_progress is not None:
|
||||
on_progress(100 * i // len(segments), f"{year}년 구간 노선 추천 중")
|
||||
terrain, streams = loader(
|
||||
project_root,
|
||||
SELECTION,
|
||||
models_dir_of(project_root, year),
|
||||
derived_dir(project_root) / f"cost_{year}",
|
||||
)
|
||||
results.append(analyze_segment(project_root, seg, terrain, streams, criteria, corridor_m))
|
||||
if on_progress is not None:
|
||||
on_progress(100, "분석 끝")
|
||||
return results
|
||||
on_progress(percent, message)
|
||||
|
||||
results: list[dict[str, Any] | None] = [None] * len(segments)
|
||||
todo = []
|
||||
for i, seg in enumerate(segments):
|
||||
stored = read_segment(project_root, seg)
|
||||
if stored is not None: # 저장본이 있을 때만 이 프로세스에서 지형을 읽어 지문을 봄
|
||||
terrain, streams = _load(project_root, int(seg["year"]), loader)
|
||||
if stored.get("fingerprint") == segment_fingerprint(
|
||||
seg, terrain, streams, criteria, corridor_m
|
||||
):
|
||||
results[i] = {**stored, "stored": True}
|
||||
continue
|
||||
todo.append(i)
|
||||
|
||||
count = min(workers, len(todo))
|
||||
if count <= 1:
|
||||
for n, i in enumerate(todo):
|
||||
report(100 * n // len(segments), f"{int(segments[i]['year'])}년 구간 노선 추천 중")
|
||||
results[i] = _segment_job(project_root, segments[i], criteria, corridor_m, loader)
|
||||
else:
|
||||
years = "·".join(str(int(segments[i]["year"])) for i in todo)
|
||||
report(0, f"{years}년 구간 노선 추천 중(나란히 {count})")
|
||||
context = multiprocessing.get_context("spawn")
|
||||
with ProcessPoolExecutor(count, mp_context=context) as pool:
|
||||
futures = {
|
||||
pool.submit(_worker_job, project_root, segments[i], criteria, corridor_m, loader): i
|
||||
for i in todo
|
||||
}
|
||||
for n, future in enumerate(as_completed(futures), 1):
|
||||
i = futures[future]
|
||||
results[i] = future.result()
|
||||
report(
|
||||
100 * n // len(todo), f"{int(segments[i]['year'])}년 구간 끝 ({n}/{len(todo)})"
|
||||
)
|
||||
report(100, "분석 끝")
|
||||
return results # type: ignore[return-value]
|
||||
|
||||
|
||||
def pick_mode(result: dict[str, Any], wanted: str | None) -> str | None:
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
"""E03 2D 확률 해석 · 민감도 (요구 351 · PLAN 537-1) — Slide2 · SLOPE/W Probabilistic 과 같은 생각.
|
||||
|
||||
확률 변수 = 물성(c · φ · γ) · 지하수위(수위선 위아래 이동 m) · r_u — 정규 · 대수정규 · 절단(평균 아래 · 위
|
||||
거리 = Slide2 Relative Min · Max · 물리 한계 c ≥ 0 · 0 ≤ φ < 89 · γ > 0 · 0 ≤ r_u < 1 도 절단) · 변수 사이 상관
|
||||
(Slide2 c–φ Correlation · 표준정규에서 촐레스키) → 몬테카를로 · 라틴 하이퍼큐브 N 회
|
||||
→ 안전율 표본 → 평균 · 표준편차 · Pf = P(F < 1) · β 정규 (μ − 1)/σ · β 대수정규
|
||||
ln(μ/√(1+V²))/√ln(1+V²) · 히스토그램 · 누적분포 · P(F < 기준안전율).
|
||||
갈래: fixed = 결정론 임계면 고정(절편 기하 한 번 · 물성만 바꿔 묶음 평가 · 빠름 · Slide2 Global Minimum) ·
|
||||
search = 표본마다 임계면 다시 찾음(`Engine_Search.analyze` · 느림 · 프로세스 병렬 · Slide2 Overall Slope).
|
||||
민감도 = 변수 하나씩 범위(절단 끝 · 없으면 ±3σ) 50 등분 훑기(나머지 평균 · 임계면 고정 · Slide2 Sensitivity 와 같음)
|
||||
· 토네이도 = 끝값 F 폭 순.
|
||||
γ 변수 = γt 표본 · γsat 는 같은 차만큼 옮김(γsat − γt 그대로).
|
||||
search 일꾼 = `python -m E03_StabSection.E03_StabSection_Engine_Prob <job.json>`(서버 프로세스에서 풀을 열지 않음 ·
|
||||
`Worker` 머리글) · 한 줄마다 {"done": n} · 끝에 job["out"] 파일.
|
||||
NumPy · SciPy(stats) 만 · 계산 자리 = ③ 서버 단독.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from scipy.stats import norm
|
||||
|
||||
from E03_StabSection import E03_StabSection_Engine_LEM as lem
|
||||
from E03_StabSection.E03_StabSection_Engine_Search import analyze
|
||||
from E03_StabSection.E03_StabSection_Engine_Station import MIN_DEPTH_M
|
||||
from E03_StabSection.E03_StabSection_Engine_Surface import (
|
||||
circle_slices,
|
||||
evaluate,
|
||||
poly_slices,
|
||||
three_point_center,
|
||||
)
|
||||
|
||||
PROPS = ("c", "phi", "gamma", "water", "ru")
|
||||
#: 물리 한계(절단) — 지하수 이동은 한계 없음
|
||||
BOUNDS = {"c": (0.0, math.inf), "phi": (0.0, 89.0), "gamma": (1e-3, math.inf), "ru": (0.0, 0.99)}
|
||||
#: 절단 없을 때 민감도 훑기 범위(±σ 배)
|
||||
SWEEP_SIGMA = 3.0
|
||||
SWEEP_POINTS = 51
|
||||
HIST_BINS = 30
|
||||
CDF_POINTS = 101
|
||||
N_SLICES = 50
|
||||
|
||||
|
||||
# ---- 분포 · 표본 --------------------------------------------------------------
|
||||
def _bounds(v: dict) -> tuple[float, float]:
|
||||
"""절단 구간 [a, b] — 평균 ∓ 아래 · 위 거리 ∩ 물리 한계."""
|
||||
lo_phys, hi_phys = BOUNDS.get(v["prop"], (-math.inf, math.inf))
|
||||
mean = float(v["mean"])
|
||||
a = mean - float(v["lo"]) if v.get("lo") is not None else -math.inf
|
||||
b = mean + float(v["hi"]) if v.get("hi") is not None else math.inf
|
||||
return max(a, lo_phys), min(b, hi_phys)
|
||||
|
||||
|
||||
def _lognormal(v: dict) -> tuple[float, float]:
|
||||
"""대수정규 (μ_ln, σ_ln) — 평균 · 표준편차 그대로 맞춤."""
|
||||
mean, std = float(v["mean"]), float(v["std"])
|
||||
s2 = math.log1p((std / mean) ** 2)
|
||||
return math.log(mean) - 0.5 * s2, math.sqrt(s2)
|
||||
|
||||
|
||||
def ppf(v: dict, u: np.ndarray) -> np.ndarray:
|
||||
"""균등 u(0~1) → 변수 값(절단 분포 역함수)."""
|
||||
u = np.asarray(u, float)
|
||||
mean, std = float(v["mean"]), float(v.get("std") or 0.0)
|
||||
if std <= 0:
|
||||
return np.full(u.shape, mean)
|
||||
a, b = _bounds(v)
|
||||
if v.get("dist") == "lognormal" and v["prop"] != "water":
|
||||
if mean <= 0:
|
||||
raise ValueError(f"대수정규는 평균 > 0: {v}")
|
||||
mu, s = _lognormal(v)
|
||||
za = (math.log(a) - mu) / s if a > 0 else -math.inf
|
||||
zb = (math.log(b) - mu) / s if b < math.inf else math.inf
|
||||
pa, pb = norm.cdf(za), norm.cdf(zb)
|
||||
return np.exp(mu + s * norm.ppf(pa + u * (pb - pa)))
|
||||
pa, pb = norm.cdf((a - mean) / std), norm.cdf((b - mean) / std)
|
||||
return mean + std * norm.ppf(pa + u * (pb - pa))
|
||||
|
||||
|
||||
def sample(
|
||||
variables: list[dict],
|
||||
n: int,
|
||||
*,
|
||||
sampling: str = "lhs",
|
||||
seed: int = 0,
|
||||
correlations: list | None = None,
|
||||
) -> np.ndarray:
|
||||
"""표본 (n, 변수 수) — lhs = 라틴 하이퍼큐브(변수마다 n 칸 하나씩 · 칸 차례 섞음) · mc = 단순 무작위.
|
||||
|
||||
correlations = [(i, j, ρ), …] — 표준정규 칸에서 촐레스키로 섞은 뒤 분포로(LHS 층은 섞인 변수만 흐려짐)."""
|
||||
rng = np.random.default_rng(int(seed))
|
||||
k = len(variables)
|
||||
if sampling == "lhs":
|
||||
order = np.argsort(rng.random((n, k)), axis=0)
|
||||
u = (order + rng.random((n, k))) / n
|
||||
else:
|
||||
u = rng.random((n, k))
|
||||
u = np.clip(u, 1e-12, 1 - 1e-12)
|
||||
if correlations:
|
||||
corr = np.eye(k)
|
||||
for i, j, rho in correlations:
|
||||
corr[i, j] = corr[j, i] = float(rho)
|
||||
u = norm.cdf(norm.ppf(u) @ np.linalg.cholesky(corr).T)
|
||||
u = np.clip(u, 1e-12, 1 - 1e-12)
|
||||
return np.column_stack([ppf(v, u[:, j]) for j, v in enumerate(variables)]) if k else u
|
||||
|
||||
|
||||
def sweep_range(v: dict) -> tuple[float, float]:
|
||||
"""민감도 훑기 범위 — 절단 끝 · 없으면 평균 ±3σ(물리 한계 안)."""
|
||||
mean, std = float(v["mean"]), float(v.get("std") or 0.0)
|
||||
a, b = _bounds(v)
|
||||
lo = a if math.isfinite(a) and v.get("lo") is not None else mean - SWEEP_SIGMA * std
|
||||
hi = b if math.isfinite(b) and v.get("hi") is not None else mean + SWEEP_SIGMA * std
|
||||
lo_phys, hi_phys = BOUNDS.get(v["prop"], (-math.inf, math.inf))
|
||||
return max(lo, lo_phys), min(hi, hi_phys)
|
||||
|
||||
|
||||
# ---- 통계 -------------------------------------------------------------------
|
||||
def statistics(F: np.ndarray, required: float | None = None, bins: int = HIST_BINS) -> dict:
|
||||
"""안전율 표본 → 평균 · 표준편차 · Pf · β 정규 · 대수정규 · 히스토그램 · 누적분포(해 없는 표본 = 셈 밖)."""
|
||||
F = np.asarray(F, float)
|
||||
ok = F[np.isfinite(F)]
|
||||
out = {"n": int(len(F)), "n_valid": int(len(ok))}
|
||||
if len(ok) < 2:
|
||||
return {**out, "mean": None, "std": None, "pf": None, "beta_normal": None}
|
||||
mean, std = float(ok.mean()), float(ok.std(ddof=1))
|
||||
V = std / mean if mean > 0 else math.nan
|
||||
beta_ln = (
|
||||
math.log(mean / math.sqrt(1 + V * V)) / math.sqrt(math.log1p(V * V))
|
||||
if mean > 0 and V > 0
|
||||
else None
|
||||
)
|
||||
counts, edges = np.histogram(ok, bins=bins)
|
||||
p = np.linspace(0.0, 1.0, CDF_POINTS)
|
||||
out.update(
|
||||
mean=mean,
|
||||
std=std,
|
||||
cov=V if math.isfinite(V) else None,
|
||||
min=float(ok.min()),
|
||||
max=float(ok.max()),
|
||||
pf=float(np.mean(ok < 1.0)),
|
||||
n_fail=int(np.sum(ok < 1.0)),
|
||||
beta_normal=(mean - 1.0) / std if std > 0 else None,
|
||||
beta_lognormal=beta_ln,
|
||||
required=required,
|
||||
p_below_required=float(np.mean(ok < required)) if required else None,
|
||||
histogram={"edges": edges.tolist(), "counts": counts.tolist()},
|
||||
cdf={"p": p.tolist(), "F": np.quantile(ok, p).tolist()},
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ---- 변수 → 모형 ---------------------------------------------------------------
|
||||
def perturb(model: dict, variables: list[dict], row) -> dict:
|
||||
"""모형 사본 — 표본 한 줄을 물성 · 지하수위에 넣음(search 갈래)."""
|
||||
out = dict(model)
|
||||
mats = {m["id"]: dict(m) for m in model["materials"]}
|
||||
dz = 0.0
|
||||
for v, x in zip(variables, row):
|
||||
if v["prop"] == "water":
|
||||
dz += float(x)
|
||||
continue
|
||||
if v["prop"] == "ru":
|
||||
gw = model.get("groundwater") or {}
|
||||
if gw.get("mode") == "ru":
|
||||
out["groundwater"] = {**gw, "ru": float(x)}
|
||||
continue
|
||||
m = mats[v["material"]]
|
||||
if v["prop"] == "gamma":
|
||||
gt = float(m["gamma_t"])
|
||||
m["gamma_sat"] = float(m.get("gamma_sat", gt)) + float(x) - gt
|
||||
m["gamma_t"] = float(x)
|
||||
else:
|
||||
m[v["prop"]] = float(x)
|
||||
out["materials"] = list(mats.values())
|
||||
gw = out.get("groundwater") or {}
|
||||
if dz and gw.get("mode") == "line":
|
||||
out["groundwater"] = {**gw, "line": [[x, z + dz] for x, z in gw["line"]]}
|
||||
return out
|
||||
|
||||
|
||||
class Fixed:
|
||||
"""임계면 고정 — 모형 · 면 하나를 한 번 준비해 표본마다 물성 칸만 갈아 끼우고 절편 묶음으로 평가."""
|
||||
|
||||
def __init__(self, model: dict, surface: dict, direction: int, n_slices: int = N_SLICES):
|
||||
self.prep = lem.Prepared(model, direction)
|
||||
self.n = n_slices
|
||||
s = self.prep.direction
|
||||
self.index = {m["id"]: i for i, m in enumerate(self.prep.materials)}
|
||||
if surface["kind"] == "circle":
|
||||
xs = sorted((s * surface["entry"][0], s * surface["exit"][0]))
|
||||
self.geom = (
|
||||
"circle",
|
||||
s * surface["center"][0],
|
||||
surface["center"][1],
|
||||
surface["radius"],
|
||||
)
|
||||
self.geom += tuple(xs)
|
||||
else:
|
||||
pts = sorted((s * x, z) for x, z in surface["points"])
|
||||
self.geom = ("polyline", np.array([p[0] for p in pts]), np.array([p[1] for p in pts]))
|
||||
|
||||
def _prep(self, variables: list[dict], row) -> lem.Prepared:
|
||||
base = self.prep
|
||||
p = copy.copy(base)
|
||||
gt, gs = base.gamma_t.copy(), base.gamma_sat.copy()
|
||||
c, tp = base.c.copy(), base.tanphi.copy()
|
||||
dz = 0.0
|
||||
for v, x in zip(variables, row):
|
||||
if v["prop"] == "water":
|
||||
dz += float(x)
|
||||
continue
|
||||
if v["prop"] == "ru":
|
||||
if base.gw_mode == "ru":
|
||||
p.ru = float(x)
|
||||
continue
|
||||
i = self.index[v["material"]]
|
||||
if v["prop"] == "c":
|
||||
c[i] = x
|
||||
elif v["prop"] == "phi":
|
||||
tp[i] = math.tan(math.radians(x))
|
||||
else:
|
||||
gs[i] += x - gt[i]
|
||||
gt[i] = x
|
||||
p.gamma_t, p.gamma_sat, p.c, p.tanphi = gt, gs, c, tp
|
||||
p.table = base.table.copy()
|
||||
p.table[:, 4, :] = gt[base.mat]
|
||||
p.table[:, 5, :] = gs[base.mat] - gt[base.mat]
|
||||
if dz and base.gw_mode == "line":
|
||||
p.water_z = base.water_z + dz
|
||||
return p
|
||||
|
||||
def _slices(self, prep: lem.Prepared) -> tuple[dict, np.ndarray]:
|
||||
g = self.geom
|
||||
if g[0] == "circle":
|
||||
_, xc, zc, R, xE, xX = g
|
||||
sl = circle_slices(prep, [xc], [zc], [R], [xE], [xX], self.n)
|
||||
return sl, np.array([[xc, zc]])
|
||||
vx, vz = g[1][None, :], g[2][None, :]
|
||||
return poly_slices(prep, vx, vz, self.n), three_point_center(vx, vz)
|
||||
|
||||
def F(self, variables: list[dict], X: np.ndarray, method: str) -> np.ndarray:
|
||||
"""표본 줄마다 안전율(해 없음 = nan)."""
|
||||
X = np.atleast_2d(np.asarray(X, float))
|
||||
parts = [self._slices(self._prep(variables, row)) for row in X]
|
||||
sl = _stack([p[0] for p in parts])
|
||||
center = np.vstack([p[1] for p in parts])
|
||||
mp_seed = None
|
||||
if method == "morgenstern_price": # describe 와 같은 출발 참고값(같은 면 Spencer)
|
||||
mp_seed = evaluate(sl, center, ("spencer",))["spencer"]["F"]
|
||||
sl.pop("_pre", None)
|
||||
F = evaluate(sl, center, (method,), mp_seed=mp_seed)[method]["F"]
|
||||
return np.where(sl["valid"], F, np.nan)
|
||||
|
||||
|
||||
def _stack(parts: list[dict]) -> dict:
|
||||
"""한 줄짜리 절편 묶음들 → (S, …) 하나(배열 칸만 잇고 스칼라는 그대로)."""
|
||||
out = {}
|
||||
for key, value in parts[0].items():
|
||||
if key == "_pre":
|
||||
continue
|
||||
if isinstance(value, np.ndarray) and value.ndim >= 1 and value.shape[0] == 1:
|
||||
out[key] = np.concatenate([p[key] for p in parts])
|
||||
else:
|
||||
out[key] = value
|
||||
return out
|
||||
|
||||
|
||||
def _chunks(n: int, size: int):
|
||||
for i in range(0, n, size):
|
||||
yield slice(i, min(i + size, n))
|
||||
|
||||
|
||||
def sensitivity(
|
||||
fixed: Fixed, variables: list[dict], method: str, points: int = SWEEP_POINTS
|
||||
) -> dict:
|
||||
"""변수 하나씩 범위 훑기(나머지 평균) · 토네이도 = 끝값 F 폭 큰 순."""
|
||||
means = np.array([float(v["mean"]) for v in variables])
|
||||
rows, spans = [], []
|
||||
for j, v in enumerate(variables):
|
||||
lo, hi = sweep_range(v)
|
||||
xs = np.linspace(lo, hi, points) if hi > lo else np.full(points, means[j])
|
||||
block = np.tile(means, (points, 1))
|
||||
block[:, j] = xs
|
||||
rows.append(block)
|
||||
spans.append(xs)
|
||||
F0 = float(fixed.F(variables, means[None, :], method)[0])
|
||||
if not rows:
|
||||
return {"F_mean": F0, "curves": [], "tornado": []}
|
||||
F = fixed.F(variables, np.vstack(rows), method).reshape(len(variables), points)
|
||||
num = lambda a: [float(x) if np.isfinite(x) else None for x in a] # noqa: E731
|
||||
curves = []
|
||||
for v, xs, f in zip(variables, spans, F):
|
||||
ends = [f[0], f[-1]]
|
||||
swing = abs(ends[1] - ends[0]) if all(np.isfinite(ends)) else None
|
||||
curves.append(
|
||||
{
|
||||
"name": v["name"],
|
||||
"x": num(xs),
|
||||
"F": num(f),
|
||||
"F_lo": num([ends[0]])[0],
|
||||
"F_hi": num([ends[1]])[0],
|
||||
"swing": swing,
|
||||
}
|
||||
)
|
||||
tornado = sorted(
|
||||
(c for c in curves if c["swing"] is not None), key=lambda c: c["swing"], reverse=True
|
||||
)
|
||||
return {"F_mean": F0 if math.isfinite(F0) else None, "curves": curves, "tornado": [
|
||||
{"name": c["name"], "F_lo": c["F_lo"], "F_hi": c["F_hi"], "swing": c["swing"]}
|
||||
for c in tornado
|
||||
]} # fmt: skip
|
||||
|
||||
|
||||
# ---- 매회 탐색(search) ------------------------------------------------------------
|
||||
_CTX: dict = {}
|
||||
|
||||
|
||||
def _init(ctx: dict) -> None:
|
||||
_CTX.clear()
|
||||
_CTX.update(ctx)
|
||||
|
||||
|
||||
def _search_one(row) -> float:
|
||||
c = _CTX
|
||||
m = c["method"]
|
||||
methods = tuple(dict.fromkeys(("bishop", m)))
|
||||
res = analyze(
|
||||
perturb(c["model"], c["variables"], row),
|
||||
direction=c["direction"],
|
||||
limits=c["limits"],
|
||||
methods=methods,
|
||||
noncircular=m not in ("ordinary", "bishop"), # 비원호 Fellenius · Bishop = 참고 칸
|
||||
min_depth=MIN_DEPTH_M,
|
||||
)
|
||||
best = res["methods"][m]["min"]
|
||||
return float(best["F"]) if best and best.get("F") is not None else math.nan
|
||||
|
||||
|
||||
def search_F(ctx: dict, X: np.ndarray, workers: int = 1, emit=None) -> np.ndarray:
|
||||
"""표본마다 임계면 다시 찾음 — workers > 1 = 프로세스 풀(서버 밖 일꾼에서만)."""
|
||||
out = np.full(len(X), np.nan)
|
||||
if workers <= 1:
|
||||
_init(ctx)
|
||||
for i, row in enumerate(X):
|
||||
out[i] = _search_one(row)
|
||||
emit and emit(i + 1)
|
||||
return out
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
|
||||
with ProcessPoolExecutor(max_workers=workers, initializer=_init, initargs=(ctx,)) as pool:
|
||||
futures = {pool.submit(_search_one, row): i for i, row in enumerate(X)}
|
||||
for done, future in enumerate(as_completed(futures), 1):
|
||||
out[futures[future]] = future.result()
|
||||
emit and emit(done)
|
||||
return out
|
||||
|
||||
|
||||
# ---- 한 번 해석 ---------------------------------------------------------------
|
||||
def run(
|
||||
model: dict,
|
||||
*,
|
||||
surface: dict,
|
||||
direction: int,
|
||||
limits=None,
|
||||
variables: list[dict],
|
||||
correlations: list | None = None,
|
||||
n: int = 1000,
|
||||
sampling: str = "lhs",
|
||||
seed: int = 0,
|
||||
mode: str = "fixed",
|
||||
method: str = "bishop",
|
||||
required: float | None = None,
|
||||
workers: int = 1,
|
||||
emit=None,
|
||||
) -> dict:
|
||||
"""확률 해석 + 민감도 한 벌. emit(끝난 표본 수) = 진행률."""
|
||||
t0 = time.perf_counter()
|
||||
X = sample(variables, n, sampling=sampling, seed=seed, correlations=correlations)
|
||||
fixed = Fixed(model, surface, direction)
|
||||
t1 = time.perf_counter()
|
||||
if mode == "search":
|
||||
ctx = {
|
||||
"model": model,
|
||||
"variables": variables,
|
||||
"direction": direction,
|
||||
"limits": limits,
|
||||
"method": method,
|
||||
}
|
||||
F = search_F(ctx, X, workers, emit)
|
||||
else:
|
||||
F = np.full(n, np.nan)
|
||||
for part in _chunks(n, 250):
|
||||
F[part] = fixed.F(variables, X[part], method)
|
||||
emit and emit(part.stop)
|
||||
t2 = time.perf_counter()
|
||||
sens = sensitivity(fixed, variables, method)
|
||||
t3 = time.perf_counter()
|
||||
realized = [
|
||||
{"name": v["name"], "mean": float(X[:, j].mean()), "std": float(X[:, j].std(ddof=1))}
|
||||
for j, v in enumerate(variables)
|
||||
]
|
||||
return {
|
||||
"mode": mode,
|
||||
"method": method,
|
||||
"n": n,
|
||||
"sampling": sampling,
|
||||
"seed": seed,
|
||||
"variables": variables,
|
||||
"correlations": correlations or [],
|
||||
"realized": realized,
|
||||
"stats": statistics(F, required),
|
||||
"F": [round(float(f), 4) if np.isfinite(f) else None for f in F],
|
||||
"sensitivity": sens,
|
||||
"elapsed_s": {
|
||||
"prepare": round(t1 - t0, 3),
|
||||
"samples": round(t2 - t1, 3),
|
||||
"sensitivity": round(t3 - t2, 3),
|
||||
"total": round(t3 - t0, 3),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""일꾼 — job = run() 인자 + out(결과 파일) · 한 줄마다 진행."""
|
||||
path = Path(sys.argv[1])
|
||||
job = json.loads(path.read_text(encoding="utf-8"))
|
||||
out = Path(job.pop("out"))
|
||||
result = run(**job, emit=lambda k: print(json.dumps({"done": k}), flush=True))
|
||||
out.write_text(json.dumps(result, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,400 @@
|
||||
"""E03 확률 해석 · 민감도 API (요구 351 · PLAN 537-1) — 대표 단면만.
|
||||
|
||||
- `GET /api/projects/{id}/stab/prob/setup/{station_key}?side=&condition=` — 그 단면 지층 물성(E02 값 = 평균) ·
|
||||
기본값 후보(`prob_defaults.json` · 근거 · 사용자 확정 전) · 지하수 갈래 · 기준안전율 · 마지막 결과
|
||||
- `POST …/stab/prob/{station_key}` — 분포 · 표본 수 · 갈래 → 뒤에서(진행 `GET …/stab/prob/progress`) ·
|
||||
같은 지문 = 저장본(fresh)
|
||||
- `GET …/stab/prob` — 지금 2D 결과에 붙은 기록 목록(측점 · 비탈 · 조건마다 가장 최근 · E05 「확률해석」 시트)
|
||||
|
||||
평균 = E02 지반 조건(작업본) 값 · 바꾸려면 E02 에서. 임계면 = 지금 2D 결과의 그 방법 최소 면.
|
||||
결과 = 파생 저장본 `_derived/stab2d_prob/<지문>` · 지문 = 원본 측점 지문 + 요청 + 코드.
|
||||
엔진 = `Engine_Prob` — 임계면 고정 = 서버 스레드(빠름) · 매회 탐색 = 일꾼 프로세스(풀 · 느림).
|
||||
계산 자리 = ③ 서버 단독.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from common_util.common_util_derived_store import (
|
||||
derived_dir,
|
||||
derived_key,
|
||||
read_derived,
|
||||
write_derived,
|
||||
)
|
||||
from E02_StabGround import E02_StabGround_Router as e02
|
||||
from E03_StabSection import E03_StabSection_Engine_Prob as prob
|
||||
from E03_StabSection import E03_StabSection_Router as e03
|
||||
from E03_StabSection import E03_StabSection_Router_WhatIf as whatif
|
||||
from E03_StabSection import E03_StabSection_View as view
|
||||
from E03_StabSection.E03_StabSection_Engine_LEM import WALL_ID
|
||||
from E03_StabSection.E03_StabSection_Engine_Model import section_model_from_b06
|
||||
from E03_StabSection.E03_StabSection_Types import groundwater as norm_groundwater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["E03 Stab Probabilistic"])
|
||||
KIND = "stab2d_prob"
|
||||
KEEP = 200
|
||||
DEFAULTS_PATH = (
|
||||
Path(__file__).resolve().parents[1] / "resources/master_template/stability/prob_defaults.json"
|
||||
)
|
||||
#: 매회 탐색 표본 수 상한(측점 하나 탐색 수 초 × N)
|
||||
SEARCH_MAX_N = 2000
|
||||
#: 일꾼 프로세스 수 — None = 코어 − 2 · 1 = 서버 안에서 차례로(시험)
|
||||
WORKERS: int | None = None
|
||||
|
||||
_JOBS: dict[str, dict[str, Any]] = {}
|
||||
_TASKS: set[asyncio.Task] = set()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def code_hash() -> str:
|
||||
digest = hashlib.sha1(e03.engine_hash().encode())
|
||||
for name in ("E03_StabSection_Engine_Prob.py", Path(__file__).name):
|
||||
digest.update((Path(__file__).parent / name).read_bytes())
|
||||
return digest.hexdigest()[:12]
|
||||
|
||||
|
||||
def load_defaults() -> dict[str, Any]:
|
||||
return json.loads(DEFAULTS_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class VarIn(BaseModel):
|
||||
"""확률 변수 하나 — c · φ · γ = 변동계수 · 지하수위 이동(m) · r_u = 표준편차 · 절단 = 평균 ± k·σ."""
|
||||
|
||||
material: str | None = None
|
||||
prop: Literal["c", "phi", "gamma", "water", "ru"]
|
||||
dist: Literal["normal", "lognormal"] = "normal"
|
||||
cov: float | None = Field(None, ge=0.0, le=2.0)
|
||||
std: float | None = Field(None, ge=0.0, le=20.0)
|
||||
trunc_sigma: float | None = Field(3.0, gt=0.0, le=10.0)
|
||||
|
||||
|
||||
class ProbRequest(BaseModel):
|
||||
side: Literal["left", "right"]
|
||||
condition: Literal["dry", "wet", "seismic"]
|
||||
method: Literal["ordinary", "bishop", "janbu", "spencer", "morgenstern_price"] = "bishop"
|
||||
mode: Literal["fixed", "search"] = "fixed"
|
||||
n: int = Field(1000, ge=50, le=20000)
|
||||
sampling: Literal["lhs", "mc"] = "lhs"
|
||||
seed: int = Field(0, ge=0, le=2**31 - 1)
|
||||
variables: list[VarIn] = Field(default_factory=list, max_length=40)
|
||||
rho_c_phi: float = Field(0.0, ge=-0.95, le=0.95)
|
||||
|
||||
|
||||
def _model(ctx: dict[str, Any], condition: str) -> dict[str, Any]:
|
||||
"""그 조건 모형 — `analyze_station` 과 같은 입력(지층 · 지반선 · 벽 · 하중 · E02 지하수 갈래)."""
|
||||
kw = ctx["kw"]
|
||||
model = section_model_from_b06(
|
||||
ctx["cross"],
|
||||
ctx["design"],
|
||||
materials=kw["materials"],
|
||||
layers=kw["layers"],
|
||||
ground=kw["ground"],
|
||||
wet=condition == "wet",
|
||||
seismic_kh=kw["seismic_kh"] if condition == "seismic" else 0.0,
|
||||
road_surcharge_kpa=kw["road_surcharge_kpa"],
|
||||
walls=kw.get("walls"),
|
||||
)
|
||||
model["groundwater"] = norm_groundwater(kw["groundwater_for"](condition, model))
|
||||
return model
|
||||
|
||||
|
||||
def _slot(root: Path, station_key: str, side: str, condition: str, method: str) -> dict:
|
||||
"""측점 · 비탈 · 조건 — 대표 단면만 · 모형 · 임계면 · 기준안전율."""
|
||||
ctx = whatif._context(root, station_key)
|
||||
rows = [dict(r) for r in e03._latest_summary(root, ctx["plan"]["fp"])[0]["stations"]]
|
||||
view.mark_representative(rows, e03._overrides(root))
|
||||
mark = next((r.get("representative") or {} for r in rows if r["key"] == station_key), {})
|
||||
if not mark.get("on"):
|
||||
raise HTTPException(
|
||||
status_code=422, detail="대표 단면만 확률 해석합니다 — 먼저 대표 단면으로."
|
||||
)
|
||||
if condition not in ctx["kw"]["conditions"]:
|
||||
raise HTTPException(status_code=422, detail="그 조건은 해석하지 않았습니다.")
|
||||
case = next(
|
||||
(
|
||||
c
|
||||
for c in ctx["value"]["out"]["cases"]
|
||||
if c["condition"] == condition and c["side"] == side
|
||||
),
|
||||
None,
|
||||
)
|
||||
if case is None:
|
||||
raise HTTPException(status_code=422, detail="그 쪽 비탈이 없습니다.")
|
||||
methods = case["result"]["methods"]
|
||||
surface = (methods.get(method) or {}).get("min") or None
|
||||
model = _model(ctx, condition)
|
||||
sd = next(s for s in model["sides"] if s["side"] == side)
|
||||
return {
|
||||
"ctx": ctx,
|
||||
"model": model,
|
||||
"side": sd,
|
||||
"role": case["role"],
|
||||
"surface": surface,
|
||||
"methods": methods,
|
||||
"required": view.required_factor(ctx["plan"]["factors"], condition, case["role"]),
|
||||
}
|
||||
|
||||
|
||||
def _materials(model: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""단면에 쓰인 물성(벽 뺌 · 지층 차례)."""
|
||||
used = list(dict.fromkeys(r["material_id"] for r in model["regions"]))
|
||||
by = {m["id"]: m for m in model["materials"]}
|
||||
return [by[i] for i in used if i != WALL_ID and i in by]
|
||||
|
||||
|
||||
def _variables(req: ProbRequest, model: dict[str, Any]) -> tuple[list[dict], list]:
|
||||
"""요청 → 엔진 변수(평균 = 모형 값 · σ 0 은 뺌) · c–φ 상관 짝."""
|
||||
mats = {m["id"]: m for m in _materials(model)}
|
||||
gw = model.get("groundwater") or {}
|
||||
out = []
|
||||
for v in req.variables:
|
||||
if v.prop in ("water", "ru"):
|
||||
need = "line" if v.prop == "water" else "ru"
|
||||
if gw.get("mode") != need:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail=f"{'지하수위' if v.prop == 'water' else 'r_u'} 변수는 그 조건 지하수 갈래가 "
|
||||
f"{'수위선' if need == 'line' else 'r_u'} 일 때만.",
|
||||
)
|
||||
mean, std = (0.0, v.std) if v.prop == "water" else (float(gw["ru"]), v.std)
|
||||
name = "지하수위" if v.prop == "water" else "r_u"
|
||||
else:
|
||||
m = mats.get(v.material or "")
|
||||
if m is None:
|
||||
raise HTTPException(status_code=422, detail=f"단면에 없는 물성: {v.material}")
|
||||
mean = float(m["gamma_t"] if v.prop == "gamma" else m[v.prop])
|
||||
std = mean * float(v.cov) if v.cov is not None else v.std
|
||||
name = f"{m.get('name') or m['id']} {dict(c='c', phi='φ', gamma='γ')[v.prop]}"
|
||||
if not std:
|
||||
continue
|
||||
if v.dist == "lognormal" and (mean <= 0 or v.prop in ("water",)):
|
||||
raise HTTPException(status_code=422, detail=f"대수정규는 평균 > 0 만: {name}")
|
||||
k = v.trunc_sigma
|
||||
out.append(
|
||||
{
|
||||
"name": name,
|
||||
"material": v.material if v.prop not in ("water", "ru") else None,
|
||||
"prop": v.prop,
|
||||
"dist": v.dist,
|
||||
"mean": mean,
|
||||
"std": float(std),
|
||||
"lo": None if k is None else k * float(std),
|
||||
"hi": None if k is None else k * float(std),
|
||||
}
|
||||
)
|
||||
pairs = []
|
||||
if req.rho_c_phi:
|
||||
for i, a in enumerate(out):
|
||||
for j, b in enumerate(out):
|
||||
if a["prop"] == "c" and b["prop"] == "phi" and a["material"] == b["material"]:
|
||||
pairs.append((i, j, req.rho_c_phi))
|
||||
return out, pairs
|
||||
|
||||
|
||||
def _run_search(job: dict[str, Any], emit: Any) -> dict[str, Any]:
|
||||
"""매회 탐색 일꾼(`python -m …Engine_Prob job.json`) — 한 줄마다 emit · 실패면 RuntimeError."""
|
||||
with tempfile.TemporaryDirectory(prefix="aislo_e03p_") as folder:
|
||||
path, out = Path(folder) / "job.json", Path(folder) / "out.json"
|
||||
path.write_text(json.dumps({**job, "out": str(out)}, ensure_ascii=False), encoding="utf-8")
|
||||
log = Path(folder) / "stderr.txt"
|
||||
with log.open("w", encoding="utf-8") as errors:
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, "-m", "E03_StabSection.E03_StabSection_Engine_Prob", str(path)],
|
||||
cwd=e03.PROJECT_ROOT,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=errors,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
for line in process.stdout:
|
||||
if line.startswith("{"):
|
||||
emit(json.loads(line).get("done"))
|
||||
code = process.wait()
|
||||
if code != 0:
|
||||
tail = log.read_text(encoding="utf-8", errors="replace").strip().splitlines()[-1:]
|
||||
raise RuntimeError(f"확률 해석 일꾼 실패: {tail}")
|
||||
return json.loads(out.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def run_prob(root: Path, station_key: str, req: ProbRequest, report: Any) -> dict[str, Any]:
|
||||
slot = _slot(root, station_key, req.side, req.condition, req.method)
|
||||
if slot["surface"] is None:
|
||||
raise HTTPException(status_code=422, detail="그 방법 임계면이 없습니다(해 없음).")
|
||||
if req.mode == "search" and req.n > SEARCH_MAX_N:
|
||||
raise HTTPException(status_code=422, detail=f"매회 탐색은 표본 {SEARCH_MAX_N} 회까지.")
|
||||
variables, pairs = _variables(req, slot["model"])
|
||||
if not variables:
|
||||
raise HTTPException(status_code=422, detail="확률 변수가 없습니다(변동계수 · 표준편차 0).")
|
||||
row = slot["ctx"]["row"]
|
||||
key = derived_key(code_hash(), row["fp"], req.model_dump())
|
||||
cached = read_derived(root, KIND, key)
|
||||
if cached:
|
||||
return cached["value"]
|
||||
job = {
|
||||
"model": slot["model"],
|
||||
"surface": slot["surface"],
|
||||
"direction": slot["side"]["direction"],
|
||||
"limits": list(slot["side"]["limits"]),
|
||||
"variables": variables,
|
||||
"correlations": pairs,
|
||||
"n": req.n,
|
||||
"sampling": req.sampling,
|
||||
"seed": req.seed,
|
||||
"mode": req.mode,
|
||||
"method": req.method,
|
||||
"required": slot["required"],
|
||||
}
|
||||
|
||||
def emit(done: int | None) -> None:
|
||||
if done:
|
||||
report(2 + 93 * int(done) // req.n, f"표본 {done} / {req.n}")
|
||||
|
||||
report(1, "시작")
|
||||
workers = WORKERS or max(1, (os.cpu_count() or 2) - 2)
|
||||
if req.mode == "search" and workers > 1:
|
||||
result = _run_search({**job, "workers": workers}, emit)
|
||||
else:
|
||||
result = prob.run(**job, emit=emit)
|
||||
record = {
|
||||
"station_key": station_key,
|
||||
"chainage_m": row["chainage_m"],
|
||||
"label": row.get("label"),
|
||||
"base_fp": row["fp"],
|
||||
"side": req.side,
|
||||
"role": slot["role"],
|
||||
"condition": req.condition,
|
||||
"required": slot["required"],
|
||||
"fs_deterministic": slot["surface"]["F"],
|
||||
"surface_kind": slot["surface"]["kind"],
|
||||
"request": req.model_dump(),
|
||||
"result": result,
|
||||
"computed_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
}
|
||||
write_derived(root, KIND, key, record, keep=KEEP)
|
||||
return record
|
||||
|
||||
|
||||
def records(root: Path) -> list[dict[str, Any]]:
|
||||
"""가장 최근 2D 요약 측점에 붙은 기록 — (측점 · 비탈 · 조건)마다 가장 최근 하나 · 누가거리 순."""
|
||||
summary, _ = e03._latest_summary(root, "")
|
||||
fps = {r["fp"] for r in (summary or {}).get("stations") or []}
|
||||
latest: dict[tuple, dict[str, Any]] = {}
|
||||
for path in derived_dir(root, KIND).glob("*.json"):
|
||||
value = (read_derived(root, KIND, path.stem) or {}).get("value")
|
||||
if not value or value.get("base_fp") not in fps:
|
||||
continue
|
||||
slot = (value["station_key"], value["side"], value["condition"])
|
||||
if slot not in latest or value["computed_at"] > latest[slot]["computed_at"]:
|
||||
latest[slot] = value
|
||||
return sorted(
|
||||
latest.values(), key=lambda v: (float(v["chainage_m"]), v["side"], v["condition"])
|
||||
)
|
||||
|
||||
|
||||
def setup(root: Path, station_key: str, side: str, condition: str) -> dict[str, Any]:
|
||||
slot = _slot(root, station_key, side, condition, "bishop")
|
||||
gw = slot["model"].get("groundwater") or {}
|
||||
last = next(
|
||||
(
|
||||
r
|
||||
for r in records(root)
|
||||
if (r["station_key"], r["side"], r["condition"]) == (station_key, side, condition)
|
||||
),
|
||||
None,
|
||||
)
|
||||
return {
|
||||
"station_key": station_key,
|
||||
"side": side,
|
||||
"condition": condition,
|
||||
"role": slot["role"],
|
||||
"required": slot["required"],
|
||||
"fs_deterministic": {m: (v.get("min") or {}).get("F") for m, v in slot["methods"].items()},
|
||||
"materials": [
|
||||
{k: m.get(k) for k in ("id", "name", "c", "phi", "gamma_t")}
|
||||
for m in _materials(slot["model"])
|
||||
],
|
||||
"groundwater": {"mode": gw.get("mode", "none"), "ru": gw.get("ru")},
|
||||
"defaults": load_defaults(),
|
||||
"search_max_n": SEARCH_MAX_N,
|
||||
"last": last,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{project_id}/stab/prob/setup/{station_key}", response_model=None)
|
||||
async def get_setup(project_id: UUID, station_key: str, side: str, condition: str) -> dict:
|
||||
root, _ = await e02._project(project_id)
|
||||
return await asyncio.to_thread(setup, root, station_key, side, condition)
|
||||
|
||||
|
||||
@router.post("/{project_id}/stab/prob/{station_key}", response_model=None)
|
||||
async def post_prob(project_id: UUID, station_key: str, payload: ProbRequest) -> dict:
|
||||
"""뒤에서 — 같은 지문이면 바로 저장본(fresh) · 진행 = `GET …/stab/prob/progress`."""
|
||||
root, _ = await e02._project(project_id)
|
||||
key = str(project_id)
|
||||
if (_JOBS.get(key) or {}).get("running"):
|
||||
raise HTTPException(status_code=409, detail="이미 확률 해석 중입니다.")
|
||||
# 입력 검사 · 저장본 확인은 바로(422 를 그 자리에서 돌려줌)
|
||||
slot = await asyncio.to_thread(
|
||||
_slot, root, station_key, payload.side, payload.condition, payload.method
|
||||
)
|
||||
_variables(payload, slot["model"])
|
||||
fp = derived_key(code_hash(), slot["ctx"]["row"]["fp"], payload.model_dump())
|
||||
saved = read_derived(root, KIND, fp)
|
||||
if saved:
|
||||
return {"status": "fresh", "record": saved["value"]}
|
||||
job = {
|
||||
"running": True,
|
||||
"percent": 0,
|
||||
"message": "시작",
|
||||
"error": None,
|
||||
"station_key": station_key,
|
||||
}
|
||||
_JOBS[key] = job
|
||||
|
||||
def report(percent: int, message: str) -> None:
|
||||
job.update(percent=int(percent), message=message)
|
||||
|
||||
async def work() -> None:
|
||||
try:
|
||||
await asyncio.to_thread(run_prob, root, station_key, payload, report)
|
||||
job.update(percent=100, message="끝")
|
||||
except Exception as error: # noqa: BLE001 — 진행률에 까닭을 그대로 올림
|
||||
logger.exception("E03 확률 해석 실패: project_id=%s", project_id)
|
||||
job["error"] = str(getattr(error, "detail", None) or error)
|
||||
finally:
|
||||
job["running"] = False
|
||||
|
||||
task = asyncio.create_task(work(), name=f"E03 prob {project_id}")
|
||||
_TASKS.add(task)
|
||||
task.add_done_callback(_TASKS.discard)
|
||||
return {"status": "accepted", **job}
|
||||
|
||||
|
||||
@router.get("/{project_id}/stab/prob/progress", response_model=None)
|
||||
async def get_progress(project_id: UUID) -> dict:
|
||||
await e02._project(project_id)
|
||||
job = _JOBS.get(str(project_id))
|
||||
return dict(job) if job else {"running": False, "percent": 0, "message": "", "error": None}
|
||||
|
||||
|
||||
@router.get("/{project_id}/stab/prob", response_model=None)
|
||||
async def get_records(project_id: UUID) -> dict:
|
||||
root, _ = await e02._project(project_id)
|
||||
return {"records": await asyncio.to_thread(records, root)}
|
||||
@@ -6,6 +6,7 @@
|
||||
* 계산 = 서버 단독([해석] → POST …/stab/analyze2d · 잠금판 진행률 = GET …/stab/progress) ·
|
||||
* 대표 단면 켬/끔 = sessionStorage 캐시 → [대표 단면 저장] = PUT(자동저장 없음).
|
||||
* 「보강 검토」 탭 = 우측을 가정 재해석 · 벽 외적 안정으로 바꿈(`UI_Reinforce` · 서버 따로 저장본).
|
||||
* 「확률 · 민감도」 탭 = 대표 단면 확률 해석(`UI_Prob` · 서버 따로 저장본 · 카드 안 진행률).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./E03_StabSection_UI_Style.css";
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
type WhatIfRecord,
|
||||
} from "./E03_StabSection_UI_Reinforce";
|
||||
import { buildSupport, type SupportAsk, type SupportRecord } from "./E03_StabSection_UI_Support";
|
||||
import { createProbPanel } from "./E03_StabSection_UI_Prob";
|
||||
import type {
|
||||
Analysis,
|
||||
Condition,
|
||||
@@ -100,6 +102,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
return void main.append(el("p", { className: "e03-empty", text: t("E03_NoProject") }));
|
||||
const pid: string = projectId;
|
||||
const base = `/projects/${pid}/stab`;
|
||||
const prob = createProbPanel(pid, base, () => paintSection());
|
||||
|
||||
let result: Result2d | null = null;
|
||||
const sections = new Map<string, SectionView>();
|
||||
@@ -110,7 +113,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
let side: string | null = null;
|
||||
let picked: number | null = null;
|
||||
const toggles: SectionToggles = { map: true, centers: false, candidates: true };
|
||||
let mode: "result" | "reinforce" = "result";
|
||||
let mode: "result" | "reinforce" | "prob" = "result";
|
||||
let busy = false;
|
||||
const slopeRecs = new Map<string, WhatIfRecord>(); // 측점 열쇠 → 마지막 가정
|
||||
const wallRecs = new Map<string, WallRecord>();
|
||||
@@ -310,6 +313,7 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
options: [
|
||||
{ value: "result", text: t("E03_Tab_Result") },
|
||||
{ value: "reinforce", text: t("E03_Tab_Reinforce") },
|
||||
{ value: "prob", text: t("E03_Tab_Prob") },
|
||||
],
|
||||
value: mode,
|
||||
compact: true,
|
||||
@@ -336,6 +340,12 @@ export function renderE03StabSection(root: HTMLElement): void {
|
||||
if (view.regions.some((r) => r.material_id === WALL_ID))
|
||||
legendHost.append(el("p", { className: "e03-muted", text: t("E03_Walls_Hint") }));
|
||||
const row = currentRow();
|
||||
if (mode === "prob" && row) {
|
||||
resultHost.append(
|
||||
prob.render({ row, condition, side, editable: !!result?.editable && !busy }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (mode === "reinforce" && row) {
|
||||
if (row.walls && !wallRecs.has(row.key) && !busy && result?.editable)
|
||||
queueMicrotask(() => void runWall(row.key, { method: "coulomb", water_ratio: 0 }));
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/* E03 확률 · 민감도 탭(537-1) — 입력 표 · 히스토그램 · 민감도 선 · 토네이도 막대 */
|
||||
.e03-prob-vars input[type="number"] {
|
||||
width: 5.5em;
|
||||
}
|
||||
|
||||
.e03-prob-vars select {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.e03-prob-run {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.e03-prob-src summary {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.e03-prob-bar {
|
||||
fill: var(--e03-mat-0);
|
||||
fill-opacity: 0.75;
|
||||
}
|
||||
|
||||
.e03-prob-bar--fail {
|
||||
fill: var(--e03-ng);
|
||||
}
|
||||
|
||||
.e03-prob-mark {
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.e03-prob-mark--one {
|
||||
stroke: var(--e03-ng);
|
||||
}
|
||||
|
||||
.e03-prob-mark--req {
|
||||
stroke: var(--e03-mat-3);
|
||||
stroke-dasharray: 5 3;
|
||||
}
|
||||
|
||||
.e03-prob-mark--det {
|
||||
stroke: var(--e03-critical);
|
||||
stroke-dasharray: 2 2;
|
||||
}
|
||||
|
||||
.e03-prob-mark__text {
|
||||
font-size: 10px;
|
||||
stroke: none;
|
||||
fill: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.e03-prob-line {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.e03-prob-line--0 {
|
||||
stroke: var(--e03-mat-0);
|
||||
}
|
||||
.e03-prob-line--1 {
|
||||
stroke: var(--e03-mat-1);
|
||||
}
|
||||
.e03-prob-line--2 {
|
||||
stroke: var(--e03-mat-2);
|
||||
}
|
||||
.e03-prob-line--3 {
|
||||
stroke: var(--e03-mat-3);
|
||||
}
|
||||
.e03-prob-line--4 {
|
||||
stroke: var(--e03-mat-4);
|
||||
}
|
||||
.e03-prob-line--5 {
|
||||
stroke: var(--e03-mat-5);
|
||||
}
|
||||
.e03-prob-line--6 {
|
||||
stroke: var(--e03-mat-6);
|
||||
}
|
||||
.e03-prob-line--7 {
|
||||
stroke: var(--e03-mat-7);
|
||||
}
|
||||
|
||||
.e03-prob-tbar--lo {
|
||||
fill: var(--e03-mat-1);
|
||||
}
|
||||
|
||||
.e03-prob-tbar--hi {
|
||||
fill: var(--e03-mat-0);
|
||||
}
|
||||
|
||||
.e03-prob-tlabel {
|
||||
font-size: 10px;
|
||||
fill: var(--color-text-secondary);
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_UI_Prob.ts
|
||||
* E03 「확률 · 민감도」 탭(요구 351 · PLAN 537-1) — 대표 단면 · 고른 비탈 · 조건.
|
||||
* 분포 입력(단면 물성 × c · φ · γ · 지하수위 · r_u — 쓰기 · 분포 · 변동계수/표준편차 · 절단 ±kσ) ·
|
||||
* 표본(LHS · MC · N · 시드) · 갈래(임계면 고정 · 매회 탐색) · 방법 · c–φ 상관 →
|
||||
* 서버 계산(POST …/stab/prob/{측점} · 뒤에서 · 진행 GET …/stab/prob/progress · 카드 안 진행률).
|
||||
* 기본값 후보 = 서버 `prob_defaults.json`(근거 · 사용자 확정 전) · 입력 = sessionStorage 캐시(자동저장 없음).
|
||||
* 결과 = 요약 표 · 히스토그램 · 누적분포 · 민감도 · 토네이도(`UI_ProbChart`).
|
||||
* ========================================================================== */
|
||||
|
||||
import "./E03_StabSection_UI_Prob.css";
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import {
|
||||
createButton,
|
||||
createInputField,
|
||||
createSelectField,
|
||||
el,
|
||||
showToast,
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import {
|
||||
changedCount,
|
||||
fillCache,
|
||||
probRequest,
|
||||
probRows,
|
||||
type ProbCache,
|
||||
type ProbRecord,
|
||||
type ProbRow,
|
||||
type ProbSetup,
|
||||
} from "./logic/E03_StabSection_Prob";
|
||||
import type { Condition, StationRow } from "./logic/E03_StabSection_Logic";
|
||||
import {
|
||||
cdfChart,
|
||||
histogramChart,
|
||||
sensitivityChart,
|
||||
tornadoChart,
|
||||
} from "./E03_StabSection_UI_ProbChart";
|
||||
|
||||
export interface ProbView {
|
||||
row: StationRow;
|
||||
condition: Condition;
|
||||
side: string | null;
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
running: boolean;
|
||||
percent: number;
|
||||
message: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const fmt = (v: number | null | undefined, d = 3): string =>
|
||||
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
||||
const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));
|
||||
const METHODS = ["bishop", "janbu", "spencer", "morgenstern_price", "ordinary"] as const;
|
||||
const methodName = (m: string): string =>
|
||||
t(m === "morgenstern_price" ? "E03_Pb_Method_mp" : (`E03_Method_${m}` as LocaleKey));
|
||||
|
||||
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...init,
|
||||
});
|
||||
const body = (await response.json().catch(() => ({}))) as T & { detail?: unknown };
|
||||
if (!response.ok)
|
||||
throw new Error(typeof body.detail === "string" ? body.detail : `HTTP ${response.status}`);
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 탭 하나 — 준비(setup) · 결과 · 진행을 들고 다시 그릴 때 `repaint` 를 부름 */
|
||||
export function createProbPanel(projectId: string, base: string, repaint: () => void) {
|
||||
const cacheKey = `e03_prob_cache:${projectId}`;
|
||||
const setups = new Map<string, ProbSetup | "loading" | string>();
|
||||
const records = new Map<string, ProbRecord>();
|
||||
let cache: ProbCache | null = JSON.parse(sessionStorage.getItem(cacheKey) ?? "null");
|
||||
let busy: Progress | null = null;
|
||||
|
||||
const save = (): void => sessionStorage.setItem(cacheKey, JSON.stringify(cache));
|
||||
const slotOf = (v: ProbView, side: string): string => `${v.row.key}|${side}|${v.condition}`;
|
||||
|
||||
async function load(slot: string, key: string, side: string, condition: string): Promise<void> {
|
||||
setups.set(slot, "loading");
|
||||
try {
|
||||
const q = `side=${side}&condition=${condition}`;
|
||||
const s = await api<ProbSetup>(`${base}/prob/setup/${encodeURIComponent(key)}?${q}`);
|
||||
setups.set(slot, s);
|
||||
if (s.last) records.set(slot, s.last);
|
||||
} catch (error) {
|
||||
setups.set(slot, error instanceof Error ? error.message : t("Common_Status_Error"));
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
async function run(slot: string, setup: ProbSetup): Promise<void> {
|
||||
if (!cache) return;
|
||||
busy = { running: true, percent: 0, message: "", error: null };
|
||||
repaint();
|
||||
try {
|
||||
const body = probRequest(setup, cache);
|
||||
const path = `${base}/prob/${encodeURIComponent(setup.station_key)}`;
|
||||
const first = await api<{ status: string; record?: ProbRecord }>(path, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (first.status !== "fresh") {
|
||||
for (;;) {
|
||||
await sleep(700);
|
||||
busy = await api<Progress>(`${base}/prob/progress`);
|
||||
repaint();
|
||||
if (!busy.running) break;
|
||||
}
|
||||
if (busy.error) throw new Error(busy.error);
|
||||
}
|
||||
busy = null;
|
||||
await load(slot, setup.station_key, setup.side, setup.condition);
|
||||
showToast(t("E03_Pb_Done"), "success");
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
||||
} finally {
|
||||
busy = null;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
function inputTable(setup: ProbSetup): HTMLElement {
|
||||
const table = el("table", {
|
||||
className: "e03-table e03-prob-vars",
|
||||
attrs: { "data-role": "prob-vars" },
|
||||
});
|
||||
const heads = [
|
||||
"E03_Pb_Use",
|
||||
"E03_Pb_Var",
|
||||
"E03_Pb_Mean",
|
||||
"E03_Pb_Dist",
|
||||
"E03_Pb_Spread",
|
||||
"E03_Pb_Trunc",
|
||||
];
|
||||
table.append(
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((k) => el("th", { text: t(k as LocaleKey) })) })],
|
||||
}),
|
||||
);
|
||||
const body = el("tbody");
|
||||
for (const row of probRows(setup)) body.append(inputRow(row));
|
||||
table.append(body);
|
||||
return table;
|
||||
}
|
||||
|
||||
function inputRow(row: ProbRow): HTMLElement {
|
||||
const v = cache!.vars[row.key];
|
||||
const tr = el("tr", { attrs: { "data-key": row.key } });
|
||||
const use = el("input", { attrs: { type: "checkbox", "data-field": "use" } });
|
||||
use.checked = v.use;
|
||||
use.addEventListener("change", () => ((v.use = use.checked), save()));
|
||||
const dist = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { "data-field": "dist" },
|
||||
});
|
||||
for (const d of row.prop === "water" ? ["normal"] : ["normal", "lognormal"])
|
||||
dist.append(el("option", { text: t(`E03_Pb_Dist_${d}` as LocaleKey), attrs: { value: d } }));
|
||||
dist.value = v.dist;
|
||||
dist.addEventListener("change", () => ((v.dist = dist.value as typeof v.dist), save()));
|
||||
const num = (value: number | null, field: string, apply: (n: number | null) => void) => {
|
||||
const input = el("input", {
|
||||
className: "ui-input ui-input--sm",
|
||||
attrs: { type: "number", step: "0.01", min: "0", "data-field": field },
|
||||
});
|
||||
input.value = value === null ? "" : String(value);
|
||||
input.addEventListener("input", () => {
|
||||
const n = Number(input.value);
|
||||
if (input.value.trim() === "") apply(null);
|
||||
else if (Number.isFinite(n) && n >= 0) apply(n);
|
||||
save();
|
||||
});
|
||||
return input;
|
||||
};
|
||||
const unit = row.kind === "cov" ? t("E03_Pb_Cov") : row.prop === "water" ? "σ m" : "σ";
|
||||
const label =
|
||||
row.prop === "water" || row.prop === "ru"
|
||||
? t(`E03_Pb_Prop_${row.prop}` as LocaleKey)
|
||||
: `${row.name} ${t(`E03_Pb_Prop_${row.prop}` as LocaleKey)}`;
|
||||
tr.append(
|
||||
el("td", { children: [use] }),
|
||||
el("td", { text: label }),
|
||||
el("td", { className: "is-num", text: row.prop === "water" ? "±0" : fmt(row.mean, 2) }),
|
||||
el("td", { children: [dist] }),
|
||||
el("td", {
|
||||
children: [
|
||||
num(v.value, "value", (n) => (v.value = n ?? 0)),
|
||||
el("span", { className: "e03-muted", text: ` ${unit}` }),
|
||||
],
|
||||
}),
|
||||
el("td", { children: [num(v.trunc, "trunc", (n) => (v.trunc = n))] }),
|
||||
);
|
||||
return tr;
|
||||
}
|
||||
|
||||
function runFields(setup: ProbSetup, slot: string, editable: boolean): HTMLElement {
|
||||
const r = cache!.run;
|
||||
const wrap = el("div", { className: "e03-panel__row e03-prob-run" });
|
||||
const set = <K extends keyof typeof r>(k: K, v: (typeof r)[K]): void => {
|
||||
r[k] = v;
|
||||
save();
|
||||
};
|
||||
const sel = (
|
||||
label: LocaleKey,
|
||||
value: string,
|
||||
opts: [string, string][],
|
||||
on: (v: string) => void,
|
||||
) => {
|
||||
const f = createSelectField({
|
||||
label: t(label),
|
||||
options: opts.map(([value, text]) => ({ value, text })),
|
||||
value,
|
||||
compact: true,
|
||||
onChange: on,
|
||||
});
|
||||
f.select.dataset.field = label;
|
||||
return f.root;
|
||||
};
|
||||
const numField = (label: LocaleKey, value: number, step: string, on: (n: number) => void) => {
|
||||
const f = createInputField({
|
||||
label: t(label),
|
||||
type: "number",
|
||||
compact: true,
|
||||
value: String(value),
|
||||
});
|
||||
f.input.step = step;
|
||||
f.input.dataset.field = label;
|
||||
f.input.addEventListener("input", () => {
|
||||
const n = Number(f.input.value);
|
||||
if (f.input.value.trim() !== "" && Number.isFinite(n)) on(n);
|
||||
});
|
||||
return f.root;
|
||||
};
|
||||
wrap.append(
|
||||
sel(
|
||||
"E03_Pb_Mode",
|
||||
r.mode,
|
||||
[
|
||||
["fixed", t("E03_Pb_Mode_fixed")],
|
||||
["search", t("E03_Pb_Mode_search")],
|
||||
],
|
||||
(v) => set("mode", v as typeof r.mode),
|
||||
),
|
||||
sel(
|
||||
"E03_Pb_Method",
|
||||
r.method,
|
||||
METHODS.map((m) => [m, methodName(m)]),
|
||||
(v) => set("method", v as typeof r.method),
|
||||
),
|
||||
sel(
|
||||
"E03_Pb_Sampling",
|
||||
r.sampling,
|
||||
[
|
||||
["lhs", "LHS"],
|
||||
["mc", "Monte Carlo"],
|
||||
],
|
||||
(v) => set("sampling", v as typeof r.sampling),
|
||||
),
|
||||
numField("E03_Pb_N", r.n, "100", (n) => set("n", n)),
|
||||
numField("E03_Pb_Seed", r.seed, "1", (n) => set("seed", n)),
|
||||
numField("E03_Pb_Rho", r.rho, "0.1", (n) => set("rho", n)),
|
||||
);
|
||||
const go = createButton({
|
||||
label: busy ? `${t("E03_Rf_Busy")} ${busy.percent}%` : t("E03_Pb_Run"),
|
||||
variant: "filled",
|
||||
onClick: () => void run(slot, setup),
|
||||
});
|
||||
go.dataset.role = "prob-run";
|
||||
go.disabled = !editable || !!busy;
|
||||
const reset = createButton({
|
||||
label: t("E03_Pb_Defaults"),
|
||||
variant: "ghost",
|
||||
onClick: () => {
|
||||
cache = fillCache(setup, null);
|
||||
save();
|
||||
repaint();
|
||||
},
|
||||
});
|
||||
reset.disabled = !!busy;
|
||||
wrap.append(reset, go);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function summary(rec: ProbRecord): HTMLElement {
|
||||
const s = rec.result.stats;
|
||||
const rows: [LocaleKey, string][] = [
|
||||
["E03_Pb_Det", fmt(rec.fs_deterministic)],
|
||||
["E03_Pb_MeanF", fmt(s.mean)],
|
||||
["E03_Pb_Std", fmt(s.std)],
|
||||
["E03_Pb_Pf", `${fmt((s.pf ?? 0) * 100, 2)} %`],
|
||||
["E03_Pb_BetaN", fmt(s.beta_normal)],
|
||||
["E03_Pb_BetaLn", fmt(s.beta_lognormal)],
|
||||
[
|
||||
"E03_Pb_PReq",
|
||||
s.p_below_required === null || s.p_below_required === undefined
|
||||
? "-"
|
||||
: `${fmt(s.p_below_required * 100, 2)} %`,
|
||||
],
|
||||
["E03_Pb_Valid", `${s.n_valid} / ${s.n}`],
|
||||
["E03_Pb_Time", `${fmt(rec.result.elapsed_s.total, 1)} s`],
|
||||
];
|
||||
const dl = el("dl", { className: "e03-kpi", attrs: { "data-role": "prob-summary" } });
|
||||
for (const [k, v] of rows)
|
||||
dl.append(el("dt", { text: t(k) }), el("dd", { text: v, attrs: { "data-k": k } }));
|
||||
const r = rec.result;
|
||||
const note = `${t(`E03_Pb_Mode_${r.mode}` as LocaleKey)} · ${methodName(r.method)} · ${r.sampling.toUpperCase()} ${r.n} · seed ${r.seed} · ${rec.computed_at}`;
|
||||
return el("div", { children: [dl, el("p", { className: "e03-muted", text: note })] });
|
||||
}
|
||||
|
||||
function sources(setup: ProbSetup, changed: number): HTMLElement {
|
||||
const d = el("details", { className: "e03-prob-src" });
|
||||
const tag = changed
|
||||
? t("E03_Pb_UserInput").replace("{n}", String(changed))
|
||||
: t("E03_Pb_Unconfirmed");
|
||||
d.append(el("summary", { text: tag }));
|
||||
for (const [k, v] of Object.entries(setup.defaults.sources))
|
||||
d.append(el("p", { className: "e03-muted", text: `${k} — ${v}` }));
|
||||
return d;
|
||||
}
|
||||
|
||||
function render(v: ProbView): HTMLElement {
|
||||
const host = el("div", { className: "e03-card e03-prob", attrs: { "data-role": "prob" } });
|
||||
const head = (key: LocaleKey): HTMLElement =>
|
||||
el("h3", { className: "e03-card__title", text: t(key) });
|
||||
host.append(head("E03_Tab_Prob"));
|
||||
if (!v.row.representative?.on) {
|
||||
host.append(el("p", { className: "e03-empty", text: t("E03_Pb_RepOnly") }));
|
||||
return host;
|
||||
}
|
||||
const side = v.side ?? v.row.sides[0]?.side;
|
||||
if (!side) return host;
|
||||
const slot = slotOf(v, side);
|
||||
const got = setups.get(slot);
|
||||
if (got === undefined) queueMicrotask(() => void load(slot, v.row.key, side, v.condition));
|
||||
if (typeof got !== "object") {
|
||||
const text = typeof got === "string" && got !== "loading" ? got : t("E03_Loading");
|
||||
host.append(el("p", { className: "e03-empty", text }));
|
||||
return host;
|
||||
}
|
||||
cache = fillCache(got, cache);
|
||||
const sideText = `${t(side === "left" ? "E03_Left" : "E03_Right")} · ${t(got.role === "cut" ? "E03_Cut" : "E03_Fill")}`;
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: `${sideText} · ${t(`E03_Cond_${v.condition}` as LocaleKey)} · ${t("E03_Pb_MeanNote")}`,
|
||||
}),
|
||||
);
|
||||
host.append(
|
||||
inputTable(got),
|
||||
runFields(got, slot, v.editable),
|
||||
sources(got, changedCount(got, cache)),
|
||||
);
|
||||
if (busy)
|
||||
host.append(
|
||||
el("p", {
|
||||
className: "e03-muted",
|
||||
text: `${busy.percent}% · ${busy.message}`,
|
||||
attrs: { "data-role": "prob-progress" },
|
||||
}),
|
||||
);
|
||||
const rec = records.get(slot);
|
||||
if (!rec) return host;
|
||||
const res = rec.result;
|
||||
host.append(
|
||||
head("E03_Pb_Result"),
|
||||
summary(rec),
|
||||
head("E03_Pb_Hist"),
|
||||
histogramChart(res, rec.required, rec.fs_deterministic),
|
||||
head("E03_Pb_Cdf"),
|
||||
cdfChart(res, rec.required),
|
||||
head("E03_Pb_Sens"),
|
||||
sensitivityChart(res),
|
||||
head("E03_Pb_Tornado"),
|
||||
tornadoChart(res),
|
||||
);
|
||||
return host;
|
||||
}
|
||||
|
||||
return { render, isBusy: (): boolean => !!busy };
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_UI_ProbChart.ts
|
||||
* E03 「확률 · 민감도」 그래프 SVG(PLAN 537-1) — 안전율 히스토그램(F < 1 빨강 · F = 1 · 기준 선) ·
|
||||
* 누적분포 · 민감도(가로 = 범위 % · 변수마다 선 · Slide2 Sensitivity Plot) · 토네이도(끝값 F 폭 순).
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { axisTicks, type ProbCurve, type ProbResult } from "./logic/E03_StabSection_Prob";
|
||||
import { pointsText, type Point } from "./logic/E03_StabSection_Logic";
|
||||
import { svgEl } from "./E03_StabSection_UI_Section";
|
||||
|
||||
const W = 380;
|
||||
const H = 200;
|
||||
const PAD = { l: 44, r: 12, t: 12, b: 28 };
|
||||
const fmt = (v: number | null | undefined, d = 2): string =>
|
||||
v === null || v === undefined || !Number.isFinite(v) ? "-" : v.toFixed(d);
|
||||
|
||||
interface Frame {
|
||||
svg: SVGSVGElement;
|
||||
sx: (x: number) => number;
|
||||
sy: (y: number) => number;
|
||||
}
|
||||
|
||||
/** 축 틀 — 가로 · 세로 눈금 · 축 이름 */
|
||||
function frame(
|
||||
role: string,
|
||||
xr: [number, number],
|
||||
yr: [number, number],
|
||||
xLabel: string,
|
||||
yLabel: string,
|
||||
pad = PAD,
|
||||
yAxis = true,
|
||||
): Frame {
|
||||
const xt = axisTicks(xr[0], xr[1], 5);
|
||||
const yt = axisTicks(yr[0], yr[1], 4);
|
||||
const [x0, x1] = [xt[0], xt[xt.length - 1]];
|
||||
const [y0, y1] = [yt[0], yt[yt.length - 1]];
|
||||
const sx = (x: number): number =>
|
||||
pad.l + ((x - x0) / Math.max(x1 - x0, 1e-9)) * (W - pad.l - pad.r);
|
||||
const sy = (y: number): number =>
|
||||
H - pad.b - ((y - y0) / Math.max(y1 - y0, 1e-9)) * (H - pad.t - pad.b);
|
||||
const svg = svgEl("svg", { viewBox: `0 0 ${W} ${H}`, "data-role": role }) as SVGSVGElement;
|
||||
const axis = svgEl("g", { class: "e03-axis" });
|
||||
for (const v of yAxis ? yt : []) {
|
||||
axis.append(svgEl("line", { x1: pad.l, x2: W - pad.r, y1: sy(v), y2: sy(v) }));
|
||||
axis.append(svgEl("text", { x: pad.l - 4, y: sy(v) + 4, "text-anchor": "end" }, String(v)));
|
||||
}
|
||||
for (const v of xt)
|
||||
axis.append(svgEl("text", { x: sx(v), y: H - pad.b + 14, "text-anchor": "middle" }, String(v)));
|
||||
axis.append(svgEl("text", { x: W - pad.r, y: H - 2, "text-anchor": "end" }, xLabel));
|
||||
axis.append(svgEl("text", { x: 2, y: pad.t - 2 }, yLabel));
|
||||
svg.append(axis);
|
||||
return { svg, sx, sy };
|
||||
}
|
||||
|
||||
function mark(f: Frame, x: number, cls: string, label: string): void {
|
||||
f.svg.append(
|
||||
svgEl("line", {
|
||||
x1: f.sx(x),
|
||||
x2: f.sx(x),
|
||||
y1: PAD.t,
|
||||
y2: H - PAD.b,
|
||||
class: `e03-prob-mark ${cls}`,
|
||||
}),
|
||||
svgEl("text", { x: f.sx(x) + 3, y: PAD.t + 10, class: `e03-prob-mark__text ${cls}` }, label),
|
||||
);
|
||||
}
|
||||
|
||||
function box(role: string, svg: SVGSVGElement, legend?: HTMLElement): HTMLElement {
|
||||
const host = el("div", { className: "e03-graph", attrs: { "data-role": `${role}-box` } });
|
||||
host.append(svg as unknown as HTMLElement);
|
||||
if (legend) host.append(legend);
|
||||
return host;
|
||||
}
|
||||
|
||||
/** 안전율 히스토그램 — 칸 개수 · F < 1 칸 빨강 · F = 1 · 기준 · 결정론 선 */
|
||||
export function histogramChart(
|
||||
res: ProbResult,
|
||||
required: number | null,
|
||||
fsDet: number | null,
|
||||
): HTMLElement {
|
||||
const h = res.stats.histogram;
|
||||
if (!h) return el("div");
|
||||
const { edges, counts } = h;
|
||||
const xr: [number, number] = [
|
||||
Math.min(edges[0], 1),
|
||||
Math.max(edges[edges.length - 1], required ?? 0),
|
||||
];
|
||||
const f = frame("prob-hist", xr, [0, Math.max(...counts, 1)], "Fs", t("E03_Pb_Count"));
|
||||
counts.forEach((n, i) => {
|
||||
const [a, b] = [edges[i], edges[i + 1]];
|
||||
const bar = svgEl("rect", {
|
||||
x: f.sx(a),
|
||||
y: f.sy(n),
|
||||
width: Math.max(f.sx(b) - f.sx(a) - 0.5, 0.5),
|
||||
height: f.sy(0) - f.sy(n),
|
||||
class: b <= 1 ? "e03-prob-bar e03-prob-bar--fail" : "e03-prob-bar",
|
||||
"data-count": n,
|
||||
});
|
||||
bar.append(svgEl("title", {}, `${fmt(a, 3)} ~ ${fmt(b, 3)} · ${n}`));
|
||||
f.svg.append(bar);
|
||||
});
|
||||
mark(f, 1, "e03-prob-mark--one", "F = 1");
|
||||
if (required) mark(f, required, "e03-prob-mark--req", `${t("E03_Required")} ${fmt(required)}`);
|
||||
if (fsDet) mark(f, fsDet, "e03-prob-mark--det", t("E03_Pb_Det"));
|
||||
return box("prob-hist", f.svg);
|
||||
}
|
||||
|
||||
/** 누적분포 — 가로 F · 세로 P(F ≤ x) · F = 1 에서 Pf */
|
||||
export function cdfChart(res: ProbResult, required: number | null): HTMLElement {
|
||||
const c = res.stats.cdf;
|
||||
if (!c) return el("div");
|
||||
const xr: [number, number] = [Math.min(c.F[0], 1), Math.max(c.F[c.F.length - 1], required ?? 0)];
|
||||
const f = frame("prob-cdf", xr, [0, 1], "Fs", "P(F ≤ x)");
|
||||
const pts: Point[] = c.F.map((x, i) => [f.sx(x), f.sy(c.p[i])]);
|
||||
f.svg.append(
|
||||
svgEl("polyline", { points: pointsText(pts), class: "e03-prob-line e03-prob-line--0" }),
|
||||
);
|
||||
mark(f, 1, "e03-prob-mark--one", `Pf ${fmt((res.stats.pf ?? 0) * 100, 1)} %`);
|
||||
if (required) mark(f, required, "e03-prob-mark--req", `${t("E03_Required")} ${fmt(required)}`);
|
||||
return box("prob-cdf", f.svg);
|
||||
}
|
||||
|
||||
function legendOf(curves: ProbCurve[]): HTMLElement {
|
||||
const legend = el("div", { className: "e03-legend" });
|
||||
curves.forEach((c, i) => {
|
||||
const item = el("span", { className: "e03-legend__item" });
|
||||
const sw = svgEl("svg", { width: 18, height: 8 });
|
||||
sw.append(
|
||||
svgEl("line", {
|
||||
x1: 0,
|
||||
x2: 18,
|
||||
y1: 4,
|
||||
y2: 4,
|
||||
class: `e03-prob-line e03-prob-line--${i % 8}`,
|
||||
}),
|
||||
);
|
||||
item.append(sw as unknown as HTMLElement, c.name);
|
||||
legend.append(item);
|
||||
});
|
||||
return legend;
|
||||
}
|
||||
|
||||
/** 민감도 — 가로 = 범위 %(0 = 아래 끝 · 100 = 위 끝) · 나머지 변수 평균 · 임계면 고정 */
|
||||
export function sensitivityChart(res: ProbResult): HTMLElement {
|
||||
const curves = res.sensitivity.curves;
|
||||
const all = curves.flatMap((c) => c.F).filter((v): v is number => v !== null);
|
||||
if (!all.length) return el("div");
|
||||
const f = frame(
|
||||
"prob-sens",
|
||||
[0, 100],
|
||||
[Math.min(...all, 1), Math.max(...all)],
|
||||
t("E03_Pb_Range"),
|
||||
"Fs",
|
||||
);
|
||||
curves.forEach((c, i) => {
|
||||
const n = c.F.length;
|
||||
const pts: Point[] = [];
|
||||
c.F.forEach((v, k) => {
|
||||
if (v !== null) pts.push([f.sx((100 * k) / Math.max(n - 1, 1)), f.sy(v)]);
|
||||
});
|
||||
const line = svgEl("polyline", {
|
||||
points: pointsText(pts),
|
||||
class: `e03-prob-line e03-prob-line--${i % 8}`,
|
||||
"data-name": c.name,
|
||||
});
|
||||
line.append(svgEl("title", {}, `${c.name} · ${fmt(c.x[0])} ~ ${fmt(c.x[n - 1])}`));
|
||||
f.svg.append(line);
|
||||
});
|
||||
return box("prob-sens", f.svg, legendOf(curves));
|
||||
}
|
||||
|
||||
/** 토네이도 — 변수마다 아래 끝 · 위 끝 F 막대(평균 F 기준) · 폭 큰 순 */
|
||||
export function tornadoChart(res: ProbResult): HTMLElement {
|
||||
const rows = res.sensitivity.tornado;
|
||||
const F0 = res.sensitivity.F_mean;
|
||||
if (!rows.length || F0 === null) return el("div");
|
||||
const vals = rows.flatMap((r) => [r.F_lo, r.F_hi]).filter((v): v is number => v !== null);
|
||||
const pad = { ...PAD, l: 96 };
|
||||
const f = frame(
|
||||
"prob-tornado",
|
||||
[Math.min(...vals), Math.max(...vals)],
|
||||
[0, 1],
|
||||
"Fs",
|
||||
"",
|
||||
pad,
|
||||
false,
|
||||
);
|
||||
const step = (H - PAD.t - PAD.b) / rows.length;
|
||||
rows.forEach((r, i) => {
|
||||
const y = PAD.t + i * step + step * 0.15;
|
||||
const hh = step * 0.7;
|
||||
for (const [v, cls] of [
|
||||
[r.F_lo, "e03-prob-tbar--lo"],
|
||||
[r.F_hi, "e03-prob-tbar--hi"],
|
||||
] as const) {
|
||||
if (v === null) continue;
|
||||
const [a, b] = [Math.min(f.sx(v), f.sx(F0)), Math.max(f.sx(v), f.sx(F0))];
|
||||
const bar = svgEl("rect", {
|
||||
x: a,
|
||||
y,
|
||||
width: Math.max(b - a, 0.5),
|
||||
height: hh,
|
||||
class: `e03-prob-tbar ${cls}`,
|
||||
});
|
||||
bar.append(svgEl("title", {}, `${r.name} · ${fmt(v, 3)}`));
|
||||
f.svg.append(bar);
|
||||
}
|
||||
f.svg.append(
|
||||
svgEl(
|
||||
"text",
|
||||
{ x: pad.l - 4, y: y + hh / 2 + 4, "text-anchor": "end", class: "e03-prob-tlabel" },
|
||||
r.name,
|
||||
),
|
||||
);
|
||||
});
|
||||
mark(f, F0, "e03-prob-mark--det", fmt(F0, 3));
|
||||
return box("prob-tornado", f.svg);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/* =============================================================================
|
||||
* E03_StabSection_Prob.ts
|
||||
* E03 「확률 · 민감도」 탭 계산(PLAN 537-1) — 변수 줄(단면 물성 × c · φ · γ + 지하수위 · r_u) ·
|
||||
* 기본값 후보(서버 `prob_defaults.json`) · 입력 캐시 → 요청 꼴 · 그래프 눈금.
|
||||
* 화면 · fetch · sessionStorage 없음(페이지가 부름).
|
||||
* ========================================================================== */
|
||||
|
||||
export type ProbProp = "c" | "phi" | "gamma" | "water" | "ru";
|
||||
export type ProbDist = "normal" | "lognormal";
|
||||
export type ProbMethod = "ordinary" | "bishop" | "janbu" | "spencer" | "morgenstern_price";
|
||||
|
||||
export interface ProbPropDefault {
|
||||
dist: ProbDist;
|
||||
cov?: number;
|
||||
std?: number;
|
||||
trunc_sigma: number | null;
|
||||
}
|
||||
|
||||
export interface ProbDefaults {
|
||||
run: {
|
||||
n: number;
|
||||
sampling: "lhs" | "mc";
|
||||
seed: number;
|
||||
mode: "fixed" | "search";
|
||||
method: ProbMethod;
|
||||
};
|
||||
properties: Record<ProbProp, ProbPropDefault>;
|
||||
rho_c_phi: number;
|
||||
sources: Record<string, string>;
|
||||
materials?: Record<string, Partial<Record<ProbProp, Partial<ProbPropDefault>>>>;
|
||||
}
|
||||
|
||||
export interface ProbMaterial {
|
||||
id: string;
|
||||
name: string | null;
|
||||
c: number;
|
||||
phi: number;
|
||||
gamma_t: number;
|
||||
}
|
||||
|
||||
export interface ProbStats {
|
||||
n: number;
|
||||
n_valid: number;
|
||||
mean: number | null;
|
||||
std: number | null;
|
||||
cov?: number | null;
|
||||
min?: number;
|
||||
max?: number;
|
||||
pf: number | null;
|
||||
n_fail?: number;
|
||||
beta_normal: number | null;
|
||||
beta_lognormal?: number | null;
|
||||
required?: number | null;
|
||||
p_below_required?: number | null;
|
||||
histogram?: { edges: number[]; counts: number[] };
|
||||
cdf?: { p: number[]; F: number[] };
|
||||
}
|
||||
|
||||
export interface ProbCurve {
|
||||
name: string;
|
||||
x: (number | null)[];
|
||||
F: (number | null)[];
|
||||
F_lo: number | null;
|
||||
F_hi: number | null;
|
||||
swing: number | null;
|
||||
}
|
||||
|
||||
export interface ProbVariable {
|
||||
name: string;
|
||||
material: string | null;
|
||||
prop: ProbProp;
|
||||
dist: ProbDist;
|
||||
mean: number;
|
||||
std: number;
|
||||
lo: number | null;
|
||||
hi: number | null;
|
||||
}
|
||||
|
||||
export interface ProbResult {
|
||||
mode: "fixed" | "search";
|
||||
method: ProbMethod;
|
||||
n: number;
|
||||
sampling: string;
|
||||
seed: number;
|
||||
variables: ProbVariable[];
|
||||
stats: ProbStats;
|
||||
sensitivity: {
|
||||
F_mean: number | null;
|
||||
curves: ProbCurve[];
|
||||
tornado: { name: string; F_lo: number | null; F_hi: number | null; swing: number }[];
|
||||
};
|
||||
elapsed_s: { total: number; samples: number };
|
||||
}
|
||||
|
||||
export interface ProbRecord {
|
||||
station_key: string;
|
||||
side: string;
|
||||
condition: string;
|
||||
required: number | null;
|
||||
fs_deterministic: number | null;
|
||||
request: Record<string, unknown>;
|
||||
result: ProbResult;
|
||||
computed_at: string;
|
||||
}
|
||||
|
||||
export interface ProbSetup {
|
||||
station_key: string;
|
||||
side: string;
|
||||
condition: string;
|
||||
role: "cut" | "fill";
|
||||
required: number | null;
|
||||
fs_deterministic: Record<string, number | null>;
|
||||
materials: ProbMaterial[];
|
||||
groundwater: { mode: "none" | "line" | "ru"; ru: number | null };
|
||||
defaults: ProbDefaults;
|
||||
search_max_n: number;
|
||||
last: ProbRecord | null;
|
||||
}
|
||||
|
||||
/** 변수 줄 하나 — kind cov = 변동계수(평균 곱) · std = 표준편차(m · r_u) */
|
||||
export interface ProbRow {
|
||||
key: string;
|
||||
material: string | null;
|
||||
prop: ProbProp;
|
||||
name: string;
|
||||
mean: number;
|
||||
kind: "cov" | "std";
|
||||
}
|
||||
|
||||
export interface ProbInput {
|
||||
use: boolean;
|
||||
dist: ProbDist;
|
||||
value: number;
|
||||
trunc: number | null;
|
||||
}
|
||||
|
||||
export interface ProbRunInput {
|
||||
n: number;
|
||||
sampling: "lhs" | "mc";
|
||||
seed: number;
|
||||
mode: "fixed" | "search";
|
||||
method: ProbMethod;
|
||||
rho: number;
|
||||
}
|
||||
|
||||
export interface ProbCache {
|
||||
vars: Record<string, ProbInput>;
|
||||
run: ProbRunInput;
|
||||
}
|
||||
|
||||
export const varKey = (material: string | null, prop: ProbProp): string =>
|
||||
`${material ?? "*"}|${prop}`;
|
||||
|
||||
/** 단면 물성마다 c · φ · γ + 지하수 갈래가 수위선이면 지하수위 · r_u 면 r_u */
|
||||
export function probRows(setup: ProbSetup): ProbRow[] {
|
||||
const rows: ProbRow[] = [];
|
||||
for (const m of setup.materials) {
|
||||
const name = m.name ?? m.id;
|
||||
rows.push(
|
||||
{ key: varKey(m.id, "c"), material: m.id, prop: "c", name, mean: m.c, kind: "cov" },
|
||||
{ key: varKey(m.id, "phi"), material: m.id, prop: "phi", name, mean: m.phi, kind: "cov" },
|
||||
{
|
||||
key: varKey(m.id, "gamma"),
|
||||
material: m.id,
|
||||
prop: "gamma",
|
||||
name,
|
||||
mean: m.gamma_t,
|
||||
kind: "cov",
|
||||
},
|
||||
);
|
||||
}
|
||||
const gw = setup.groundwater;
|
||||
if (gw.mode === "line")
|
||||
rows.push({
|
||||
key: varKey(null, "water"),
|
||||
material: null,
|
||||
prop: "water",
|
||||
name: "",
|
||||
mean: 0,
|
||||
kind: "std",
|
||||
});
|
||||
if (gw.mode === "ru")
|
||||
rows.push({
|
||||
key: varKey(null, "ru"),
|
||||
material: null,
|
||||
prop: "ru",
|
||||
name: "",
|
||||
mean: gw.ru ?? 0,
|
||||
kind: "std",
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 기본값 후보 — 성질 기본 위에 물성별 덮어쓰기 · 0 이면 끔 */
|
||||
export function defaultInput(defaults: ProbDefaults, row: ProbRow): ProbInput {
|
||||
const base = defaults.properties[row.prop];
|
||||
const own = (row.material && defaults.materials?.[row.material]?.[row.prop]) || {};
|
||||
const merged = { ...base, ...own };
|
||||
const value = Number((row.kind === "cov" ? merged.cov : merged.std) ?? 0);
|
||||
return {
|
||||
use: value > 0,
|
||||
dist: row.prop === "water" ? "normal" : merged.dist,
|
||||
value,
|
||||
trunc: merged.trunc_sigma ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 캐시가 없는 줄만 기본값 후보로 채움 */
|
||||
export function fillCache(setup: ProbSetup, cache: ProbCache | null): ProbCache {
|
||||
const d = setup.defaults;
|
||||
const vars: Record<string, ProbInput> = { ...(cache?.vars ?? {}) };
|
||||
for (const row of probRows(setup)) vars[row.key] ??= defaultInput(d, row);
|
||||
const run = cache?.run ?? { ...d.run, rho: d.rho_c_phi };
|
||||
return { vars, run };
|
||||
}
|
||||
|
||||
/** 기본값과 다른 줄 수(「사용자 입력」 표기) */
|
||||
export function changedCount(setup: ProbSetup, cache: ProbCache): number {
|
||||
return probRows(setup).filter((row) => {
|
||||
const a = defaultInput(setup.defaults, row);
|
||||
const b = cache.vars[row.key];
|
||||
return (
|
||||
b && (a.use !== b.use || a.dist !== b.dist || a.value !== b.value || a.trunc !== b.trunc)
|
||||
);
|
||||
}).length;
|
||||
}
|
||||
|
||||
/** POST …/stab/prob/{측점} 몸 — 켠 줄만 · 값 0 은 뺌 */
|
||||
export function probRequest(setup: ProbSetup, cache: ProbCache): Record<string, unknown> {
|
||||
const variables = probRows(setup)
|
||||
.map((row) => ({ row, v: cache.vars[row.key] }))
|
||||
.filter(({ v }) => v?.use && v.value > 0)
|
||||
.map(({ row, v }) => ({
|
||||
material: row.material,
|
||||
prop: row.prop,
|
||||
dist: v.dist,
|
||||
...(row.kind === "cov" ? { cov: v.value } : { std: v.value }),
|
||||
trunc_sigma: v.trunc,
|
||||
}));
|
||||
const r = cache.run;
|
||||
return {
|
||||
side: setup.side,
|
||||
condition: setup.condition,
|
||||
method: r.method,
|
||||
mode: r.mode,
|
||||
n: Math.round(r.n),
|
||||
sampling: r.sampling,
|
||||
seed: Math.round(r.seed),
|
||||
rho_c_phi: r.rho,
|
||||
variables,
|
||||
};
|
||||
}
|
||||
|
||||
/** 그래프 눈금 — 0 을 억지로 넣지 않음(안전율 축) */
|
||||
export function axisTicks(min: number, max: number, count = 5): number[] {
|
||||
const lo = Math.min(min, max);
|
||||
const hi = Math.max(lo + 1e-9, max);
|
||||
const raw = (hi - lo) / Math.max(count, 1);
|
||||
const mag = 10 ** Math.floor(Math.log10(raw));
|
||||
const step = [1, 2, 5, 10].map((m) => m * mag).find((v) => v >= raw) ?? raw;
|
||||
const out: number[] = [];
|
||||
for (let v = Math.floor(lo / step) * step; v <= hi + step * 1e-6; v += step)
|
||||
out.push(Math.round(v * 1e6) / 1e6);
|
||||
if (out[out.length - 1] < hi) out.push(out[out.length - 1] + step);
|
||||
return out;
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
입력 = E02 지반 조건(`ground.json`) · E03 2D 파생 저장본(`stab2d_summary` 가장 최근 · 측점별
|
||||
`stab2d_station`) · 대표 단면(E03 자동 + `representative.json` 켬/끔) · E04 3D 저장본 요약
|
||||
(있으면) · E07 광역 저장본(있으면) · E03 보강 검토 저장본(있으면 · 없으면 s8 뺌) ·
|
||||
E06 암반 해석 저장본(있으면 · 없으면 s11 뺌) ·
|
||||
E06 암반 해석 저장본(있으면 · 없으면 s11 뺌) · E03 확률 해석 저장본(있으면 · 없으면 s12 뺌) ·
|
||||
E01 복사본(측점 · 횡단 · 노선).
|
||||
양식 = `resources/master_template/report/stab_report.json`
|
||||
칸 자리 = 같은 폴더 `stab_report_slots.md`.
|
||||
|
||||
몫: 사용자(표지 s1 · 개요 s2) = 처음 만들 때만 채움 ·
|
||||
프로그램(목차 s9 · 입력조건 s3 · 측점표 s4 · 대표단면 s5 · 3D요약 s6 · 광역요약 s7 · 보강검토 s8 ·
|
||||
암반검토 s11 · 부록_절편 s10) = [보고서 갱신] 때 양식에서 통째로 다시 씀. 대표 단면 · 부록 = `_Section` ·
|
||||
암반검토 s11 · 확률해석 s12 · 부록_절편 s10) = [보고서 갱신] 때 양식에서 통째로 다시 씀. 대표 단면 · 부록 = `_Section` ·
|
||||
목차 쪽 번호 · 머리말/꼬리말 · 긴 표 쪽 나눔 = `_Print` · 칸 공용 = `_Cells`.
|
||||
판정 글 칸 = 엔진 판정 값 + N.G 서식 10 · O.K 서식 11 을 직접 붙임
|
||||
(인쇄 HTML · xlsx 는 조건부 서식을 안 먹음).
|
||||
@@ -38,6 +38,7 @@ from E04_Stab3D import E04_Stab3D_Run as run3d
|
||||
from E05_StabReport import E05_StabReport_Area as area
|
||||
from E05_StabReport import E05_StabReport_Figure as figure
|
||||
from E05_StabReport import E05_StabReport_Print as printer
|
||||
from E05_StabReport import E05_StabReport_Prob as prob
|
||||
from E05_StabReport import E05_StabReport_Reinforce as reinforce
|
||||
from E05_StabReport import E05_StabReport_Rock as rock
|
||||
from E05_StabReport import E05_StabReport_Section as section
|
||||
@@ -62,7 +63,7 @@ FORM_PATH = ROOT / "resources" / "master_template" / "report" / "stab_report.jso
|
||||
STAGE = "E05_StabReport"
|
||||
REPORT, META = "report.json", "report_meta.json"
|
||||
USER_SHEETS = ("s1", "s2")
|
||||
PROGRAM_SHEETS = ("s9", "s3", "s4", "s5", "s6", "s7", "s8", "s11", "s10")
|
||||
PROGRAM_SHEETS = ("s9", "s3", "s4", "s5", "s6", "s7", "s8", "s11", "s12", "s10")
|
||||
REPRESENTATIVE = Path("E03_StabSection") / "representative.json"
|
||||
BAND_KEY = "fig/3d_band.png"
|
||||
|
||||
@@ -170,6 +171,7 @@ def analysis_fingerprint(project_root: Path) -> str:
|
||||
"area": area.stamp(project_root),
|
||||
"reinforce": reinforce.stamp(project_root),
|
||||
"rock": rock.stamp(project_root),
|
||||
"prob": prob.stamp(project_root),
|
||||
"form": hashlib.sha1(FORM_PATH.read_bytes()).hexdigest(),
|
||||
}
|
||||
text = json.dumps(body, ensure_ascii=False, sort_keys=True, default=str)
|
||||
@@ -221,6 +223,7 @@ def gather(project_root: Path) -> dict[str, Any]:
|
||||
"area": area.read(project_root),
|
||||
"reinforce": reinforce.read(project_root),
|
||||
"rock": rock.read(project_root),
|
||||
"prob": prob.read(project_root),
|
||||
"fingerprint": analysis_fingerprint(project_root),
|
||||
}
|
||||
|
||||
@@ -453,6 +456,10 @@ def build_program(book: dict[str, Any], inputs: dict[str, Any]) -> list[tuple[st
|
||||
rock.fill_rock(by["s11"], inputs["rock"], _labels(inputs["stations"]))
|
||||
else: # 암반 해석이 없으면 시트를 뺌
|
||||
book["시트"] = [s for s in book["시트"] if s["id"] != "s11"]
|
||||
if inputs.get("prob"):
|
||||
prob.fill_prob(by["s12"], inputs["prob"], _labels(inputs["stations"]))
|
||||
else: # 확률 해석이 없으면 시트를 뺌
|
||||
book["시트"] = [s for s in book["시트"] if s["id"] != "s12"]
|
||||
printer.fill_toc(by["s9"], book, None) # 쪽 번호 = 저장 전 `printer.number_toc`
|
||||
return jobs
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ TITLES = {
|
||||
"s7": "광역 자연사면 위험 요약",
|
||||
"s8": "보강 검토 — 비탈 완화 · 소단 · 벽 외적 안정",
|
||||
"s11": "암반 비탈면 검토 — 평사투영 · 평면 · 쐐기 안전율",
|
||||
"s12": "확률 해석 · 민감도 — 파괴확률 · 신뢰도 지수",
|
||||
"s10": "대표 단면 절편 계산표",
|
||||
}
|
||||
APPENDIX = {"s10"}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""E05 보고서 s12 확률해석 — E03 확률 해석 저장본(대표 단면 · 비탈 · 조건마다 가장 최근) → 시트(PLAN 537-1).
|
||||
|
||||
칸 자리 = `resources/master_template/report/stab_report_slots.md` s12.
|
||||
저장본이 없으면 시트를 뺌(있을 때만) — `build_program` 이 책에서 지움.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from E03_StabSection.E03_StabSection_Router_Prob import load_defaults, records
|
||||
from E05_StabReport.E05_StabReport_Cells import _set, mark
|
||||
|
||||
CONDITION_NAMES = {"dry": "건기", "wet": "우기", "seismic": "지진"}
|
||||
SIDE_NAMES = {"left": "좌", "right": "우"}
|
||||
ROLE_NAMES = {"cut": "깎기", "fill": "쌓기"}
|
||||
METHOD_NAMES = {
|
||||
"ordinary": "Fellenius",
|
||||
"bishop": "Bishop",
|
||||
"janbu": "Janbu 보정",
|
||||
"spencer": "Spencer",
|
||||
"morgenstern_price": "M-P",
|
||||
}
|
||||
MODE_NAMES = {"fixed": "임계면 고정", "search": "매회 탐색"}
|
||||
DIST_NAMES = {"normal": "정규", "lognormal": "대수정규"}
|
||||
COLS = "ABCDEFGHIJK"
|
||||
|
||||
|
||||
def read(project_root: Path) -> list[dict[str, Any]]:
|
||||
return records(Path(project_root))
|
||||
|
||||
|
||||
def stamp(project_root: Path) -> str:
|
||||
"""해석 지문용 — 저장본 목록 내용(표본 F 뺌)."""
|
||||
body = [
|
||||
{**r, "result": {k: v for k, v in r["result"].items() if k != "F"}}
|
||||
for r in read(project_root)
|
||||
]
|
||||
text = json.dumps(body, ensure_ascii=False, sort_keys=True, default=str)
|
||||
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _is_default(rec: dict[str, Any], defaults: dict[str, Any]) -> bool:
|
||||
"""요청 변수 · 표본 설정이 기본값 후보 그대로인지(「사용자 확정 전」 표기)."""
|
||||
req = rec["request"]
|
||||
props, own = defaults["properties"], defaults.get("materials") or {}
|
||||
for v in req["variables"]:
|
||||
base = {
|
||||
**props[v["prop"]],
|
||||
**((own.get(v.get("material") or "") or {}).get(v["prop"]) or {}),
|
||||
}
|
||||
spread = v.get("cov") if v.get("cov") is not None else v.get("std")
|
||||
want = base.get("cov") if "cov" in base else base.get("std")
|
||||
if (
|
||||
v["dist"] != base["dist"]
|
||||
or spread != want
|
||||
or v.get("trunc_sigma") != base["trunc_sigma"]
|
||||
):
|
||||
return False
|
||||
run = defaults["run"]
|
||||
same = all(req.get(k) == run[k] for k in ("n", "sampling", "seed", "mode", "method"))
|
||||
return same and req.get("rho_c_phi", 0.0) == defaults["rho_c_phi"]
|
||||
|
||||
|
||||
def fill_prob(sheet: dict[str, Any], recs: list[dict[str, Any]], labels: dict) -> None:
|
||||
"""s12 — 1. 측점 · 비탈 · 조건마다 한 줄(6 행 서식) · 2. 변수마다 한 줄(10 행 서식 · 토네이도 순)."""
|
||||
tpl = {
|
||||
r: {c: copy.deepcopy(sheet["칸"].get(f"{c}{r}") or {}) for c in COLS} for r in (6, 8, 9, 10)
|
||||
}
|
||||
for a1 in [k for k in sheet["칸"] if int("".join(ch for ch in k if ch.isdigit())) >= 6]:
|
||||
del sheet["칸"][a1]
|
||||
sheet["병합"] = [
|
||||
m for m in sheet["병합"] if int("".join(ch for ch in m.split(":")[0] if ch.isdigit())) < 6
|
||||
]
|
||||
sheet["행"] = {k: v for k, v in sheet["행"].items() if int(k) < 6}
|
||||
|
||||
def put_row(r: int, styles: dict[str, dict]) -> None:
|
||||
for c, cell in styles.items():
|
||||
sheet["칸"][f"{c}{r}"] = copy.deepcopy(cell)
|
||||
|
||||
def name(rec: dict[str, Any]) -> str:
|
||||
ch = float(rec["chainage_m"])
|
||||
return rec.get("label") or labels.get(round(ch, 3)) or f"{ch:.2f}"
|
||||
|
||||
def where(rec: dict[str, Any]) -> str:
|
||||
return (
|
||||
f"{SIDE_NAMES[rec['side']]} {ROLE_NAMES.get(rec['role'], rec['role'])} · "
|
||||
f"{CONDITION_NAMES.get(rec['condition'], rec['condition'])}"
|
||||
)
|
||||
|
||||
defaults = load_defaults()
|
||||
same = all(_is_default(rec, defaults) for rec in recs)
|
||||
_set(sheet, "A3", mark("변동계수 · 분포 · 표본 = 문헌 기본값 후보(근거 E03 탭)", same))
|
||||
pct = lambda v: None if v is None else 100.0 * float(v) # noqa: E731
|
||||
r = 6
|
||||
for rec in recs:
|
||||
res, st = rec["result"], rec["result"]["stats"]
|
||||
put_row(r, tpl[6])
|
||||
_set(sheet, f"A{r}", name(rec))
|
||||
_set(sheet, f"B{r}", where(rec))
|
||||
_set(
|
||||
sheet,
|
||||
f"C{r}",
|
||||
f"{METHOD_NAMES.get(res['method'], res['method'])} · {MODE_NAMES[res['mode']]}",
|
||||
)
|
||||
_set(sheet, f"D{r}", int(st["n_valid"]))
|
||||
_set(sheet, f"E{r}", rec.get("fs_deterministic"))
|
||||
for c, k in zip("FG", ("mean", "std")):
|
||||
_set(sheet, f"{c}{r}", st.get(k))
|
||||
_set(sheet, f"H{r}", pct(st.get("pf")))
|
||||
_set(sheet, f"I{r}", st.get("beta_normal"))
|
||||
_set(sheet, f"J{r}", st.get("beta_lognormal"))
|
||||
_set(sheet, f"K{r}", pct(st.get("p_below_required")))
|
||||
r += 1
|
||||
r += 1
|
||||
put_row(r, tpl[8])
|
||||
sheet["병합"].append(f"A{r}:K{r}")
|
||||
sheet["행"][str(r)] = {"높이": 22}
|
||||
sheet["행"][str(r + 1)] = {"높이": 30}
|
||||
put_row(r + 1, tpl[9])
|
||||
r += 2
|
||||
for rec in recs:
|
||||
res = rec["result"]
|
||||
curves = {c["name"]: c for c in res["sensitivity"]["curves"]}
|
||||
rank = {t["name"]: i + 1 for i, t in enumerate(res["sensitivity"]["tornado"])}
|
||||
for v in sorted(res["variables"], key=lambda v: rank.get(v["name"], 99)):
|
||||
c = curves.get(v["name"]) or {}
|
||||
xs = c.get("x") or [None]
|
||||
put_row(r, tpl[10])
|
||||
_set(sheet, f"A{r}", f"{name(rec)} · {where(rec)}")
|
||||
_set(sheet, f"B{r}", v["name"])
|
||||
_set(sheet, f"C{r}", DIST_NAMES.get(v["dist"], v["dist"]))
|
||||
_set(sheet, f"D{r}", float(v["mean"]))
|
||||
_set(sheet, f"E{r}", float(v["std"]))
|
||||
_set(sheet, f"F{r}", v["lo"] / v["std"] if v.get("lo") is not None else "-")
|
||||
_set(sheet, f"G{r}", xs[0])
|
||||
_set(sheet, f"H{r}", c.get("F_lo"))
|
||||
_set(sheet, f"I{r}", xs[-1])
|
||||
_set(sheet, f"J{r}", c.get("F_hi"))
|
||||
_set(sheet, f"K{r}", rank.get(v["name"]))
|
||||
r += 1
|
||||
@@ -149,6 +149,9 @@ SHEET_SURFACE_METHODS = [
|
||||
).split(",")
|
||||
if method.strip()
|
||||
]
|
||||
# D 예정노선 [분석] 연도 구간을 나란히 셀 프로세스 수 상한(프로세스 하나 약 0.8 GB) — 여럿이 동시에
|
||||
# 돌리면 줄임.
|
||||
PLAN_ANALYZE_WORKERS = int(os.getenv("PLAN_ANALYZE_WORKERS", "3"))
|
||||
# 확정에 쓸 기본 방식 — 라플라스 (2026-08-30 사용자 확정).
|
||||
SHEET_SURFACE_DEFAULT_METHOD = os.getenv("SHEET_SURFACE_DEFAULT_METHOD", "laplace")
|
||||
# 자동 전처리가 만드는 도엽 방식 — 기본 하나뿐이다. 여섯 방식을 매번 다 만들면
|
||||
|
||||
@@ -86,6 +86,7 @@ from E05_StabReport.E05_StabReport_Router import router as e05_stab_report_route
|
||||
from E07_StabArea.E07_StabArea_Router import router as e07_stab_area_router
|
||||
from E03_StabSection.E03_StabSection_Router import router as e03_stab_section_router
|
||||
from E03_StabSection.E03_StabSection_Router_WhatIf import router as e03_stab_whatif_router
|
||||
from E03_StabSection.E03_StabSection_Router_Prob import router as e03_stab_prob_router
|
||||
from E06_StabRock.E06_StabRock_Router import router as e06_stab_rock_router
|
||||
from M01_MasterData.M01_MasterData_Router import router as m01_master_data_router
|
||||
from M02_MasterTemplete.M02_Labor_Router import router as m02_labor_router
|
||||
@@ -499,6 +500,7 @@ app.include_router(e05_stab_report_router, dependencies=protected_with_company)
|
||||
app.include_router(e07_stab_area_router, dependencies=protected_with_company)
|
||||
app.include_router(e03_stab_section_router, dependencies=protected_with_company)
|
||||
app.include_router(e03_stab_whatif_router, dependencies=protected_with_company)
|
||||
app.include_router(e03_stab_prob_router, dependencies=protected_with_company)
|
||||
app.include_router(e06_stab_rock_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_corridor_router, dependencies=protected_with_company)
|
||||
app.include_router(b05_route_replan_router, dependencies=protected_with_company)
|
||||
|
||||
@@ -4133,6 +4133,452 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s12",
|
||||
"이름": "확률해석",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"서식": 1,
|
||||
"값": "확률 해석 · 민감도 (대표 단면)"
|
||||
},
|
||||
"B1": {
|
||||
"서식": 1
|
||||
},
|
||||
"C1": {
|
||||
"서식": 1
|
||||
},
|
||||
"D1": {
|
||||
"서식": 1
|
||||
},
|
||||
"E1": {
|
||||
"서식": 1
|
||||
},
|
||||
"F1": {
|
||||
"서식": 1
|
||||
},
|
||||
"G1": {
|
||||
"서식": 1
|
||||
},
|
||||
"H1": {
|
||||
"서식": 1
|
||||
},
|
||||
"I1": {
|
||||
"서식": 1
|
||||
},
|
||||
"J1": {
|
||||
"서식": 1
|
||||
},
|
||||
"K1": {
|
||||
"서식": 1
|
||||
},
|
||||
"A2": {
|
||||
"서식": 2,
|
||||
"값": "물성 = 확률 변수(문헌 추정 변동계수) · Pf = P(Fs < 1) · β 정규 = (μ − 1)/σ · β 대수정규 = ln(μ/√(1+V²))/√ln(1+V²) — Slide2 · SLOPE/W Probabilistic 과 같은 정의"
|
||||
},
|
||||
"B2": {
|
||||
"서식": 2
|
||||
},
|
||||
"C2": {
|
||||
"서식": 2
|
||||
},
|
||||
"D2": {
|
||||
"서식": 2
|
||||
},
|
||||
"E2": {
|
||||
"서식": 2
|
||||
},
|
||||
"F2": {
|
||||
"서식": 2
|
||||
},
|
||||
"G2": {
|
||||
"서식": 2
|
||||
},
|
||||
"H2": {
|
||||
"서식": 2
|
||||
},
|
||||
"I2": {
|
||||
"서식": 2
|
||||
},
|
||||
"J2": {
|
||||
"서식": 2
|
||||
},
|
||||
"K2": {
|
||||
"서식": 2
|
||||
},
|
||||
"A3": {
|
||||
"서식": 12
|
||||
},
|
||||
"B3": {
|
||||
"서식": 12
|
||||
},
|
||||
"C3": {
|
||||
"서식": 12
|
||||
},
|
||||
"D3": {
|
||||
"서식": 12
|
||||
},
|
||||
"E3": {
|
||||
"서식": 12
|
||||
},
|
||||
"F3": {
|
||||
"서식": 12
|
||||
},
|
||||
"G3": {
|
||||
"서식": 12
|
||||
},
|
||||
"H3": {
|
||||
"서식": 12
|
||||
},
|
||||
"I3": {
|
||||
"서식": 12
|
||||
},
|
||||
"J3": {
|
||||
"서식": 12
|
||||
},
|
||||
"K3": {
|
||||
"서식": 12
|
||||
},
|
||||
"A4": {
|
||||
"서식": 3,
|
||||
"값": "1. 측점별 결과 — 안전율 분포 · 파괴확률 · 신뢰도 지수"
|
||||
},
|
||||
"B4": {
|
||||
"서식": 3
|
||||
},
|
||||
"C4": {
|
||||
"서식": 3
|
||||
},
|
||||
"D4": {
|
||||
"서식": 3
|
||||
},
|
||||
"E4": {
|
||||
"서식": 3
|
||||
},
|
||||
"F4": {
|
||||
"서식": 3
|
||||
},
|
||||
"G4": {
|
||||
"서식": 3
|
||||
},
|
||||
"H4": {
|
||||
"서식": 3
|
||||
},
|
||||
"I4": {
|
||||
"서식": 3
|
||||
},
|
||||
"J4": {
|
||||
"서식": 3
|
||||
},
|
||||
"K4": {
|
||||
"서식": 3
|
||||
},
|
||||
"A5": {
|
||||
"서식": 6,
|
||||
"값": "측점"
|
||||
},
|
||||
"B5": {
|
||||
"서식": 6,
|
||||
"값": "비탈 · 조건"
|
||||
},
|
||||
"C5": {
|
||||
"서식": 6,
|
||||
"값": "방법 · 임계면"
|
||||
},
|
||||
"D5": {
|
||||
"서식": 6,
|
||||
"값": "표본"
|
||||
},
|
||||
"E5": {
|
||||
"서식": 6,
|
||||
"값": "결정론 Fs"
|
||||
},
|
||||
"F5": {
|
||||
"서식": 6,
|
||||
"값": "Fs 평균"
|
||||
},
|
||||
"G5": {
|
||||
"서식": 6,
|
||||
"값": "표준편차"
|
||||
},
|
||||
"H5": {
|
||||
"서식": 6,
|
||||
"값": "Pf %"
|
||||
},
|
||||
"I5": {
|
||||
"서식": 6,
|
||||
"값": "β 정규"
|
||||
},
|
||||
"J5": {
|
||||
"서식": 6,
|
||||
"값": "β 대수정규"
|
||||
},
|
||||
"K5": {
|
||||
"서식": 6,
|
||||
"값": "P(F<기준) %"
|
||||
},
|
||||
"A6": {
|
||||
"서식": 7
|
||||
},
|
||||
"B6": {
|
||||
"서식": 7
|
||||
},
|
||||
"C6": {
|
||||
"서식": 5
|
||||
},
|
||||
"D6": {
|
||||
"서식": 9
|
||||
},
|
||||
"E6": {
|
||||
"서식": 8
|
||||
},
|
||||
"F6": {
|
||||
"서식": 8
|
||||
},
|
||||
"G6": {
|
||||
"서식": 8
|
||||
},
|
||||
"H6": {
|
||||
"서식": 8
|
||||
},
|
||||
"I6": {
|
||||
"서식": 8
|
||||
},
|
||||
"J6": {
|
||||
"서식": 8
|
||||
},
|
||||
"K6": {
|
||||
"서식": 8
|
||||
},
|
||||
"A8": {
|
||||
"서식": 3,
|
||||
"값": "2. 확률 변수 · 민감도 — 토네이도 순(나머지 평균 · 임계면 고정 · 범위 끝 Fs)"
|
||||
},
|
||||
"B8": {
|
||||
"서식": 3
|
||||
},
|
||||
"C8": {
|
||||
"서식": 3
|
||||
},
|
||||
"D8": {
|
||||
"서식": 3
|
||||
},
|
||||
"E8": {
|
||||
"서식": 3
|
||||
},
|
||||
"F8": {
|
||||
"서식": 3
|
||||
},
|
||||
"G8": {
|
||||
"서식": 3
|
||||
},
|
||||
"H8": {
|
||||
"서식": 3
|
||||
},
|
||||
"I8": {
|
||||
"서식": 3
|
||||
},
|
||||
"J8": {
|
||||
"서식": 3
|
||||
},
|
||||
"K8": {
|
||||
"서식": 3
|
||||
},
|
||||
"A9": {
|
||||
"서식": 6,
|
||||
"값": "측점 · 비탈 · 조건"
|
||||
},
|
||||
"B9": {
|
||||
"서식": 6,
|
||||
"값": "변수"
|
||||
},
|
||||
"C9": {
|
||||
"서식": 6,
|
||||
"값": "분포"
|
||||
},
|
||||
"D9": {
|
||||
"서식": 6,
|
||||
"값": "평균"
|
||||
},
|
||||
"E9": {
|
||||
"서식": 6,
|
||||
"값": "표준편차"
|
||||
},
|
||||
"F9": {
|
||||
"서식": 6,
|
||||
"값": "절단 ±σ"
|
||||
},
|
||||
"G9": {
|
||||
"서식": 6,
|
||||
"값": "아래 끝"
|
||||
},
|
||||
"H9": {
|
||||
"서식": 6,
|
||||
"값": "Fs(아래)"
|
||||
},
|
||||
"I9": {
|
||||
"서식": 6,
|
||||
"값": "위 끝"
|
||||
},
|
||||
"J9": {
|
||||
"서식": 6,
|
||||
"값": "Fs(위)"
|
||||
},
|
||||
"K9": {
|
||||
"서식": 6,
|
||||
"값": "순위"
|
||||
},
|
||||
"A10": {
|
||||
"서식": 7
|
||||
},
|
||||
"B10": {
|
||||
"서식": 5
|
||||
},
|
||||
"C10": {
|
||||
"서식": 7
|
||||
},
|
||||
"D10": {
|
||||
"서식": 8
|
||||
},
|
||||
"E10": {
|
||||
"서식": 8
|
||||
},
|
||||
"F10": {
|
||||
"서식": 16
|
||||
},
|
||||
"G10": {
|
||||
"서식": 8
|
||||
},
|
||||
"H10": {
|
||||
"서식": 8
|
||||
},
|
||||
"I10": {
|
||||
"서식": 8
|
||||
},
|
||||
"J10": {
|
||||
"서식": 8
|
||||
},
|
||||
"K10": {
|
||||
"서식": 9
|
||||
},
|
||||
"A12": {
|
||||
"서식": 5,
|
||||
"값": "확률 해석 없음 — E03 [확률 · 민감도] 뒤 [보고서 갱신]"
|
||||
},
|
||||
"B12": {
|
||||
"서식": 5
|
||||
},
|
||||
"C12": {
|
||||
"서식": 5
|
||||
},
|
||||
"D12": {
|
||||
"서식": 5
|
||||
},
|
||||
"E12": {
|
||||
"서식": 5
|
||||
},
|
||||
"F12": {
|
||||
"서식": 5
|
||||
},
|
||||
"G12": {
|
||||
"서식": 5
|
||||
},
|
||||
"H12": {
|
||||
"서식": 5
|
||||
},
|
||||
"I12": {
|
||||
"서식": 5
|
||||
},
|
||||
"J12": {
|
||||
"서식": 5
|
||||
},
|
||||
"K12": {
|
||||
"서식": 5
|
||||
}
|
||||
},
|
||||
"병합": [
|
||||
"A1:K1",
|
||||
"A2:K2",
|
||||
"A3:K3",
|
||||
"A4:K4",
|
||||
"A8:K8",
|
||||
"A12:K12"
|
||||
],
|
||||
"열": {
|
||||
"A": {
|
||||
"폭": 13
|
||||
},
|
||||
"B": {
|
||||
"폭": 16
|
||||
},
|
||||
"C": {
|
||||
"폭": 18
|
||||
},
|
||||
"D": {
|
||||
"폭": 7
|
||||
},
|
||||
"E": {
|
||||
"폭": 8
|
||||
},
|
||||
"F": {
|
||||
"폭": 8
|
||||
},
|
||||
"G": {
|
||||
"폭": 8
|
||||
},
|
||||
"H": {
|
||||
"폭": 8
|
||||
},
|
||||
"I": {
|
||||
"폭": 8
|
||||
},
|
||||
"J": {
|
||||
"폭": 8
|
||||
},
|
||||
"K": {
|
||||
"폭": 8
|
||||
}
|
||||
},
|
||||
"행": {
|
||||
"1": {
|
||||
"높이": 36
|
||||
},
|
||||
"2": {
|
||||
"높이": 30
|
||||
},
|
||||
"3": {
|
||||
"높이": 22
|
||||
},
|
||||
"4": {
|
||||
"높이": 22
|
||||
},
|
||||
"5": {
|
||||
"높이": 30
|
||||
},
|
||||
"8": {
|
||||
"높이": 22
|
||||
},
|
||||
"9": {
|
||||
"높이": 30
|
||||
}
|
||||
},
|
||||
"보기": {
|
||||
"눈금선": false
|
||||
},
|
||||
"인쇄": {
|
||||
"용지": "A4",
|
||||
"방향": "landscape",
|
||||
"맞춤": {
|
||||
"폭": 1,
|
||||
"높이": 0
|
||||
},
|
||||
"여백": {
|
||||
"위": 15,
|
||||
"아래": 15,
|
||||
"왼": 10,
|
||||
"오른": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "s10",
|
||||
"이름": "부록_절편",
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
## 공통
|
||||
|
||||
- 몫: 사용자 몫(표지 · 개요) = 처음 만들 때만 채움 · 다시 만들 때(보고서 갱신) 안 건드림. 프로그램 몫(목차 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · 보강검토 · 암반검토 · 부록_절편) = 갱신 때 통째 다시 씀 · 작업본에 없는 프로그램 몫 시트는 양식 차례 자리에 끼움 · 새 서식은 작업본 서식 표 끝에 붙여 번호를 바꿈.
|
||||
- 시트 차례: 표지 · 목차 · 개요 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · (보강검토) · (암반검토) · 부록_절편.
|
||||
- 몫: 사용자 몫(표지 · 개요) = 처음 만들 때만 채움 · 다시 만들 때(보고서 갱신) 안 건드림. 프로그램 몫(목차 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · 보강검토 · 암반검토 · 확률해석 · 부록_절편) = 갱신 때 통째 다시 씀 · 작업본에 없는 프로그램 몫 시트는 양식 차례 자리에 끼움 · 새 서식은 작업본 서식 표 끝에 붙여 번호를 바꿈.
|
||||
- 시트 차례: 표지 · 목차 · 개요 · 입력조건 · 측점표 · 대표단면 · 3D요약 · 광역요약 · (보강검토) · (암반검토) · (확률해석) · 부록_절편.
|
||||
- PDF: 표지 = 쪽 번호 · 머리말 없음 · 본문 = 머리말(왼 사업명 = 표지 `C8` · 오른 보고서 이름) · 꼬리말 「- n / N -」(목차 = 1 쪽).
|
||||
- 사용자 확정 전 표기 한 규칙 — 기본값 그대로 = 「(기본값 · 사용자 확정 전)」 · 바꿈 = 「(사용자 입력)」 · 물성 = 「문헌 추정치 · 사용자 확정 전」.
|
||||
- 서식 번호(`서식` 표): 1 title · 2 sub · 3 section · 4 label · 5 text · 6 head · 7 body(가운데 글) · 8 fs(0.00) · 9 int(0) · 10 ng(N.G 빨강 채움) · 11 ok(O.K 초록) · 12 notice · 13 num1(#,##0.0) · 14 num0(#,##0) · 15 para(위 맞춤 긴 글) · 16 prop(0.0) · 17 chain(#,##0.00) · 18 head8 · 19 num8(0.00) · 20 num8(0.0) · 21 int8 · 22 목차 내용 · 23 목차 쪽 · 24 목차 장 · 25 sum8(0.00 · 회색) · 26 표지 제목.
|
||||
@@ -126,6 +126,16 @@
|
||||
| 11 · 12 행 꼴(제목 3 · 머리 6) 다음 절리군 하나 = 한 줄(13 행 서식) | A 이름(7) · B 경사방향 · C 경사 · D φj · E cj(16) · F 지속성(13) · G · H 구간 시작 · 끝(7) · 모르면 「-」 · 없으면 「절리군 없음」 |
|
||||
| 절리 줄 다음 한 줄 띄고 15 · 16 행 꼴 · 그 아래 측점 비탈 × 위험 모드 × 조건 = 한 줄(17 행 서식) | A 측점(7) · B 비탈 글(쪽 · 경사 / 방향 · 높이)(5) · C 파괴 형태(평면 · 쐐기 · 전도 + 절리)(7) · D 조건(7) · E Fs · F 기준(8) · G 판정(7 → 10 · 11 · 전도 「검토」) · H 비고(블록 안 생김 · 미끄러짐 면 · 지속성)(5) · 위험 모드 없는 비탈 = 「없음」 한 줄 |
|
||||
|
||||
## s12 확률해석 (프로그램 몫 · E03 확률 해석 저장본 · 없으면 시트를 뺌 · 가로 쪽)
|
||||
|
||||
| 칸 | 내용 |
|
||||
| --- | --- |
|
||||
| `A3` (A:K 병합 · 서식 12) | 「변동계수 · 분포 · 표본 = 문헌 기본값 후보(근거 E03 탭)」 + 사용자 확정 전 표기(모든 기록이 `prob_defaults.json` 그대로면 기본값 · 아니면 사용자 입력) |
|
||||
| 4 · 5 행 꼴(제목 3 · 머리 6) 다음 (측점 · 비탈 · 조건)마다 한 줄(6 행 서식) | A 측점(7) · B 비탈 · 조건(7) · C 방법 · 임계면 고정 / 매회 탐색(5) · D 유효 표본(9) · E 결정론 Fs · F 평균 · G 표준편차 · H Pf % · I β 정규 · J β 대수정규 · K P(F < 기준) %(8) |
|
||||
| 한 줄 띄고 8 · 9 행 꼴 · 그 아래 기록마다 변수 = 한 줄(10 행 서식 · 토네이도 순) | A 측점 · 비탈 · 조건(7) · B 변수(5) · C 분포(7) · D 평균 · E 표준편차(8) · F 절단 ±σ(16 · 없으면 「-」) · G 아래 끝 · H 그때 Fs · I 위 끝 · J 그때 Fs(8) · K 순위(9) |
|
||||
|
||||
- 저장본 = (측점 · 비탈 · 조건)마다 가장 최근 하나 · 지금 2D 결과 측점 지문에 붙은 것만.
|
||||
|
||||
## s10 부록_절편 (프로그램 몫 · 대표 단면마다 한 쪽)
|
||||
|
||||
양식 1~5 행 = 덩이 꼴(1 제목 · 2 단위 · 3 머리 · 4 절편 줄 서식 · 5 합계 서식). 대표 단면마다 지배 조건(Fs / 기준 가장 낮음) 임계면의 절편표.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"version": 1,
|
||||
"note": "확률 해석 기본값 후보 — 문헌 · 상용 SW 예제 값 · 현장 시험 없음 · 사용자 확정 전(E03 확률 · 민감도 탭에서 바꿈)",
|
||||
"run": {
|
||||
"n": 1000,
|
||||
"sampling": "lhs",
|
||||
"seed": 0,
|
||||
"mode": "fixed",
|
||||
"method": "bishop"
|
||||
},
|
||||
"properties": {
|
||||
"c": {
|
||||
"dist": "lognormal",
|
||||
"cov": 0.3,
|
||||
"trunc_sigma": 3.0
|
||||
},
|
||||
"phi": {
|
||||
"dist": "normal",
|
||||
"cov": 0.1,
|
||||
"trunc_sigma": 3.0
|
||||
},
|
||||
"gamma": {
|
||||
"dist": "normal",
|
||||
"cov": 0.05,
|
||||
"trunc_sigma": 3.0
|
||||
},
|
||||
"water": {
|
||||
"dist": "normal",
|
||||
"std": 0.0,
|
||||
"trunc_sigma": 3.0
|
||||
},
|
||||
"ru": {
|
||||
"dist": "normal",
|
||||
"std": 0.0,
|
||||
"trunc_sigma": 3.0
|
||||
}
|
||||
},
|
||||
"rho_c_phi": 0.0,
|
||||
"sources": {
|
||||
"c": "COV 0.30(범위 0.2~0.5) · 대수정규(음수 막음) — 국내 화강풍화토 실측 정남수 외(2011) 0.34~0.49 · 장정윤 · 박혁진(2017) 0.21 · 해석 가정 Cho(2018 · 2019) 0.20 · Lee 외(2020) 0.25 · ISSMGE TC304(2021) 홍콩 완전풍화화강암 0.51 · Harr(1987) 최대 0.40 · 붕적토 0.40 = 국내 자료 없어 국외 범위 위쪽(협의 필요)",
|
||||
"phi": "COV 0.10(범위 0.05~0.15) · 정규 — 국내 화강풍화토 실측 정남수 외(2011) 0.09~0.12 · 장정윤 · 박혁진(2017) 0.03 · Cho(2018 · 2019) · Lee 외(2020) 0.10 · TC304(2021) 모래 평균 0.079 · 홍콩 CDG 0.096 · Duncan(2000) 2~13 %",
|
||||
"gamma": "COV 0.05(범위 0.03~0.07) · 정규 — Duncan(2000) 3~7 % · 홍콩 CDG 2.6 % · Li & Lumb(1987) 5 % · 국내 논문은 고정값으로 둠",
|
||||
"water": "지하수위 이동 표준편차 기본 0(확률 변수 아님) · 수위선 갈래(E02)일 때만",
|
||||
"ru": "r_u 표준편차 기본 0 · r_u 갈래(E02)일 때만 · Li & Lumb(1987) COV 10 %",
|
||||
"rho_c_phi": "기본 0(상관 무시 = 보수적) · Slide2 · Cho(2019) 기본 −0.5 · 문헌 −0.25~−0.75(Wolff 1985 −0.47 · Lumb 1970 −0.7~−0.37)",
|
||||
"trunc_sigma": "평균 ±3σ 절단 — Slide2 「3x 표준편차」 · SLOPE/W James Bay 예제",
|
||||
"run": "LHS 1000 · 임계면 고정(Slide2 Global Minimum) · Bishop — Slide2 Tutorial 08"
|
||||
},
|
||||
"materials": {
|
||||
"colluvium": {
|
||||
"c": {
|
||||
"cov": 0.4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,7 @@ SEGMENTS = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def hill() -> Terrain:
|
||||
def _hill() -> Terrain:
|
||||
x = np.arange(0.0, 1102.0, 2.0)
|
||||
y = np.arange(0.0, 402.0, 2.0)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
@@ -26,6 +25,16 @@ def hill() -> Terrain:
|
||||
return Terrain(x, y, z, np.ones(xx.shape, dtype=bool))
|
||||
|
||||
|
||||
def hill_loader(root, selection, models_dir, cache_dir):
|
||||
"""나란히 셈 프로세스가 이름으로 부르는 지형(모듈 맨 위 함수)."""
|
||||
return _hill(), []
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def hill() -> Terrain:
|
||||
return _hill()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run(tmp_path, hill):
|
||||
_POOLS.clear()
|
||||
@@ -37,7 +46,7 @@ def run(tmp_path, hill):
|
||||
|
||||
def go(criteria=CRITERIA, corridor_m=150.0):
|
||||
return analyze.analyze_plan(
|
||||
tmp_path, SEGMENTS, criteria, loader=loader, corridor_m=corridor_m
|
||||
tmp_path, SEGMENTS, criteria, loader=loader, corridor_m=corridor_m, workers=1
|
||||
)
|
||||
|
||||
go.calls = calls
|
||||
@@ -91,6 +100,54 @@ def test_same_input_reads_store(run, monkeypatch) -> None:
|
||||
run(criteria={**CRITERIA, "max_grade": 0.12})
|
||||
|
||||
|
||||
def test_parallel_years_match_sequential(tmp_path) -> None:
|
||||
"""연도 구간 나란히(프로세스 둘) = 차례로 — 갈래 넷 노선 좌표 · 잰 값 차 0(531-6)."""
|
||||
_POOLS.clear()
|
||||
seq = analyze.analyze_plan(
|
||||
tmp_path / "a", SEGMENTS, CRITERIA, loader=hill_loader, corridor_m=150.0, workers=1
|
||||
)
|
||||
_POOLS.clear()
|
||||
steps = []
|
||||
par = analyze.analyze_plan(
|
||||
tmp_path / "b",
|
||||
SEGMENTS,
|
||||
CRITERIA,
|
||||
loader=hill_loader,
|
||||
corridor_m=150.0,
|
||||
workers=2,
|
||||
on_progress=lambda p, m: steps.append(m),
|
||||
)
|
||||
assert [r["routes"] for r in par] == [r["routes"] for r in seq]
|
||||
assert [r["fingerprint"] for r in par] == [r["fingerprint"] for r in seq]
|
||||
assert "나란히 2" in steps[0] and steps[-1] == "분석 끝"
|
||||
# 저장본은 제 프로세스에서 지문만 보고 그대로
|
||||
again = analyze.analyze_plan(
|
||||
tmp_path / "b", SEGMENTS, CRITERIA, loader=hill_loader, corridor_m=150.0, workers=2
|
||||
)
|
||||
assert [r["stored"] for r in again] == [True, True]
|
||||
_POOLS.clear()
|
||||
|
||||
|
||||
def test_near_streams_keeps_every_nearest_line(hill) -> None:
|
||||
"""계류 거르기 — 지형 상자 안 점의 계류 거리 · 건너기가 통째와 같고 먼 선은 빠짐(531-6)."""
|
||||
import shapely
|
||||
from shapely.geometry import LineString, MultiLineString
|
||||
|
||||
rng = np.random.default_rng(7)
|
||||
streams = []
|
||||
for _ in range(80): # 지형(1.1 × 0.4 km) 둘레 5 km 에 흩은 계류
|
||||
start = rng.uniform(-2000.0, 3000.0, 2)
|
||||
streams.append([tuple(start + step) for step in np.cumsum(rng.normal(0, 60, (6, 2)), 0)])
|
||||
kept = analyze.near_streams(hill, streams)
|
||||
assert 0 < len(kept) < len(streams)
|
||||
points = shapely.points(rng.uniform((0.0, 0.0), (1100.0, 400.0), (4000, 2)))
|
||||
whole, part = MultiLineString(streams), MultiLineString(kept)
|
||||
assert np.array_equal(shapely.distance(points, whole), shapely.distance(points, part))
|
||||
route = LineString([(0.0, 0.0), (1100.0, 400.0)])
|
||||
assert route.intersection(whole).equals(route.intersection(part))
|
||||
assert analyze.near_streams(hill, []) == []
|
||||
|
||||
|
||||
def test_corridor_argument_keeps_b_default(hill) -> None:
|
||||
line = [SEGMENTS[0]["start"], SEGMENTS[0]["end"]]
|
||||
base = SearchGrid(line, hill, [], stream_offset_m=10.0)
|
||||
|
||||
@@ -38,7 +38,7 @@ PRINT_KEYS = {"용지", "방향", "배율", "맞춤", "여백", "머리행", "
|
||||
A1 = re.compile(r"^([A-Z]{1,3})(\d+)$")
|
||||
NAMES = [
|
||||
"표지", "목차", "개요", "입력조건", "측점표", "대표단면", "3D요약", "광역요약", "보강검토", "암반검토",
|
||||
"부록_절편",
|
||||
"확률해석", "부록_절편",
|
||||
] # fmt: skip
|
||||
|
||||
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
@@ -151,7 +151,7 @@ def test_양식_계약():
|
||||
book = _form()
|
||||
_contract(book)
|
||||
assert [s["이름"] for s in book["시트"]] == NAMES
|
||||
ids = ["s1", "s9", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s11", "s10"]
|
||||
ids = ["s1", "s9", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s11", "s12", "s10"]
|
||||
assert [s["id"] for s in book["시트"]] == ids
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ def test_양식_인쇄_쪽나눔_머리행_그림():
|
||||
"광역요약": 0,
|
||||
"보강검토": 0,
|
||||
"암반검토": 0,
|
||||
"확률해석": 0,
|
||||
}
|
||||
# N.G 빨강 조건부 서식 — 측점표 판정 열 · 서식 표에 N.G 칸 서식(빌더가 인쇄 · xlsx 용으로 직접 씀)
|
||||
assert by["측점표"]["조건부서식"][0]["범위"][0]["c0"] == 9
|
||||
@@ -239,9 +240,9 @@ def test_견본_pdf_쪽수_머리행(png):
|
||||
sections = [i for i, t in enumerate(texts) if "대표 단면" in t and "안전율 Fs" in t]
|
||||
assert len(sections) == 2 # 단면마다 한 쪽
|
||||
# 표지 1 + 목차 1 + 개요 1 + 입력조건 1 + 측점표 n + 대표단면 2 + 3D 1 + 광역 1 + 보강 1 + 암반 1
|
||||
# + 부록 1
|
||||
# + 확률 1 + 부록 1
|
||||
# (양식 빈 판)
|
||||
assert len(pages) == 1 + 1 + 1 + 1 + len(stab) + 2 + 1 + 1 + 1 + 1 + 1
|
||||
assert len(pages) == 1 + 1 + 1 + 1 + len(stab) + 2 + 1 + 1 + 1 + 1 + 1 + 1
|
||||
assert all(float(pages[i].mediabox.width) > float(pages[i].mediabox.height) for i in stab)
|
||||
html = spreadsheet.print_htmls([{"book": book, "figures": {k: png for k in figs}}])[0]
|
||||
assert html.count("<img ") == 3 # 단면 2 + 3D 띠 1
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""537-1 E03 2D 확률 해석 · 민감도 — 엔진(`E03_StabSection_Engine_Prob`).
|
||||
|
||||
- 무한사면 해석해: F = c/(γ z sinβ cosβ) + (1 − m γw/γ) tanφ/tanβ · c · φ 정규(MIT OCW 1.151 App. 17 ·
|
||||
z 5 · m 0.5 · β 35° · c N(25, 5) · φ N(30°, 7.5°)) — Pf · μ · σ 정확값 = φ 위 수치 적분 · LHS 1만 ±2 %
|
||||
· c · tanφ 정규면 F 도 정규 → Pf = Φ(−β) · 절편 경로(임계면 고정 · 건조 무한사면) = 닫힌 꼴.
|
||||
- Slide2 Tutorial 08(Probabilistic Analysis · LHS 1000 · Global Minimum · Bishop): FS 평균 1.145 · σ 0.1206
|
||||
· PF 10.2 % · RI 정규 1.204 · 대수정규 1.238.
|
||||
- Slide2 Verification #36(Li & Lumb 1987 · Hassan & Wolff 1999 · r_u): 결정론 1.340(H&W 1.334) · 평균 1.355 ·
|
||||
PF 1.5 % · RI 대수정규 2.482(H&W 2.336).
|
||||
- 고정 시드 재현 · LHS 층 · 절단 · 대수정규 평균 · 민감도(평균 = 결정론 F · 토네이도 순) · 매회 탐색 갈래.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_537_1_e03_prob.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy import integrate
|
||||
from scipy.stats import norm
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from E03_StabSection import E03_StabSection_Engine_Prob as prob # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Search import analyze # noqa: E402
|
||||
|
||||
|
||||
def var(name, prop, mean, std, rel=None, mat="s", dist="normal"):
|
||||
return {
|
||||
"name": name, "material": mat, "prop": prop, "dist": dist,
|
||||
"mean": mean, "std": std, "lo": rel, "hi": rel,
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def near(v, ref, tol):
|
||||
assert v is not None and abs(v - ref) <= tol * abs(ref), (v, ref)
|
||||
|
||||
|
||||
# ---- 무한사면 해석해 ---------------------------------------------------------------
|
||||
Z, BETA, G, GW = 5.0, math.radians(35.0), 20.0, 9.81
|
||||
C_PHI = [var("c", "c", 25.0, 5.0), var("phi", "phi", 30.0, 7.5)]
|
||||
|
||||
|
||||
def _ab(m):
|
||||
return 1 / (G * Z * math.sin(BETA) * math.cos(BETA)), (1 - m * GW / G) / math.tan(BETA)
|
||||
|
||||
|
||||
def _exact(m):
|
||||
"""φ 위 수치 적분 — Pf = ∫ Φ((1 − b tanφ − a μc)/(a σc)) f(φ) dφ · 평균 · 표준편차."""
|
||||
a, b = _ab(m)
|
||||
f = lambda p: norm.pdf(p, 30.0, 7.5) # noqa: E731
|
||||
F = lambda p: a * 25.0 + b * math.tan(math.radians(p)) # noqa: E731
|
||||
pf = integrate.quad(lambda p: norm.cdf((1 - F(p)) / (a * 5.0)) * f(p), 0, 89, limit=200)[0]
|
||||
mu = integrate.quad(lambda p: F(p) * f(p), 0, 89)[0]
|
||||
m2 = integrate.quad(lambda p: (F(p) ** 2 + (a * 5.0) ** 2) * f(p), 0, 89)[0]
|
||||
return pf, mu, math.sqrt(m2 - mu * mu)
|
||||
|
||||
|
||||
def test_infinite_slope_closed_form_pf_beta():
|
||||
"""MIT App. 17 무한사면 — LHS 1만 Pf · β ±2 % · 평균 · 표준편차 ±1 %(φ 정규 · 각도)."""
|
||||
a, b = _ab(0.5)
|
||||
X = prob.sample(C_PHI, 10000, seed=0)
|
||||
s = prob.statistics(a * X[:, 0] + b * np.tan(np.radians(X[:, 1])))
|
||||
pf, mu, sd = _exact(0.5)
|
||||
near(pf, 0.2252, 0.01) # 같은 문헌 MC 2×10⁶
|
||||
near(s["pf"], pf, 0.02)
|
||||
near(s["beta_normal"], (mu - 1) / sd, 0.02)
|
||||
near(s["mean"], mu, 0.01)
|
||||
near(s["std"], sd, 0.01)
|
||||
V = sd / mu
|
||||
near(
|
||||
s["beta_lognormal"],
|
||||
math.log(mu / math.sqrt(1 + V * V)) / math.sqrt(math.log1p(V * V)),
|
||||
0.02,
|
||||
)
|
||||
|
||||
|
||||
def test_infinite_slope_normal_tanphi_exact():
|
||||
"""c · tanφ 정규 · 독립 → F 정규 → Pf = Φ(−β) 정확(LHS 2만 ±2 %)."""
|
||||
a, b = _ab(0.5)
|
||||
vs = [var("c", "c", 25.0, 5.0), {**var("t", "c", 0.57735, 0.11547), "prop": "x"}]
|
||||
X = prob.sample(vs, 20000, seed=0)
|
||||
s = prob.statistics(a * X[:, 0] + b * X[:, 1])
|
||||
mu = a * 25.0 + b * 0.57735
|
||||
sd = math.hypot(a * 5.0, b * 0.11547)
|
||||
near(mu, 1.154412, 1e-5)
|
||||
near(sd, 0.163756, 1e-4)
|
||||
near(s["beta_normal"], (mu - 1) / sd, 0.02)
|
||||
near(s["pf"], norm.cdf(-(mu - 1) / sd), 0.02)
|
||||
|
||||
|
||||
def _infinite_model():
|
||||
"""건조 무한사면 β 35° · 지표 아래 5 m 나란한 면 · 모형 양 끝까지(끝 효과 없음)."""
|
||||
t, L = math.tan(BETA), 100.0
|
||||
model = {
|
||||
"materials": [{"id": "s", "name": "s", "gamma_t": G, "c": 25.0, "phi": 30.0}],
|
||||
"regions": [{"material_id": "s", "polygon": [[0, 0], [L, -t * L], [L, -t * L - 20], [0, -20]]}],
|
||||
} # fmt: skip
|
||||
return model, {"kind": "polyline", "points": [[0, -Z], [L, -t * L - Z]]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["ordinary", "bishop", "janbu"])
|
||||
def test_fixed_surface_slices_equal_closed_form(method):
|
||||
"""임계면 고정 절편 경로 = 무한사면 닫힌 꼴(물성 바꾼 줄마다) · LHS 1만 Pf ±2 %."""
|
||||
model, surface = _infinite_model()
|
||||
fixed = prob.Fixed(model, surface, 1)
|
||||
a, b = _ab(0.0)
|
||||
X = np.array([[25.0, 30.0], [10.0, 20.0], [40.0, 35.0]])
|
||||
got = fixed.F(C_PHI, X, method)
|
||||
assert got == pytest.approx(a * X[:, 0] + b * np.tan(np.radians(X[:, 1])), rel=1e-9)
|
||||
out = prob.run(model, surface=surface, direction=1, variables=C_PHI, n=10000, method=method)
|
||||
near(out["stats"]["pf"], _exact(0.0)[0], 0.02)
|
||||
|
||||
|
||||
# ---- Slide2 대조 -------------------------------------------------------------------
|
||||
T08 = {
|
||||
"materials": [{"id": "s", "name": "s", "gamma_t": 19.0, "c": 5.0, "phi": 30.0}],
|
||||
"regions": [{"material_id": "s", "polygon": [
|
||||
[0, 0], [130, 0], [130, 50], [80, 50], [50, 30], [0, 30]]}],
|
||||
} # fmt: skip
|
||||
T08_VARS = [var("c", "c", 5, 1, 3), var("phi", "phi", 30, 3, 9), var("g", "gamma", 19, 0.5, 1.5)]
|
||||
|
||||
|
||||
def _surface(model, method="bishop"):
|
||||
return analyze(model, direction=-1, noncircular=False, methods=(method,))["methods"][method][
|
||||
"min"
|
||||
]
|
||||
|
||||
|
||||
def test_slide2_tutorial08_global_minimum():
|
||||
"""Tutorial 08 — LHS 1000 · 고정 시드 셋 평균: FS 평균 ±1 % · σ ±3 % · PF ±2 %p · RI ±3 %."""
|
||||
surface = _surface(T08)
|
||||
near(surface["F"], 1.142, 0.01)
|
||||
runs = [
|
||||
prob.run(T08, surface=surface, direction=-1, variables=T08_VARS, n=1000, seed=k)["stats"]
|
||||
for k in range(3)
|
||||
]
|
||||
avg = {
|
||||
k: float(np.mean([r[k] for r in runs]))
|
||||
for k in ("mean", "std", "pf", "beta_normal", "beta_lognormal")
|
||||
}
|
||||
near(avg["mean"], 1.145, 0.01)
|
||||
near(avg["std"], 0.1206, 0.03)
|
||||
assert abs(avg["pf"] - 0.102) <= 0.02
|
||||
near(avg["beta_normal"], 1.204, 0.03)
|
||||
near(avg["beta_lognormal"], 1.238, 0.03)
|
||||
|
||||
|
||||
def test_slide2_36_li_lumb_ru():
|
||||
"""#36 r_u 확률 변수 · MC 1만 — 결정론 ±1 % · 평균 ±1.5 % · PF ±1 %p · RI 대수정규 = Hassan & Wolff ±3 %."""
|
||||
model = {
|
||||
"materials": [{"id": "s", "name": "s", "gamma_t": 18.0, "c": 18.0, "phi": 30.0}],
|
||||
"regions": [{"material_id": "s", "polygon": [[0, 0], [20, 0], [20, 15], [15, 15], [5, 5], [0, 5]]}],
|
||||
"groundwater": {"mode": "ru", "ru": 0.2},
|
||||
} # fmt: skip
|
||||
surface = _surface(model)
|
||||
near(surface["F"], 1.340, 0.01)
|
||||
vs = [
|
||||
var("c", "c", 18, 3.6), var("phi", "phi", 30, 3), var("g", "gamma", 18, 0.9),
|
||||
var("ru", "ru", 0.2, 0.02, mat=None),
|
||||
] # fmt: skip
|
||||
s = prob.run(model, surface=surface, direction=-1, variables=vs, n=10000, sampling="mc")[
|
||||
"stats"
|
||||
]
|
||||
near(s["mean"], 1.355, 0.015)
|
||||
assert abs(s["pf"] - 0.015) <= 0.01
|
||||
near(s["beta_lognormal"], 2.336, 0.03)
|
||||
|
||||
|
||||
# ---- 표본 · 재현 · 민감도 · 갈래 ---------------------------------------------------------
|
||||
def test_seed_reproducible_and_lhs_strata():
|
||||
"""같은 시드 = 같은 표본 · 다른 시드 = 다름 · LHS = 변수마다 n 칸에 하나씩 · 절단 안 · 상관."""
|
||||
a = prob.sample(T08_VARS, 500, seed=7)
|
||||
assert np.array_equal(a, prob.sample(T08_VARS, 500, seed=7))
|
||||
assert not np.array_equal(a, prob.sample(T08_VARS, 500, seed=8))
|
||||
for j, v in enumerate(T08_VARS):
|
||||
lo, hi = v["mean"] - v["lo"], v["mean"] + v["hi"]
|
||||
assert a[:, j].min() >= lo and a[:, j].max() <= hi
|
||||
pa, pb = norm.cdf(-3), norm.cdf(3)
|
||||
u = (norm.cdf((a[:, j] - v["mean"]) / v["std"]) - pa) / (pb - pa)
|
||||
assert sorted(np.floor(u * 500).astype(int)) == list(range(500))
|
||||
rho = prob.sample(C_PHI, 5000, seed=1, correlations=[(0, 1, -0.5)])
|
||||
assert np.corrcoef(rho.T)[0, 1] == pytest.approx(-0.5, abs=0.03)
|
||||
|
||||
|
||||
def test_truncation_and_lognormal_moments():
|
||||
"""대수정규 = 평균 · σ 그대로 · 음수 없음 · 정규 c 는 물리 한계 0 에서 절단."""
|
||||
ln = prob.sample([var("c", "c", 10.0, 5.0, dist="lognormal")], 20000, seed=2)[:, 0]
|
||||
near(ln.mean(), 10.0, 0.01)
|
||||
near(ln.std(), 5.0, 0.03)
|
||||
assert ln.min() > 0
|
||||
nc = prob.sample([var("c", "c", 5.0, 5.0)], 5000, seed=2)[:, 0]
|
||||
assert nc.min() >= 0
|
||||
|
||||
|
||||
def test_sensitivity_and_tornado():
|
||||
"""민감도 — 평균 줄 F = 결정론 F · 곡선 51 점 · c ↑ = F ↑ · 토네이도 = 폭 큰 순."""
|
||||
surface = _surface(T08)
|
||||
out = prob.run(T08, surface=surface, direction=-1, variables=T08_VARS, n=200)
|
||||
sens = out["sensitivity"]
|
||||
assert sens["F_mean"] == pytest.approx(surface["F"], rel=1e-9)
|
||||
curves = {c["name"]: c for c in sens["curves"]}
|
||||
assert len(curves["c"]["F"]) == 51 and curves["c"]["x"][0] == pytest.approx(2.0)
|
||||
assert np.all(np.diff(curves["c"]["F"]) > 0) and np.all(np.diff(curves["phi"]["F"]) > 0)
|
||||
swings = [t["swing"] for t in sens["tornado"]]
|
||||
assert swings == sorted(swings, reverse=True) and sens["tornado"][0]["name"] in ("c", "phi")
|
||||
assert out["stats"]["n"] == 200 and len(out["F"]) == 200
|
||||
|
||||
|
||||
def test_search_mode_matches_fixed_at_low_variance():
|
||||
"""매회 탐색 — 변동이 아주 작으면 고정면과 같은 F(±1 %) · 표본 수 · 시간 칸."""
|
||||
surface = _surface(T08)
|
||||
tiny = [var("c", "c", 5, 0.01), var("phi", "phi", 30, 0.01)]
|
||||
out = prob.run(T08, surface=surface, direction=-1, variables=tiny, n=4, mode="search")
|
||||
for f in out["F"]:
|
||||
near(f, surface["F"], 0.01)
|
||||
assert out["mode"] == "search" and out["elapsed_s"]["samples"] > 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-q"]))
|
||||
@@ -0,0 +1,295 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""537-1 E03 확률 해석 길(`E03_StabSection_Router_Prob`) — 검증_sub4 동결 복사 · 측점 둘 · Bishop 원호만.
|
||||
|
||||
- 준비(setup): 단면 물성 = E02 값 · 기본값 후보 · 기준안전율 · 결정론 F
|
||||
- 임계면 고정: 평균 줄 F = 저장된 결정론 F · Pf · β · 표본 수 · 같은 요청 = 저장본(다시 안 셈) · 기록 목록
|
||||
- 대표 단면 아님 · 단면에 없는 물성 · 수위선 아닌데 지하수위 변수 · 변수 없음 = 422
|
||||
- 매회 탐색 일꾼(프로세스 풀 · 진행 줄 · 결과 파일)
|
||||
- E05 확률해석 시트(있을 때만 · 결과 = 저장본 · 기본값 그대로 = 사용자 확정 전 · 바꾸면 사용자 입력 표기)
|
||||
- 화면 계산(logic/E03_StabSection_Prob.ts): 기본값 후보 · 물성 덮어쓰기 · 요청 꼴 = 서버 시험 요청
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_537_1_e03_prob_route.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from common_util import common_util_project_copy as cp # noqa: E402
|
||||
from E01_StabRegister import E01_StabRegister_Source as src # noqa: E402
|
||||
from E02_StabGround import E02_StabGround_Router as e02 # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Router as router # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Router_Prob as rp # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Router_WhatIf as wf # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_View as view # noqa: E402
|
||||
from E03_StabSection import E03_StabSection_Worker as worker # noqa: E402
|
||||
from E03_StabSection.E03_StabSection_Engine_Station import analyze_station # noqa: E402
|
||||
|
||||
SOURCE_ID = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(읽기만)
|
||||
PID = "d" * 8 + "-" + "d" * 4 + "-" + "d" * 4 + "-" + "d" * 4 + "-" + "d" * 12
|
||||
|
||||
|
||||
async def _copy(target: Path) -> None:
|
||||
import aiomysql
|
||||
|
||||
from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER
|
||||
|
||||
connection = await aiomysql.connect(
|
||||
host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, autocommit=True
|
||||
)
|
||||
try:
|
||||
await cp.copy_project(
|
||||
connection, SOURCE_ID, scope="stability", new_id=PID, project_root=target
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def _fast(cross, design, **kw):
|
||||
return analyze_station(cross, design, **{**kw, "methods": ("bishop",), "noncircular": False})
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def project():
|
||||
folder = Path(tempfile.mkdtemp(prefix="aislo_e03_prob_"))
|
||||
try:
|
||||
asyncio.run(_copy(folder / "copy"))
|
||||
except Exception as error: # noqa: BLE001
|
||||
shutil.rmtree(folder, ignore_errors=True)
|
||||
pytest.skip(f"검증 원본 복사 실패: {error}")
|
||||
root = folder / "copy"
|
||||
mp = pytest.MonkeyPatch()
|
||||
original = src.stations
|
||||
mp.setattr(router.src, "stations", lambda r: original(r)[20:22])
|
||||
|
||||
async def fake_project(_pid):
|
||||
return root, {"company_id": 1, "project_kind": "STABILITY"}
|
||||
|
||||
mp.setattr(e02, "_project", fake_project)
|
||||
mp.setattr(worker, "analyze_station", _fast)
|
||||
mp.setattr(wf, "analyze_station", _fast)
|
||||
mp.setattr(router, "WORKERS", 1)
|
||||
mp.setattr(rp, "WORKERS", 1)
|
||||
router._JOBS.clear()
|
||||
|
||||
async def go():
|
||||
await router.analyze2d(UUID(PID))
|
||||
while router._TASKS:
|
||||
await asyncio.gather(*list(router._TASKS))
|
||||
|
||||
asyncio.run(go())
|
||||
yield root
|
||||
mp.undo()
|
||||
shutil.rmtree(folder, ignore_errors=True)
|
||||
|
||||
|
||||
def _slot(root: Path) -> tuple[str, str]:
|
||||
"""대표 단면 중 깎기 · 쌓기 비탈 하나."""
|
||||
rows = router._latest_summary(root, "")[0]["stations"]
|
||||
view.mark_representative(rows, router._overrides(root))
|
||||
row = next(r for r in rows if (r.get("representative") or {}).get("on"))
|
||||
return row["key"], row["sides"][0]["side"]
|
||||
|
||||
|
||||
def _req(side, mats=("weathered_soil",), **kw):
|
||||
"""단면 물성마다 c(대수정규 0.3) · φ(0.1) · γ(0.05)."""
|
||||
variables = [
|
||||
v
|
||||
for m in mats
|
||||
for v in (
|
||||
{"material": m, "prop": "c", "dist": "lognormal", "cov": 0.3},
|
||||
{"material": m, "prop": "phi", "cov": 0.1},
|
||||
{"material": m, "prop": "gamma", "cov": 0.05},
|
||||
)
|
||||
]
|
||||
return rp.ProbRequest(
|
||||
**{"side": side, "condition": "dry", "n": 400, "variables": variables, **kw}
|
||||
)
|
||||
|
||||
|
||||
def test_setup_and_fixed_run(project, monkeypatch):
|
||||
key, side = _slot(project)
|
||||
s = rp.setup(project, key, side, "dry")
|
||||
ids = [m["id"] for m in s["materials"]]
|
||||
assert "weathered_soil" in ids and "wall" not in ids
|
||||
assert s["defaults"]["properties"]["c"]["cov"] == 0.3 and s["required"] in (1.5, 1.3)
|
||||
fs = s["fs_deterministic"]["bishop"]
|
||||
seen = []
|
||||
req = _req(side, ids)
|
||||
rec = rp.run_prob(project, key, req, lambda p, m: seen.append(p))
|
||||
res = rec["result"]
|
||||
assert res["sensitivity"]["F_mean"] == pytest.approx(fs, rel=1e-9)
|
||||
assert rec["fs_deterministic"] == fs and res["stats"]["n"] == 400
|
||||
st = res["stats"]
|
||||
assert 0 <= st["pf"] <= 1 and st["std"] > 0 and abs(st["mean"] - fs) < 0.1 * fs
|
||||
assert st["beta_normal"] == pytest.approx((st["mean"] - 1) / st["std"])
|
||||
assert seen and max(seen) <= 95
|
||||
# 같은 요청 = 저장본 · 기록 목록 · setup 의 마지막 결과
|
||||
calls = []
|
||||
monkeypatch.setattr(rp.prob, "run", lambda **kw: calls.append(1))
|
||||
again = rp.run_prob(project, key, req, lambda p, m: None)
|
||||
assert not calls and again["computed_at"] == rec["computed_at"]
|
||||
listed = rp.records(project)
|
||||
assert [(r["station_key"], r["side"], r["condition"]) for r in listed] == [(key, side, "dry")]
|
||||
assert rp.setup(project, key, side, "dry")["last"]["computed_at"] == rec["computed_at"]
|
||||
|
||||
|
||||
def test_rejects(project):
|
||||
key, side = _slot(project)
|
||||
with pytest.raises(HTTPException) as e:
|
||||
rp.run_prob(
|
||||
project, key, _req(side, variables=[{"material": "xx", "prop": "c", "cov": 0.2}]), print
|
||||
)
|
||||
assert e.value.status_code == 422
|
||||
with pytest.raises(HTTPException) as e:
|
||||
rp.run_prob(project, key, _req(side, variables=[{"prop": "water", "std": 0.5}]), print)
|
||||
assert e.value.status_code == 422 and "수위선" in e.value.detail
|
||||
with pytest.raises(HTTPException) as e:
|
||||
rp.run_prob(
|
||||
project,
|
||||
key,
|
||||
_req(side, variables=[{"material": "weathered_soil", "prop": "c", "cov": 0}]),
|
||||
print,
|
||||
)
|
||||
assert "변수가 없습니다" in e.value.detail
|
||||
# 대표 단면 끔 → 422
|
||||
path = project / router.REPRESENTATIVE
|
||||
old = path.read_text(encoding="utf-8") if path.is_file() else None
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"overrides": {key: False}}), encoding="utf-8")
|
||||
try:
|
||||
with pytest.raises(HTTPException) as e:
|
||||
rp.run_prob(project, key, _req(side), print)
|
||||
assert "대표 단면" in e.value.detail
|
||||
finally:
|
||||
path.unlink() if old is None else path.write_text(old, encoding="utf-8")
|
||||
|
||||
|
||||
def test_search_worker_process():
|
||||
"""매회 탐색 일꾼 — 프로세스 풀 둘 · 진행 줄 · 결과 파일(Slide2 Tutorial 08 모형 · 작은 변동)."""
|
||||
from test_537_1_e03_prob import T08, _surface, var
|
||||
|
||||
job = {
|
||||
"model": T08,
|
||||
"surface": _surface(T08),
|
||||
"direction": -1,
|
||||
"limits": None,
|
||||
"variables": [var("c", "c", 5, 0.01), var("phi", "phi", 30, 0.01)],
|
||||
"n": 4,
|
||||
"mode": "search",
|
||||
"workers": 2,
|
||||
}
|
||||
done = []
|
||||
out = rp._run_search(job, done.append)
|
||||
assert sorted(done) == [1, 2, 3, 4] and out["n"] == 4
|
||||
assert all(abs(f - 1.139) < 0.02 for f in out["F"])
|
||||
|
||||
|
||||
def test_e05_prob_sheet(project, monkeypatch):
|
||||
"""E05 s12 — 측점 줄 = 저장본 값 · 변수 줄 = 토네이도 순 · 기본값 그대로 표기 · 없으면 시트 뺌."""
|
||||
from E05_StabReport import E05_StabReport_Build as build
|
||||
|
||||
recs = rp.records(project)
|
||||
assert len(recs) == 1
|
||||
rec = recs[0]
|
||||
book = build.form()
|
||||
build.build_program(book, build.gather(project))
|
||||
s12 = next(s for s in book["시트"] if s["id"] == "s12")
|
||||
cells = s12["칸"]
|
||||
st = rec["result"]["stats"]
|
||||
assert cells["E6"]["값"] == pytest.approx(rec["fs_deterministic"], abs=1e-6)
|
||||
assert cells["F6"]["값"] == pytest.approx(st["mean"], abs=1e-6)
|
||||
assert cells["H6"]["값"] == pytest.approx(100 * st["pf"], abs=1e-6)
|
||||
assert cells["J6"]["값"] == pytest.approx(st["beta_lognormal"], abs=1e-6)
|
||||
assert "사용자 입력" in cells["A3"]["값"] # 표본 400 ≠ 기본 1000
|
||||
d = rp.load_defaults()
|
||||
plain = {
|
||||
**rec["request"],
|
||||
**{k: d["run"][k] for k in ("n", "sampling", "seed", "mode", "method")},
|
||||
}
|
||||
assert build.prob._is_default({"request": plain}, d)
|
||||
plain["variables"] = [{**plain["variables"][0], "cov": 0.25}]
|
||||
assert not build.prob._is_default({"request": plain}, d)
|
||||
first = rec["result"]["sensitivity"]["tornado"][0]["name"]
|
||||
assert cells["B10"]["값"] == first and cells["K10"]["값"] == 1
|
||||
monkeypatch.setattr(build.prob, "read", lambda root: [])
|
||||
empty = build.form()
|
||||
build.build_program(empty, build.gather(project))
|
||||
assert "s12" not in {s["id"] for s in empty["시트"]}
|
||||
|
||||
|
||||
TYPESCRIPT = ROOT / "config" / "node_modules" / "typescript"
|
||||
LOGIC_TS = ROOT / "E03_StabSection" / "logic" / "E03_StabSection_Prob.ts"
|
||||
LOGIC_SCRIPT = r"""
|
||||
const ts = require(process.argv[1]);
|
||||
const fs = require("fs");
|
||||
const src = fs.readFileSync(process.argv[2], "utf8");
|
||||
const opts = { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 };
|
||||
const js = ts.transpileModule(src, { compilerOptions: opts }).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", js)(box.exports, box);
|
||||
const m = box.exports;
|
||||
const setup = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const cache = m.fillCache(setup, null);
|
||||
const req = m.probRequest(setup, cache);
|
||||
cache.vars[m.varKey("colluvium", "c")].value = 0.5;
|
||||
process.stdout.write(JSON.stringify({ req, changed: m.changedCount(setup, cache),
|
||||
ticks: m.axisTicks(0.83, 1.47, 5) }));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TYPESCRIPT.is_dir(), reason="config/node_modules 없음")
|
||||
def test_logic_ts_request_matches_server():
|
||||
"""기본값 후보(붕적토 c 0.4 덮어씀) → 요청 꼴 = 서버 ProbRequest 로 읽힘 · 바꾼 줄 수 · 눈금."""
|
||||
setup = {
|
||||
"side": "left",
|
||||
"condition": "wet",
|
||||
"materials": [
|
||||
{"id": "weathered_soil", "name": "풍화토", "c": 15, "phi": 30, "gamma_t": 18},
|
||||
{"id": "colluvium", "name": "붕적토", "c": 5, "phi": 27, "gamma_t": 17.5},
|
||||
],
|
||||
"groundwater": {"mode": "line", "ru": None},
|
||||
"defaults": rp.load_defaults(),
|
||||
}
|
||||
out = subprocess.run(
|
||||
["node", "-e", LOGIC_SCRIPT, str(TYPESCRIPT), str(LOGIC_TS)],
|
||||
input=json.dumps(setup),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
)
|
||||
got = json.loads(out.stdout)
|
||||
req = rp.ProbRequest(**got["req"])
|
||||
assert (req.side, req.condition, req.n, req.sampling, req.mode) == (
|
||||
"left",
|
||||
"wet",
|
||||
1000,
|
||||
"lhs",
|
||||
"fixed",
|
||||
)
|
||||
by = {(v.material, v.prop): v for v in req.variables}
|
||||
assert len(by) == 6 # 물 σ 0 = 뺌
|
||||
assert by[("colluvium", "c")].cov == 0.4 and by[("weathered_soil", "c")].cov == 0.3
|
||||
assert by[("weathered_soil", "c")].dist == "lognormal" and by[("colluvium", "phi")].cov == 0.1
|
||||
assert all(v.trunc_sigma == 3.0 for v in req.variables)
|
||||
assert got["changed"] == 1
|
||||
assert got["ticks"][0] <= 0.83 and got["ticks"][-1] >= 1.47 and 0 not in got["ticks"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-q"]))
|
||||
@@ -212,4 +212,64 @@ export const ui_locales_e03 = {
|
||||
E03_Sp_Mode_prestress: ["선인장(능동)", "Prestress (active)"],
|
||||
E03_Sp_Mode_not_crossing: ["활동면 안 만남", "Not crossing"],
|
||||
E03_Sp_Mode_pending: ["결과 없음", "No result"],
|
||||
/* --- 확률 · 민감도 탭(537-1) --- */
|
||||
E03_Tab_Prob: ["확률 · 민감도", "Probabilistic · Sensitivity"],
|
||||
E03_Pb_RepOnly: [
|
||||
"대표 단면만 확률 해석합니다 — 좌측 목록에서 별(대표 단면)을 켜고 [대표 단면 저장] 뒤 보세요.",
|
||||
"Probabilistic analysis runs on representative sections only — star it in the list and save first.",
|
||||
],
|
||||
E03_Pb_MeanNote: [
|
||||
"평균 = E02 지반 조건 값(바꾸려면 E02) · 분포 · 변동계수는 기본값 후보",
|
||||
"Mean = E02 ground values (change in E02) · distributions · COV are default candidates",
|
||||
],
|
||||
E03_Pb_Use: ["쓰기", "Use"],
|
||||
E03_Pb_Var: ["변수", "Variable"],
|
||||
E03_Pb_Mean: ["평균", "Mean"],
|
||||
E03_Pb_Dist: ["분포", "Distribution"],
|
||||
E03_Pb_Dist_normal: ["정규", "Normal"],
|
||||
E03_Pb_Dist_lognormal: ["대수정규", "Lognormal"],
|
||||
E03_Pb_Spread: ["변동", "Spread"],
|
||||
E03_Pb_Cov: ["변동계수", "COV"],
|
||||
E03_Pb_Trunc: ["절단 ±σ 배", "Truncate ±kσ"],
|
||||
E03_Pb_Prop_c: ["c kPa", "c kPa"],
|
||||
E03_Pb_Prop_phi: ["φ °", "φ °"],
|
||||
E03_Pb_Prop_gamma: ["γ kN/m³", "γ kN/m³"],
|
||||
E03_Pb_Prop_water: ["지하수위 이동 m", "Water table shift m"],
|
||||
E03_Pb_Prop_ru: ["간극수압비 r_u", "Pore pressure ratio r_u"],
|
||||
E03_Pb_Mode: ["임계면", "Slip surface"],
|
||||
E03_Pb_Mode_fixed: ["임계면 고정(빠름)", "Fixed critical (fast)"],
|
||||
E03_Pb_Mode_search: ["매회 탐색(정밀 · 느림)", "Search each sample (slow)"],
|
||||
E03_Pb_Method: ["방법", "Method"],
|
||||
E03_Pb_Method_mp: ["M-P(반정현)", "M-P (half-sine)"],
|
||||
E03_Pb_Sampling: ["표본 방법", "Sampling"],
|
||||
E03_Pb_N: ["표본 수", "Samples"],
|
||||
E03_Pb_Seed: ["시드", "Seed"],
|
||||
E03_Pb_Rho: ["c–φ 상관 ρ", "c–φ correlation ρ"],
|
||||
E03_Pb_Run: ["확률 해석", "Run Probabilistic"],
|
||||
E03_Pb_Defaults: ["기본값", "Defaults"],
|
||||
E03_Pb_Done: ["확률 해석 끝", "Probabilistic analysis done"],
|
||||
E03_Pb_Unconfirmed: [
|
||||
"기본값 후보 · 사용자 확정 전 — 근거",
|
||||
"Default candidates · not confirmed — sources",
|
||||
],
|
||||
E03_Pb_UserInput: ["사용자 입력 {n} 줄 · 근거", "User input {n} rows · sources"],
|
||||
E03_Pb_Result: ["확률 해석 결과", "Probabilistic Result"],
|
||||
E03_Pb_Det: ["결정론 Fs", "Deterministic FS"],
|
||||
E03_Pb_MeanF: ["Fs 평균", "Mean FS"],
|
||||
E03_Pb_Std: ["Fs 표준편차", "FS std. dev."],
|
||||
E03_Pb_Pf: ["파괴확률 Pf(F < 1)", "Probability of failure"],
|
||||
E03_Pb_BetaN: ["신뢰도 지수 β 정규", "Reliability index (normal)"],
|
||||
E03_Pb_BetaLn: ["신뢰도 지수 β 대수정규", "Reliability index (lognormal)"],
|
||||
E03_Pb_PReq: ["P(F < 기준안전율)", "P(F < required)"],
|
||||
E03_Pb_Valid: ["유효 표본", "Valid samples"],
|
||||
E03_Pb_Time: ["계산 시간", "Elapsed"],
|
||||
E03_Pb_Hist: ["안전율 히스토그램", "FS Histogram"],
|
||||
E03_Pb_Cdf: ["누적분포", "Cumulative Distribution"],
|
||||
E03_Pb_Sens: [
|
||||
"민감도(나머지 평균 · 임계면 고정)",
|
||||
"Sensitivity (others at mean · fixed surface)",
|
||||
],
|
||||
E03_Pb_Tornado: ["토네이도(범위 끝 Fs)", "Tornado (FS at range ends)"],
|
||||
E03_Pb_Count: ["표본 수", "Count"],
|
||||
E03_Pb_Range: ["범위 %", "Percent of range"],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user