Files
Aislo/E06_StabRock/E06_StabRock_Router.py
T

207 lines
7.6 KiB
Python

"""E06 암반 평사투영 해석 API (요구 345 · 351 · 요구서 4-2 2차 · PLAN 536-1).
- `GET /api/projects/{id}/stab/rock` — 절리 작업본(없으면 초기값) · 범위 · 암 측점 · 고칠 수 있는지
- `PUT …/stab/rock` — [저장] 검사 뒤 작업본 `E06_StabRock/joints.json`
- `POST …/stab/rock/reset` — [초기화] 초기값을 작업본에 덮어씀(재계산 아님)
- `POST …/stab/rock/analyze` — [해석] 암 측점 × 깎기 쪽 · 지문이 같으면 안 셈(`fresh`)
- `GET …/stab/rock/result` — 측점별 모드 · 안전율 · 판정(+ 지문이 지금과 다른지)
- `GET …/stab/rock/result?station=&side=` — 그 측점 쪽 + 스테레오넷(등각 · 등면적)
입력 = E01 복사본 + E02 지반 조건(물성 · 지진 · 기준안전율) + 절리 작업본(저장 안 한 캐시는 안 씀).
계산 자리 = ③ 서버 단독. 결과 = 파생 저장본 `_derived/stab_rock/<지문>`.
공유 회사(보기만)는 `require_project_access` 가 PUT · POST 를 막음.
"""
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from common_util.common_util_auth import verify_session
from common_util.common_util_derived_store import (
derived_dir,
derived_key,
read_derived,
write_derived,
)
from common_util.common_util_json import atomic_write_json
from E01_StabRegister import E01_StabRegister_Source as src
from E02_StabGround import E02_StabGround_Router as e02
from E06_StabRock import E06_StabRock_Model as model
router = APIRouter(prefix="/api/projects", tags=["E06 Stab Rock"])
KIND = "stab_rock"
class JointsRequest(BaseModel):
joints: dict[str, Any]
def ensure_initial(project_root: Path) -> dict[str, Any]:
"""초기값이 없을 때만 한 번 — 절리 없음 + 기본 설정."""
path = model.initial_path(project_root)
if path.is_file():
return json.loads(path.read_text(encoding="utf-8"))
doc = model.build_initial()
atomic_write_json(path, doc)
return doc
def _working(project_root: Path) -> dict[str, Any] | None:
path = model.joints_path(project_root)
return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None
def _joints(project_root: Path) -> dict[str, Any]:
return model.upgrade(_working(project_root) or ensure_initial(project_root))
def _source(project_root: Path) -> Path:
root = src.source_dir(project_root)
if not (root / src.MANIFEST_NAME).is_file():
raise HTTPException(status_code=409, detail="원본 복사본이 없습니다.")
return root
def _fingerprint(project_root: Path, source: Path) -> tuple[str, dict, dict]:
joints = _joints(project_root)
ground = e02._read_working(project_root) or e02.ensure_initial(project_root)
fp = derived_key(
model.engine_hash(),
src.manifest(source)["source_fingerprint"],
{
"sets": joints["sets"],
"settings": {k: v for k, v in joints["settings"].items() if k != "projection"},
},
model._ground_inputs(ground),
)
return fp, joints, ground
def _latest(project_root: Path) -> dict[str, Any] | None:
"""가장 최근 저장본(지문이 달라도 — 화면이 「다시 해석」 띠를 띄움)."""
folder = derived_dir(project_root, KIND)
files = sorted(folder.glob("*.json"), key=lambda p: p.stat().st_mtime_ns, reverse=True)
for path in files:
saved = read_derived(project_root, KIND, path.stem)
if saved:
return saved
return None
@router.get("/{project_id}/stab/rock")
async def get_rock(
project_id: UUID, session: dict[str, Any] = Depends(verify_session)
) -> dict[str, Any]:
root, project = await e02._project(project_id)
initial = ensure_initial(root)
working = _working(root)
try:
stations = [
{"chainage_m": s["chainage_m"], "label": s["label"], "ground_type": s["ground_type"]}
for s in model.rock_stations(_source(root))
]
except HTTPException:
stations = []
return {
"joints": model.upgrade(working or initial),
"saved": working is not None,
"defaults": {
"settings": model.DEFAULT_SETTINGS,
"new_set": model.NEW_SET,
"limits": model.LIMITS,
},
"stations": stations,
"editable": e02._editable(session, project),
}
@router.put("/{project_id}/stab/rock")
async def put_rock(project_id: UUID, payload: JointsRequest) -> dict[str, Any]:
root, _ = await e02._project(project_id)
errors = model.check_joints(payload.joints)
if errors:
raise HTTPException(status_code=422, detail={"errors": errors})
doc = model.clean(payload.joints)
atomic_write_json(model.joints_path(root), doc)
return {"joints": doc}
@router.post("/{project_id}/stab/rock/reset")
async def reset_rock(project_id: UUID) -> dict[str, Any]:
root, _ = await e02._project(project_id)
initial = ensure_initial(root)
atomic_write_json(model.joints_path(root), initial)
return {"joints": model.upgrade(initial)}
def _summary(stations: list[dict[str, Any]]) -> dict[str, Any]:
verdicts: dict[str, int] = {}
for item in stations:
verdicts[item["verdict"]] = verdicts.get(item["verdict"], 0) + 1
return {
"slopes": len(stations),
"verdicts": verdicts,
"small_blocks": sum(item["small_blocks"] for item in stations),
"modes": {
mode: sum(item["counts"][mode] for item in stations)
for mode in ("planar", "wedge", "toppling")
},
}
def _run(root: Path) -> dict[str, Any]:
source = _source(root)
fp, joints, ground = _fingerprint(root, source)
if read_derived(root, KIND, fp):
return {"status": "fresh", "fingerprint": fp}
stations = model.analyze(source, joints, ground)
value = {
"fingerprint": fp,
"settings": joints["settings"],
"sets": joints["sets"],
"summary": _summary(stations),
"stations": stations,
}
write_derived(root, KIND, fp, value)
return {"status": "done", "fingerprint": fp, "summary": value["summary"]}
@router.post("/{project_id}/stab/rock/analyze")
async def analyze_rock(project_id: UUID) -> dict[str, Any]:
"""[해석] — 암 측점 수십 개 · 1 초 안쪽이라 바로 셈(진행률 없음)."""
root, _ = await e02._project(project_id)
return await asyncio.to_thread(_run, root)
def _result(root: Path, station: str | None, side: str | None) -> dict[str, Any]:
source = _source(root)
fp, _, _ = _fingerprint(root, source)
saved = read_derived(root, KIND, fp) or _latest(root)
if not saved:
return {"status": "empty", "stale": False, "result": None}
value = saved["value"]
stale = value["fingerprint"] != fp
if station is None:
return {"status": "success", "stale": stale, "result": value}
for entry in value["stations"]:
if entry["key"] == station and (side is None or entry["slope"]["side"] == side):
net = model.station_net(entry, value["settings"])
return {"status": "success", "stale": stale, "entry": entry, "stereonet": net}
raise HTTPException(status_code=404, detail="그 측점의 암반 해석 결과가 없습니다.")
@router.get("/{project_id}/stab/rock/result")
async def result_rock(
project_id: UUID, station: str | None = None, side: str | None = None
) -> dict[str, Any]:
root, _ = await e02._project(project_id)
return await asyncio.to_thread(_result, root, station, side)