- 기본값 정본 resources/master_template/stability/ground_defaults.json — 지층 여섯 · 값마다 출처(07 표 · 절) · KDS 11 70 05 표 4.3-1~3 · 노면 10 kPa · 지하수 규칙 · 지진 끔 · 범위
- 07 원문 대조 — 성토 φ 25 → 28(07 §4 종합 28~30 아래값) · 나머지 범위 안
- API GET · PUT /api/projects/{id}/stab/ground · POST …/reset — 작업본 E02_StabGround/ground.json · 초기값 initial_snapshot/E02_StabGround/ground.json(E 생성 직후 기본값 + B06 측점 암 경계) · 측점은 서버 값
- 검사 · 측점 적용 = 파이썬 · TS 짝(E02_StabGround_Model.py ↔ logic/E02_StabGround_Check.ts) · 거울 시험
- 화면 — 카드 넷 · 칸 위 근거 · 기본값 툴팁 · 틀린 칸 표시 · sessionStorage 캐시 · 자동저장 없음
- test_528 E01 시험 — 생성 뒤 E02 초기값 부름 확인 더함
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
8.6 KiB
Python
231 lines
8.6 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`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
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")
|
|
# 정본에 남기는 칸 — 그 밖(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 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 · 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")
|
|
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}
|
|
out["groundwater"] = {key: out["groundwater"][key] for key in WATER_KEYS}
|
|
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
|