Files
Aislo/E02_StabGround/E02_StabGround_Model.py
T
eomsangdonandClaude Opus 5.5 f4077fbcf6 feat(532-3): E02 지하수 갈래 — 우기 규칙 넷(KDS 기본 · 지표 아래 깊이 비율 · r_u · 사용자 수위선 구간) · 엔진 꼴 바꿈 · 지하수 카드
- ground.json groundwater 칸 덧붙임(wet_mode · depth_ratio · ru · lines · 기존 칸 그대로) · 기본값 = KDS · 범위 칸 넷
- check_ground ↔ checkGround 짝 고침(갈래 · 비율 · r_u · 수위선 구간 · 점) · 거울 시험 판 넷 더함
- groundwater_for · station_groundwater — 측점 모형 → {mode: none | ru | line}(좌 · 우 비탈 높이 따로 · 덮지 않은 측점 = KDS)
- GET · [초기화] = 옛 정본 빠진 칸 기본값으로 채움 · 화면 옛 캐시도 서버 값으로 채움
- 화면 = 지하수 카드 따로(「KDS 기본 · 우기 기준안전율 1.2 와 짝」 안내) · 하중 카드 = 노면 하중 · 지진
- 시험 test_532_4_stab_walls_water(벽 · 지하수) · test_532_3 기본값 지하수 칸 대조 고침(칸 늘어남)

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

