Files
Aislo/B05_Profile/B05_Profile_Router_RouteInitial_Initial.py
T
eomsangdonandClaude Opus 5.5 aa87dcfd68 feat(476-1 ③): 추천 후보 초기 종단 미리 셈 — 응답 칸 initial_profile · 저장본
- 새 `B05_Profile_Engine_InitialProfile` — 후보 노드 → 화면 펴기 짝(`draft_nodes` ↔ TS flattenServerPlan) → 노선 짜기(csv 4자리) → 관 · 구조물 · 추가 측점 따라감(새 세류선 교차 관 포함) → build_sections(438 1차 계획선) → 켬 = 1 m 균형 종단 얹기(seat_base_pvi)
- 새 `B05_Profile_Router_RouteInitial_Initial.initial_profile_of` — [적용] 체인과 같은 입력 모으기 · 지문(후보 · 지형 · 페이지 설정 · 시설 파일 · 측점 옵션 · 균형 인자 · 코드) · 저장본 `route_recommend/initial/<키>.json` · 실패 = {error}
- compute_initial 응답에 `initial_profile`(stations_m · ground_m · plan_m · line · pvi_count · curve_count · straightened · max_grade_pct · curve_length_m · warnings · with_profile · seconds)
- 시험 test_476_1_recommend_initial — 미리 셈 ↔ 체인 파일 길 꼭짓점 차 0 mm(끔 · 노선 바뀜 + 옹벽 + 추가 측점 · 켬) · 지문 · 응답 칸
- 위키 calculations N2 · rules 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:49:59 +09:00

186 lines
8.1 KiB
Python

