Files
Aislo/common_util/common_util_surface_confirmation.py
T
eomsangdonandClaude Opus 5.5 80c2aabfcf fix(763-1): 추천노선 = 확정 지표면에서만 — 확정 전 409 · 비용면 = 굳힌 격자 마디 · 조용한 DTM 대체 걷음
- `confirmed_surface_params()` — stage 1 COMPLETE · params 에 source_filter 있을 때만 · 아니면 None
- compute_initial = 확정 전 409 「지표면 확정 전 — B04 [확정] 뒤 추천이 생김」 · 미리 셈 지형 = [](건너뜀 로그)
- Solver implicit 갈래 = `implicit_grid_npz` 마디 그대로(RBF 다시 안 셈 · 짓기 16.2 s → 0.02 s · 유효 칸 최대차 0.00003 m)
- 모델 파일 없음 · 깨짐 = 까닭 그대로 FileNotFoundError · ValueError(409) · 서명에 굳힌 격자(서명 전에 지음)
- 시험 224_3 · 351_1 · 509_5 의 확정 갈아 끼우기를 새 함수로 옮김(라우터가 새 함수를 부름)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:17:31 +09:00

248 lines
9.0 KiB
Python

"""WF1 지표면 확정 선택값의 기본값·DB 스냅샷 처리."""
import json
from pathlib import Path
from typing import Any, Literal
import aiomysql
from config.config_system import (
SHEET_SURFACE_DEFAULT_METHOD,
SURFACE_CONFIRM_DEFAULT_FILTER,
SURFACE_CONFIRM_DEFAULT_METHOD,
SURFACE_CONFIRM_DEFAULT_SMOOTH,
SURFACE_CONTOUR_INTERVAL_M,
)
#: B05 계획노선 편집 모달의 분석 지형 — las = 확정 지표면 그대로 · sheet = 수치지형도 서피스
AnalysisTerrain = Literal["las", "sheet"]
#: 수치지형도 분석 지형 — LAS 없는 프로젝트 자동 확정(WF1)과 같은 모델
SHEET_ANALYSIS_FILTER = f"sheet_{SHEET_SURFACE_DEFAULT_METHOD}"
#: 설계 지형을 수치지형도로 바꿀 때 적어 두는 라이다 확정값 — 라이다로 되돌릴 자리(PLAN 133-2)
LAS_STASH_KEY = "las_selection"
_SURFACE_KEYS = ("source_filter", "method", "smooth")
SURFACE_CONFIRM_PARAM_KEYS = (
"source_filter",
"method",
"smooth",
"contour_interval_m",
)
def surface_confirmation_defaults() -> dict[str, Any]:
"""현재 config에 설정된 지표면 자동 확정 기본값을 반환한다."""
return {
"source_filter": SURFACE_CONFIRM_DEFAULT_FILTER,
"method": SURFACE_CONFIRM_DEFAULT_METHOD,
"smooth": SURFACE_CONFIRM_DEFAULT_SMOOTH,
"contour_interval_m": SURFACE_CONTOUR_INTERVAL_M,
}
def _decode_params(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return dict(value)
if isinstance(value, str) and value:
decoded = json.loads(value)
return dict(decoded) if isinstance(decoded, dict) else {}
return {}
async def get_surface_confirmation_params(
connection: aiomysql.Connection,
project_id: str,
) -> dict[str, Any]:
"""stage 1 스냅샷을 우선하고, 없으면 config 기본값으로 보완한다."""
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""
SELECT params
FROM project_workflow_stages
WHERE project_id = %s AND stage_no = 1
""",
(project_id,),
)
row = await cursor.fetchone()
resolved = surface_confirmation_defaults()
params = _decode_params(row.get("params") if row else None)
for key in SURFACE_CONFIRM_PARAM_KEYS:
if key in params and params[key] is not None:
resolved[key] = params[key]
if isinstance(params.get(LAS_STASH_KEY), dict):
resolved[LAS_STASH_KEY] = params[LAS_STASH_KEY]
return resolved
#: 확정 전 추천 요청(763-1) — 기본값으로 슬쩍 셈하지 않음
NOT_CONFIRMED = "지표면 확정 전 — B04 [확정] 뒤 추천이 생김"
async def confirmed_surface_params(
connection: aiomysql.Connection,
project_id: str,
) -> dict[str, Any] | None:
"""확정된 지표면 params — stage 1 COMPLETE · params 에 source_filter 가 있을 때만 · 아니면 None
(추천 노선이 config 기본값으로 셈하지 않게 · 763-1)."""
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"SELECT state, params FROM project_workflow_stages "
"WHERE project_id = %s AND stage_no = 1",
(project_id,),
)
row = await cursor.fetchone()
if not row or row.get("state") != "COMPLETE":
return None
if not _decode_params(row.get("params")).get("source_filter"):
return None
return await get_surface_confirmation_params(connection, project_id)
def _is_sheet(params: dict[str, Any]) -> bool:
return str(params.get("source_filter") or "").startswith("sheet_")
def design_terrain(params: dict[str, Any]) -> AnalysisTerrain:
"""지금 설계 지형 — 확정 params 가 수치지형도면 sheet."""
return "sheet" if _is_sheet(params) else "las"
def analysis_terrain_params(
params: dict[str, Any], terrain: AnalysisTerrain | None
) -> dict[str, Any]:
"""고른 지형의 지표면 params — None · 지금 설계 지형과 같으면 확정 params 그대로.
설계 지형이 수치지형도여도 적어 둔 라이다 확정값(`LAS_STASH_KEY`)이 있으면 las 로 돌아감.
라이다 없는 프로젝트는 무엇을 받아도 확정 params 그대로.
"""
if terrain is None or terrain == design_terrain(params):
return params
if terrain == "sheet":
return {**params, "source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth": True}
stash = params.get(LAS_STASH_KEY)
if not isinstance(stash, dict) or not stash.get("source_filter"):
return params
return {**params, **{key: stash[key] for key in _SURFACE_KEYS if key in stash}}
def terrain_changes(params: dict[str, Any], target: dict[str, Any]) -> bool:
"""지표면(필터 · 표현 · 평활)이 바뀌는가."""
return any(params.get(key) != target.get(key) for key in _SURFACE_KEYS)
def analysis_terrain_sources(params: dict[str, Any], models_dir: Path) -> list[str]:
"""고를 수 있는 분석 지형 — 라이다 확정값(지금 것 · 적어 둔 것)과 수치지형도가 둘 다 있으면 둘."""
if _is_sheet(params):
return ["las", "sheet"] if isinstance(params.get(LAS_STASH_KEY), dict) else ["sheet"]
if (Path(models_dir) / f"dtm_{SHEET_ANALYSIS_FILTER}.npz").is_file():
return ["las", "sheet"]
return ["las"]
async def update_contour_interval_param(
connection: aiomysql.Connection,
project_id: str,
contour_interval_m: float,
) -> None:
"""stage 1 params의 등고선 간격만 갱신한다 (B05 재적용 영속화)."""
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""
SELECT params
FROM project_workflow_stages
WHERE project_id = %s AND stage_no = 1
FOR UPDATE
""",
(project_id,),
)
row = await cursor.fetchone()
if row is None:
raise LookupError("WF1 단계 상태를 찾을 수 없습니다.")
params = _decode_params(row.get("params") if row else None)
params["contour_interval_m"] = float(contour_interval_m)
await cursor.execute(
"""
UPDATE project_workflow_stages
SET params = %s
WHERE project_id = %s AND stage_no = 1
""",
(json.dumps(params, ensure_ascii=False), project_id),
)
async def merge_surface_confirmation_params(
connection: aiomysql.Connection,
project_id: str,
selection: dict[str, Any],
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""기존 stage 1 params에 확정 선택값 전체를 병합 저장한다. `extra` 는 함께 적을 칸."""
normalized = {
"source_filter": str(selection["source_filter"]),
"method": str(selection["method"]),
"smooth": bool(selection["smooth"]),
"contour_interval_m": float(selection["contour_interval_m"]),
}
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""
SELECT params
FROM project_workflow_stages
WHERE project_id = %s AND stage_no = 1
FOR UPDATE
""",
(project_id,),
)
row = await cursor.fetchone()
if row is None:
raise LookupError("WF1 단계 상태를 찾을 수 없습니다.")
params = _decode_params(row.get("params") if row else None)
params.update(normalized)
params.update(extra or {})
await cursor.execute(
"""
UPDATE project_workflow_stages
SET params = %s
WHERE project_id = %s AND stage_no = 1
""",
(json.dumps(params, ensure_ascii=False), project_id),
)
return normalized
async def stash_las_selection(
connection: aiomysql.Connection,
project_id: str,
selection: dict[str, Any],
) -> None:
"""stage 1 params 에 라이다 확정값(`LAS_STASH_KEY`)만 적음 — 설계 지형은 그대로(B05 LAS 나중
올리기 · PLAN 637-2 · 지형 칩 라이다 열림). 트랜잭션은 부르는 쪽."""
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""
SELECT params
FROM project_workflow_stages
WHERE project_id = %s AND stage_no = 1
FOR UPDATE
""",
(project_id,),
)
row = await cursor.fetchone()
if row is None:
raise LookupError("WF1 단계 상태를 찾을 수 없습니다.")
params = _decode_params(row.get("params"))
params[LAS_STASH_KEY] = {
"source_filter": str(selection["source_filter"]),
"method": str(selection["method"]),
"smooth": bool(selection["smooth"]),
}
await cursor.execute(
"""
UPDATE project_workflow_stages
SET params = %s
WHERE project_id = %s AND stage_no = 1
""",
(json.dumps(params, ensure_ascii=False), project_id),
)