362 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""E02 지반 조건 정본 꼴 · 검사 · 측점 적용(요구 347-2 · 3 · 4 · PLAN 532-3).
정본 = `E02_StabGround/ground.json` · 초기값 = `initial_snapshot/E02_StabGround/ground.json`
(E 생성 직후 = 마스터 기본값 + B06 측점 암 경계) · 기본값 = `resources/master_template/stability/
ground_defaults.json`(M02 마스터 층). 물성 한 줄 = `E03_StabSection_Types.Material` 꼴 + `water`.
⚠ 파이썬·TS 짝 — `check_ground` ↔ `logic/E02_StabGround_Check.ts` `checkGround` ·
`resolve_stations` ↔ `resolveStations`. 한쪽을 고치면 다른 쪽도 · 거울 시험
`resources/tester/test_532_3_e02_ground.py`.
지하수 → 엔진 꼴(`station_groundwater` · 서버 단독 · 짝 없음) —
시험 `test_532_4_stab_walls_water.py`.
"""
from __future__ import annotations
import copy
import json
import math
from pathlib import Path
from typing import Any
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[1]
DEFAULTS_PATH = (
PROJECT_ROOT / "resources" / "master_template" / "stability" / "ground_defaults.json"
)
STAGE_DIR = "E02_StabGround"
GROUND_NAME = "ground.json"
MATERIAL_FIELDS = ("gamma_t", "gamma_sat", "c", "phi")
WATER_GROUPS = ("soil", "rock")
LABELS = ("soil", "ripping_rock", "blasting_rock", "fill")
ROCK_LABELS = ("ripping_rock", "blasting_rock")
CASES = ("dry", "wet", "seismic", "short_term")
SLOPES = ("cut", "fill", "soft")
WATER_KEYS = ("dry", "wet_soil", "wet_rock")
WATER_RULES = ("none", "surface", "half_height")
# 우기 지하수 갈래 — kds = 위 규칙(wet_soil · wet_rock · 기본) ·
# depth_ratio = 지표 − 비탈 높이 × 비율 · ru = 간극수압비 ·
# line = 사용자 수위선(구간 · [offset, 지표 아래 깊이]) · 덮지 않은 측점은 kds
WET_MODES = ("kds", "depth_ratio", "ru", "line")
WATER_EXTRA_KEYS = ("wet_mode", "depth_ratio", "ru", "lines")
# 정본에 남기는 칸 — 그 밖(limits · sources · note)은 기본값 파일에만
GROUND_KEYS = (
"version",
"materials",
"label_map",
"stations",
"zones",
"safety_factors",
"road_surcharge_kpa",
"groundwater",
"seismic",
)
def load_defaults() -> dict[str, Any]:
return json.loads(DEFAULTS_PATH.read_text(encoding="utf-8"))
def ground_path(project_root: Path) -> Path:
return Path(project_root) / STAGE_DIR / GROUND_NAME
def initial_path(project_root: Path) -> Path:
return Path(project_root) / "initial_snapshot" / STAGE_DIR / GROUND_NAME
def pick(doc: dict[str, Any]) -> dict[str, Any]:
"""정본 칸만 떠 옴(깊은 복사)."""
return copy.deepcopy({key: doc[key] for key in GROUND_KEYS if key in doc})
def upgrade(ground: dict[str, Any], defaults: dict[str, Any]) -> dict[str, Any]:
"""옛 정본(지하수 갈래 칸 전) = 빠진 지하수 칸만 기본값으로 채운 사본 · 있는 칸은 그대로."""
out = copy.deepcopy(ground)
water = out.setdefault("groundwater", {})
if isinstance(water, dict):
for key in WATER_EXTRA_KEYS:
water.setdefault(key, copy.deepcopy(defaults["groundwater"][key]))
return out
def build_initial(defaults: dict[str, Any], stations: list[dict[str, Any]]) -> dict[str, Any]:
"""초기값 = 기본값 정본 칸 + B06 측점(암 경계)."""
ground = pick(defaults)
ground["stations"] = stations
return ground
def _num(value: Any) -> bool:
return isinstance(value, int | float) and not isinstance(value, bool) and math.isfinite(value)
def _in(value: Any, bound: list[float]) -> bool:
return _num(value) and bound[0] <= value <= bound[1]
def _list(value: Any) -> list:
return value if isinstance(value, list) else []
def _dict(value: Any) -> dict:
return value if isinstance(value, dict) else {}
def check_ground(ground: dict[str, Any], limits: dict[str, list[float]]) -> list[dict[str, str]]:
"""틀린 칸 목록 [{path, code}] — 빈 목록 = 통과. code = number · range · order · sat_lt_t ·
name · name_dup · id_dup · enum · ref · empty · few_points · type."""
errors: list[dict[str, str]] = []
def bad(path: str, code: str) -> None:
errors.append({"path": path, "code": code})
def ranged(path: str, value: Any, key: str) -> None:
if not _num(value):
bad(path, "number")
elif not _in(value, limits[key]):
bad(path, "range")
materials = _list(ground.get("materials"))
if not materials:
bad("materials", "empty")
ids: list[str] = []
names: list[str] = []
for i, item in enumerate(materials):
base = f"materials.{i}"
if not isinstance(item, dict):
bad(base, "type")
continue
mid = item.get("id")
if not isinstance(mid, str) or not mid or mid in ids:
bad(f"{base}.id", "id_dup")
ids.append(mid if isinstance(mid, str) else "")
name = item["name"].strip() if isinstance(item.get("name"), str) else ""
low, high = limits["name_length"]
if not low <= len(name) <= high:
bad(f"{base}.name", "name")
elif name in names:
bad(f"{base}.name", "name_dup")
names.append(name)
for field in MATERIAL_FIELDS:
ranged(f"{base}.{field}", item.get(field), field)
if _num(item.get("gamma_t")) and _num(item.get("gamma_sat")):
if item["gamma_sat"] < item["gamma_t"]:
bad(f"{base}.gamma_sat", "sat_lt_t")
if item.get("water") not in WATER_GROUPS:
bad(f"{base}.water", "enum")
label_map = _dict(ground.get("label_map"))
for label in LABELS:
if not label_map.get(label) or label_map.get(label) not in ids:
bad(f"label_map.{label}", "ref")
for i, zone in enumerate(_list(ground.get("zones"))):
base = f"zones.{i}"
if not isinstance(zone, dict):
bad(base, "type")
continue
ranged(f"{base}.from_m", zone.get("from_m"), "chainage_m")
ranged(f"{base}.to_m", zone.get("to_m"), "chainage_m")
if _num(zone.get("from_m")) and _num(zone.get("to_m")):
if zone["from_m"] >= zone["to_m"]:
bad(f"{base}.to_m", "order")
for layer in ("soil", "rock"):
if zone.get(layer) is not None and zone.get(layer) not in ids:
bad(f"{base}.{layer}", "ref")
if zone.get("soil_thickness_m") is not None:
ranged(f"{base}.soil_thickness_m", zone.get("soil_thickness_m"), "soil_thickness_m")
factors = _dict(ground.get("safety_factors"))
for case in CASES:
for slope in SLOPES:
value = _dict(factors.get(case)).get(slope)
ranged(f"safety_factors.{case}.{slope}", value, "safety_factor")
ranged("road_surcharge_kpa", ground.get("road_surcharge_kpa"), "road_surcharge_kpa")
water = _dict(ground.get("groundwater"))
for key in WATER_KEYS:
if water.get(key) not in WATER_RULES:
bad(f"groundwater.{key}", "enum")
if water.get("wet_mode") not in WET_MODES:
bad("groundwater.wet_mode", "enum")
ranged("groundwater.depth_ratio", water.get("depth_ratio"), "depth_ratio")
ranged("groundwater.ru", water.get("ru"), "ru")
if not isinstance(water.get("lines"), list):
bad("groundwater.lines", "type")
for i, line in enumerate(_list(water.get("lines"))):
base = f"groundwater.lines.{i}"
if not isinstance(line, dict):
bad(base, "type")
continue
ranged(f"{base}.from_m", line.get("from_m"), "chainage_m")
ranged(f"{base}.to_m", line.get("to_m"), "chainage_m")
if _num(line.get("from_m")) and _num(line.get("to_m")):
if line["from_m"] >= line["to_m"]:
bad(f"{base}.to_m", "order")
points = _list(line.get("points"))
if len(points) < 2:
bad(f"{base}.points", "few_points")
last = None
for k, point in enumerate(points):
if not isinstance(point, list) or len(point) != 2:
bad(f"{base}.points.{k}", "type")
continue
ranged(f"{base}.points.{k}.0", point[0], "offset_m")
ranged(f"{base}.points.{k}.1", point[1], "water_depth_m")
if _num(point[0]):
if last is not None and point[0] <= last:
bad(f"{base}.points.{k}.0", "order")
last = point[0]
seismic = _dict(ground.get("seismic"))
if not isinstance(seismic.get("enabled"), bool):
bad("seismic.enabled", "enum")
ranged("seismic.kh", seismic.get("kh"), "kh")
return errors
def clean(ground: dict[str, Any]) -> dict[str, Any]:
"""검사를 지난 정본에서 알려진 칸만 남김 · 이름 앞뒤 빈칸 걷음."""
out = pick(ground)
out["materials"] = [
{
"id": m["id"],
"name": m["name"].strip(),
**{field: m[field] for field in MATERIAL_FIELDS},
"water": m["water"],
}
for m in out["materials"]
]
out["label_map"] = {label: out["label_map"][label] for label in LABELS}
out["zones"] = [
{key: z.get(key) for key in ("from_m", "to_m", "soil", "rock", "soil_thickness_m")}
for z in _list(out.get("zones"))
]
factors = out["safety_factors"]
out["safety_factors"] = {c: {s: factors[c][s] for s in SLOPES} for c in CASES}
water = out["groundwater"]
out["groundwater"] = {key: water[key] for key in (*WATER_KEYS, *WATER_EXTRA_KEYS)}
out["groundwater"]["lines"] = [
{"from_m": w["from_m"], "to_m": w["to_m"], "points": [list(p) for p in w["points"]]}
for w in water["lines"]
]
out["seismic"] = {"enabled": out["seismic"]["enabled"], "kh": out["seismic"]["kh"]}
return out
def resolve_stations(ground: dict[str, Any]) -> list[dict[str, Any]]:
"""측점마다 적용 지층 — 위 = 토사 연결 · 아래 = 암 측점의 암 연결 · 두께 = B06 · 구간이
덮어씀(뒤 구간이 이김 · 양 끝 포함 · 빈 칸은 안 바꿈) · 두께가 없으면 아래 지층 없음.
성토체는 측점과 상관없이 fill 연결."""
label_map = ground.get("label_map") or {}
out: list[dict[str, Any]] = []
for station in ground.get("stations") or []:
chainage = station["chainage_m"]
ground_type = station.get("ground_type")
upper = label_map.get("soil")
lower = label_map.get(ground_type) if ground_type in ROCK_LABELS else None
thickness = station.get("soil_thickness_m")
zoned = False
for zone in ground.get("zones") or []:
if not zone["from_m"] <= chainage <= zone["to_m"]:
continue
zoned = True
if zone.get("soil") is not None:
upper = zone["soil"]
if zone.get("rock") is not None:
lower = zone["rock"]
if zone.get("soil_thickness_m") is not None:
thickness = zone["soil_thickness_m"]
if thickness is None: # 경계 없음 = 전부 위 지층
lower = None
out.append(
{
"chainage_m": chainage,
"ground_type": ground_type,
"upper": upper,
"lower": lower,
"soil_thickness_m": thickness if lower is not None else None,
"zoned": zoned,
}
)
return out
def _face_water(ground: dict[str, Any], chainage_m: float) -> str:
"""측점 비탈 겉 지층의 지하수 무리 — 토층 두께 0 = 아래(암) 지층 · 아니면 위 지층 ·
측점 없음 = soil."""
names = {m["id"]: m for m in ground.get("materials") or []}
for row in resolve_stations(ground):
if abs(row["chainage_m"] - chainage_m) < 1e-3:
face = row["lower"] if row["lower"] and row["soil_thickness_m"] == 0 else row["upper"]
return (names.get(face) or {}).get("water", "soil")
return "soil"
def groundwater_for(
ground: dict[str, Any],
condition: str,
*,
chainage_m: float,
surface: list,
heights: dict[str, float],
) -> dict[str, Any]:
"""E02 지하수 규칙 → 엔진 꼴 `{mode: none | ru | line}`(E03 Types `Groundwater`) — 측점마다.
surface = 공사 뒤 지표 [[x, z]](좌 = +x) · heights = 비탈 높이 {left, right} m(x ≥ 0 = 좌 높이).
건기 · 지진 · 단기 = `dry` 규칙 · 우기 = `wet_mode` 갈래
(kds = 겉 지층 무리로 wet_soil | wet_rock).
"""
water = ground.get("groundwater") or {}
sx, sz = zip(*sorted((float(x), float(z)) for x, z in surface))
def below(depth_at, extra=()) -> dict[str, Any]:
# 좌 · 우 높이가 달라 x = 0 에서 끊길 수 있음 — 0 양쪽 점을 넣어 계단으로
xs = sorted({*sx, *extra, -1e-6, 1e-6})
zs = np.interp(xs, sx, sz) - np.array([depth_at(x) for x in xs])
return {"mode": "line", "line": [[float(x), float(z)] for x, z in zip(xs, zs)]}
def height(x: float) -> float:
return float(heights.get("left" if x >= 0 else "right") or 0.0)
def rule(name: str) -> dict[str, Any]:
if name == "surface":
return below(lambda x: 0.0)
if name == "half_height":
return below(lambda x: 0.5 * height(x))
return {"mode": "none"}
if condition != "wet":
return rule(water.get("dry", "none"))
mode = water.get("wet_mode", "kds")
if mode == "ru":
return {"mode": "ru", "ru": float(water["ru"])}
if mode == "depth_ratio":
ratio = float(water["depth_ratio"])
return below(lambda x: ratio * height(x))
if mode == "line":
hit = None
for item in water.get("lines") or []: # 뒤 구간이 이김(지층 구간과 같은 규칙)
if item["from_m"] <= chainage_m <= item["to_m"]:
hit = item
if hit is not None:
ox, depth = zip(*sorted((float(o), float(d)) for o, d in hit["points"]))
return below(lambda x: float(np.interp(x, ox, depth)), ox)
group = _face_water(ground, chainage_m)
return rule(water.get("wet_rock" if group == "rock" else "wet_soil", "none"))
def station_groundwater(ground: dict[str, Any], condition: str, model: dict[str, Any]) -> dict:
"""E03 측점 모형(`section_model_from_b06` 결과) → 그 측점 지하수 — 부르는 쪽이
`model["groundwater"] = station_groundwater(ground, cond, model)` 로 갈아 끼움."""
heights = {side["side"]: side["height_m"] for side in model.get("sides") or []}
return groundwater_for(
ground,
condition,
chainage_m=float((model.get("station") or {}).get("chainage_m") or 0.0),
surface=model["surface"],
heights=heights,
)