- 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>
362 lines
14 KiB
Python
362 lines
14 KiB
Python
"""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,
|
||
)
|