"""추천 후보 초기 종단 모으기(476) — `POST /route/initial` 결과에 `initial_profile` 을 붙임.
입력 모으기(DB · 파일) · 지문 · 저장본 = 여기 · 셈 조각 = `B05_Profile_Engine_InitialProfile`(머리에
흐름 · 응답 칸). `B05_Profile_Router_RouteInitial`(700줄)에서 나눔 — `compute_initial` 이 부름.
"""
from __future__ import annotations
import asyncio
import functools
import logging
import time
from pathlib import Path
from typing import Any
from uuid import UUID
from fastapi.responses import JSONResponse
from config.config_db import get_db_pool
logger = logging.getLogger(__name__)
async def initial_profile_of(
project_id: UUID,
project_root: Path,
stored_path: str,
result: dict[str, Any],
request: Any,
selection: dict[str, Any],
key: str,
) -> dict[str, Any]:
"""추천 후보 초기 종단(476 · `B05_Profile_Engine_InitialProfile` 머리) — [적용] 체인과 같은
입력 · 같은 조각 · 지문 같으면 저장본 꺼냄. 실패 = `{error}`."""
from dataclasses import asdict
import aiomysql
from B03_FileInput.B03_FileInput_Service_Chain import redesign_solve_request
from B04_PreProcess.B04_PreProcess_Router_Watershed import STREAM_FILE, _prepare, _sheet_dir
from B05_Profile import B05_Profile_Engine_InitialProfile as ip
from B05_Profile import B05_Profile_Router_Replan as replan
from B05_Profile.B05_Profile_Engine_Sections import _MODELS_SUBDIR
from B05_Profile.B05_Profile_Plus_Stations import plus_stations_path
from B05_Profile.B05_Profile_Repository import get_latest_route
from B05_Profile.B05_Profile_Route_Save import _xy, section_seed
from B05_Profile.B05_Profile_Router_RouteInitial import balanced_profile_of, project_standard
from B05_Profile.B05_Profile_Structures_Repository import structures_file_path
from B06_Section.B06_Section_Repository import (
get_latest_section_options,
get_route_generation_source,
)
from B06_Section.B06_Section_Router_Regenerate_Options import (
_regeneration_grade_options,
_regeneration_options,
)
from common_util.common_util_drainage_pipes import pipe_points_path, read_pipe_points_file
from common_util.common_util_surface_sampler import build_surface_sampler
from common_util.common_util_workflow_state import get_workflow_state
started = time.perf_counter()
try:
radius_m, _, _ = await replan._plan_criteria(project_id) # [적용] 노선 짜기와 같은 R
line, curves = ip.candidate_line(result["nodes"], result.get("curves") or [], radius_m)
pool = get_db_pool()
async with pool.acquire() as connection:
latest = await get_latest_route(connection, project_id)
source = (
await get_route_generation_source(connection, project_id, int(latest["id"]))
if latest
else None
)
old_options = await get_latest_section_options(connection, project_id)
async with connection.cursor(aiomysql.DictCursor) as cursor:
state = await get_workflow_state(cursor, str(project_id))
if not latest:
return {"error": "저장된 노선이 없어 초기 종단을 셀 수 없습니다(자동설계 먼저)."}
stage2 = next((s for s in state.get("stages", []) if int(s.get("stage_no", -1)) == 2), None)
params = dict((stage2 or {}).get("params") or {})
first, last = line[0], line[-1]
# 체인 = 새 노선 점으로 stage 2 를 세움 — 측점 간격 · 계획선 기준엔 무관
params["points"] = {
"bp": {"x": first[0], "y": first[1]},
"ep": {"x": last[0], "y": last[1]},
}
model_id = int(latest.get("surface_model_id") or 0)
seed = section_seed(redesign_solve_request(params, selection, model_id), old_options)
options = _regeneration_options(seed, params, float(seed["cross_half_width_m"]))
grade_options = _regeneration_grade_options(params)
epsg = (source or {}).get("crs_epsg")
crs = f"EPSG:{epsg}" if epsg is not None else None
surface = {k: selection.get(k) for k in ("source_filter", "method", "smooth")}
models = project_root / _MODELS_SUBDIR
route_path = str(latest.get("route_data_path") or "")
mark = ip.fingerprint(
{
"key": key,
"nodes": result["nodes"],
"curves": result.get("curves"),
"radius": radius_m,
"surface": surface,
"models": ip.stat_marks(list(models.glob(f"*_{surface['source_filter']}*.npz"))),
"streams": ip.stat_marks([_sheet_dir(stored_path) / STREAM_FILE]),
"files": ip.file_marks(
[
pipe_points_path(stored_path),
Path(structures_file_path(str(project_root))),
plus_stations_path(project_root),
project_root / route_path,
]
),
"options": asdict(options),
"grade": grade_options.as_dict() if grade_options else None,
"crs": crs,
"with_profile": request.with_profile,
"criteria": request.criteria.model_dump() if request.criteria else None,
"standard": await project_standard(project_id),
"road": await replan._road_settings(project_id),
}
)
stored = await asyncio.to_thread(ip.read_initial, project_root, key, mark)
if stored is not None:
return stored
prepared = await _prepare(project_id)
streams = [] if isinstance(prepared, JSONResponse) else prepared["streams"]
facilities = await asyncio.to_thread(
ip.followed_facilities,
project_root,
line,
route_path or None,
_xy(project_root, route_path),
read_pipe_points_file(pipe_points_path(stored_path)),
streams,
)
target = None
if request.with_profile: # [적용] `balanced_target` 과 같은 셈(노선 정점 · 새 곡선표)
points = [{"x": x, "y": y} for x, y in line]
balanced = await balanced_profile_of(
project_id, project_root, points, request.criteria, curves
)
target = None if balanced is None else (balanced["stations_m"], balanced["plan_m"])
sampler = await asyncio.to_thread(
build_surface_sampler,
models,
str(surface["source_filter"]),
str(surface["method"] or "dtm"),
bool(surface["smooth"]),
)
longitudinal = await asyncio.to_thread(
functools.partial(
ip.initial_longitudinal,
project_root,
line,
curves,
sampler,
facilities,
options=options,
grade_options=grade_options,
source_snapshot={
"filter": surface["source_filter"],
"method": surface["method"],
"smooth": bool(surface["smooth"]),
},
crs=crs,
target=target,
)
)
summary = {
**ip.profile_summary(longitudinal),
"with_profile": bool(request.with_profile),
"seconds": round(time.perf_counter() - started, 2),
}
await asyncio.to_thread(ip.write_initial, project_root, key, mark, summary)
logger.info(
"추천 초기 종단 셈: project_id=%s %s 꼭짓점 %s 직선화 %s %.2fs",
project_id,
request.mode,
summary.get("pvi_count"),
summary.get("straightened"),
summary["seconds"],
)
return summary
except Exception as error: # noqa: BLE001 — 추천은 그대로 · 화면이 이유 한 줄
logger.exception("추천 초기 종단 셈 실패: project_id=%s", project_id)
return {"error": f"초기 종단을 셈하지 못했습니다: {error}"}