Files
Aislo/E06_StabRock/E06_StabRock_Router.py
T
eomsangdonandClaude Opus 5.5 f2e8bd6d5f feat(536-1): E06 암반 평사투영 — Markland 판정 · Hoek-Bray 평면 · 쐐기 안전율 · 스테레오넷 · 절리 입력 · 화면
- 엔진(자체 · NumPy · SciPy): 평면 · 쐐기(교선) · 전도 Markland(측방 한계 20° 기본) · 평면 Hoek-Bray(인장균열 없음 · 임계 · 깊이 · 수압 · kh · 수동 볼트) · 쐐기 SWedge 꼴(미끄럼 방향 8 경우 · 두 면 · 한 면 · 수압 중력식 · kh · 볼트)
- 스테레오넷 좌표(등각 · 등면적 하반구 · 극점 · 대원 · 교선 · 마찰원 · 일광 엔벨로프 · 위험 영역 = 판정 부등식)
- 길: 절리 작업본 E06_StabRock/joints.json(초기값 · 저장 · 초기화) · 해석 = 암 측점 깎기 쪽만 · 파생 저장본 stab_rock · 결과 · 측점 스테레오넷
- 화면: 좌 절리군 표 · 설정 / 가운데 위험 띠 · 스테레오넷 / 우 측점 결과 · 안전율 표 · E 단계 막대에 E06(끝에 덧붙임)
- 시험 test_536_1(Hoek Sau Mau Ping 0.8184 · 1.0655 · Sharma 윗면 무게 · Hoek-Bray 쐐기 계수 · Kumsar · Priest 두 예제 · Markland 경계 · 투영 왕복 · 위험 영역 = 판정) · test_528 E 단계 수 4 → 5

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

205 lines
7.5 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,
"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"],
"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)