207 lines
7.6 KiB
Python
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)
|