From 472a6f78493af62928b33d8e0e4c7c0b89595d9a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 10:34:18 +0900 Subject: [PATCH] =?UTF-8?q?refactor(config):=20=EC=8B=9C=EC=8A=A4=ED=85=9C?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20700=EC=A4=84=20=EC=B4=88=EA=B3=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=E2=80=94=20=EC=A7=80=ED=98=95=C2=B7?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 937줄 한 파일을 셋으로 나눔 (값·이름 불변). - `config_system.py` 255줄 — 서버·로깅·DB·업로드·저장소·인증·CORS - `config_system_terrain.py` 186줄 — 5장·5-2장 지형 분석·지표면 모델 파라미터 - `config_system_design.py` 520줄 — 5-3장~5-6장 경로·유역·종횡단·계획선 파라미터 본체가 두 조각을 `import *` 로 다시 내보내므로 기존 `from config.config_system import …` 호출부는 그대로임. 검증: 분리 전 원본과 대조해 **공개 상수 243개 전부 값 동일**(사라진 것은 설계 모듈 내부에서만 쓰는 `_SCS_SOIL` 하나 — 밑줄 시작이라 원래도 외부 참조 없음), 백엔드 재시작 후 기동 정상·주요 API 6종 200, ruff check 통과, tmp/tests 378 passed Co-Authored-By: Claude Opus 5 (1M context) --- config/config_system.py | 690 +------------------------------- config/config_system_design.py | 520 ++++++++++++++++++++++++ config/config_system_terrain.py | 186 +++++++++ 3 files changed, 710 insertions(+), 686 deletions(-) create mode 100644 config/config_system_design.py create mode 100644 config/config_system_terrain.py diff --git a/config/config_system.py b/config/config_system.py index 84fdf524..6757cffd 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -108,692 +108,10 @@ SEND_ANALYSIS_COMPLETION_EMAIL = ( AUTO_DESIGN_CHAIN_ENABLED = os.getenv("AUTO_DESIGN_CHAIN_ENABLED", "True").lower() == "true" # ───────────────────────────────────────────────────────────────────────── -# 5. 지형 분석 알고리즘 파라미터 -# ───────────────────────────────────────────────────────────────────────── -# Trimesh 메쉬 생성 -MESH_GRID_SIZE = float(os.getenv("MESH_GRID_SIZE", "1.0")) # 미터 단위 -MESH_SMOOTHING_ITERATIONS = int(os.getenv("MESH_SMOOTHING_ITERATIONS", "0")) -SURFACE_LAS_CHUNK_SIZE = int(os.getenv("SURFACE_LAS_CHUNK_SIZE", "500000")) -SURFACE_DEFAULT_RGB_VALUE = int(os.getenv("SURFACE_DEFAULT_RGB_VALUE", "128")) -SURFACE_GRID_CELL_SIZE_M = float(os.getenv("SURFACE_GRID_CELL_SIZE_M", "2.0")) -SURFACE_GRID_HEIGHT_THRESHOLD_M = float(os.getenv("SURFACE_GRID_HEIGHT_THRESHOLD_M", "1.5")) - -# CSF (Cloth Simulation Filter) 지면 분류 파라미터 -SURFACE_CSF_CLOTH_RESOLUTION_M = float(os.getenv("SURFACE_CSF_CLOTH_RESOLUTION_M", "1.5")) -SURFACE_CSF_RIGIDNESS = int(os.getenv("SURFACE_CSF_RIGIDNESS", "1")) -SURFACE_CSF_TIME_STEP = float(os.getenv("SURFACE_CSF_TIME_STEP", "0.65")) -SURFACE_CSF_CLASS_THRESHOLD_M = float(os.getenv("SURFACE_CSF_CLASS_THRESHOLD_M", "0.5")) -# 낙하 후 스프링 정착에 쓰는 여유 반복 수 (실제 낙하 구간은 현장 기복에서 계산한다) -SURFACE_CSF_ITERATIONS = int(os.getenv("SURFACE_CSF_ITERATIONS", "150")) -# 낙하+정착 반복 상한 — 기복 1500m(0.3185m/회)까지 커버한다 -SURFACE_CSF_MAX_ITERATIONS = int(os.getenv("SURFACE_CSF_MAX_ITERATIONS", "5000")) -SURFACE_CSF_SLOPE_SMOOTH = os.getenv("SURFACE_CSF_SLOPE_SMOOTH", "True").lower() == "true" -SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M = float( - os.getenv("SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M", "1.8") -) - -# PMF (Progressive Morphological Filter) 지면 분류 파라미터 -SURFACE_PMF_CELL_SIZE_M = float(os.getenv("SURFACE_PMF_CELL_SIZE_M", "2.0")) -SURFACE_PMF_MAX_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_MAX_WINDOW_SIZE", "40")) -SURFACE_PMF_INITIAL_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_INITIAL_WINDOW_SIZE", "3")) -SURFACE_PMF_SLOPE = float(os.getenv("SURFACE_PMF_SLOPE", "1.0")) -SURFACE_PMF_MAX_DISTANCE_M = float(os.getenv("SURFACE_PMF_MAX_DISTANCE_M", "2.5")) - -# RANSAC (Local plane fitting) 지면 분류 파라미터 -SURFACE_RANSAC_DISTANCE_THRESHOLD_M = float(os.getenv("SURFACE_RANSAC_DISTANCE_THRESHOLD_M", "0.3")) -SURFACE_RANSAC_N = int(os.getenv("SURFACE_RANSAC_N", "3")) -SURFACE_RANSAC_ITERATIONS = int(os.getenv("SURFACE_RANSAC_ITERATIONS", "100")) -SURFACE_RANSAC_LOCAL_GRID_SIZE_M = float(os.getenv("SURFACE_RANSAC_LOCAL_GRID_SIZE_M", "10.0")) -SURFACE_RANSAC_SEED = int(os.getenv("SURFACE_RANSAC_SEED", "42")) - -# LAS 자체 지면분류(class 2) 필터 — 이 비율 이상 분류돼 있을 때만 필터로 제공한다. -# 미분류 LAS는 classification이 전부 0이라 마스크가 비어 버린다. -SURFACE_CLASSIFIED_GROUND_MIN_RATIO = float( - os.getenv("SURFACE_CLASSIFIED_GROUND_MIN_RATIO", "0.002") -) -# 지면점 비율이 이 값 미만이면 필터가 사실상 실패한 것으로 보고 WARNING을 남긴다. -SURFACE_GROUND_RATIO_WARN = float(os.getenv("SURFACE_GROUND_RATIO_WARN", "0.01")) - -# 계획노선이 지표면 밖으로 나가 잘릴 때, 잘린 쪽 끝에서 더 깎을 길이(m). -# 서피스 가장자리는 점 밀도가 떨어져 외곽선이 불규칙하다 — 경계에 딱 붙여 자르면 -# 그 구간 지반고가 못 미덥다 (2026-09-01 사용자 확정). -SURFACE_ROUTE_EDGE_TRIM_M = float(os.getenv("SURFACE_ROUTE_EDGE_TRIM_M", "30.0")) - -# ───────────────────────────────────────────────────────────────────────── -# 5-2. 지표면 모델 생성 파라미터 (TIN/DTM/NURBS/implicit/meshfree) -# ───────────────────────────────────────────────────────────────────────── -# 고를 수 있는 지면 필터 전체 — 관리자가 B04 드롭다운에서 요청할 때 쓰인다. -# 자동 전처리가 미리 만드는 조합이 아니다(그건 SURFACE_AUTO_* 가 정한다). -SURFACE_MODEL_SOURCE_FILTERS = tuple( - os.getenv("SURFACE_MODEL_SOURCE_FILTERS", "classification,grid_min_z,csf,pmf").split(",") -) -# 자동 전처리가 만드는 조합 — 기본 필터 1종 × 아래 표현. 나머지는 관리자 요청 시 만든다. -SURFACE_AUTO_METHODS = tuple(os.getenv("SURFACE_AUTO_METHODS", "dtm").split(",")) -# 기본 필터는 입력 LAS를 보고 정한다 — 지면분류(class 2)가 있으면 그것을, 없으면 아래 값. -SURFACE_AUTO_FALLBACK_FILTER = os.getenv("SURFACE_AUTO_FALLBACK_FILTER", "csf") -# dtm을 먼저 빌드해야 TIN 등고선 사전 캐시가 dtm footprint를 참조할 수 있다 (PLAN D-6) -SURFACE_MODEL_PRECOMPUTE = tuple( - os.getenv("SURFACE_MODEL_PRECOMPUTE", "dtm,tin,nurbs,implicit,meshfree").split(",") -) -SURFACE_MODEL_SMOOTHING_METHODS = tuple( - os.getenv("SURFACE_MODEL_SMOOTHING_METHODS", "dtm,tin").split(",") -) -SURFACE_MODEL_SYNC_TIMEOUT_SECONDS = int(os.getenv("SURFACE_MODEL_SYNC_TIMEOUT_SECONDS", "0")) - -# footprint(외곽) 산출 -SURFACE_FOOTPRINT_RESOLUTION_M = float(os.getenv("SURFACE_FOOTPRINT_RESOLUTION_M", "1.0")) -SURFACE_FOOTPRINT_GAP_CLOSE_M = float(os.getenv("SURFACE_FOOTPRINT_GAP_CLOSE_M", "1.0")) -SURFACE_BOUNDARY_INSET_M = float(os.getenv("SURFACE_BOUNDARY_INSET_M", "1.0")) -SURFACE_KEEP_LARGEST_FOOTPRINT = ( - os.getenv("SURFACE_KEEP_LARGEST_FOOTPRINT", "True").lower() == "true" -) -SURFACE_TILE_SIZE_M = float(os.getenv("SURFACE_TILE_SIZE_M", "50.0")) -SURFACE_MAX_PREVIEW_VERTICES = int(os.getenv("SURFACE_MAX_PREVIEW_VERTICES", "500000")) - -# 표현별 파라미터 (old 버전 검증값 기준 — PLAN F) -SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "500000")) -SURFACE_DTM_GRID_RESOLUTION_M = float(os.getenv("SURFACE_DTM_GRID_RESOLUTION_M", "1.0")) -SURFACE_NURBS_DEGREE = int(os.getenv("SURFACE_NURBS_DEGREE", "3")) -SURFACE_NURBS_PATCH_SIZE_M = float(os.getenv("SURFACE_NURBS_PATCH_SIZE_M", "50.0")) -SURFACE_NURBS_CONTROL_POINTS_PER_AXIS = int( - os.getenv("SURFACE_NURBS_CONTROL_POINTS_PER_AXIS", "16") -) -SURFACE_IMPLICIT_MAX_POINTS_PER_TILE = int( - os.getenv("SURFACE_IMPLICIT_MAX_POINTS_PER_TILE", "10000") -) -SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.1")) -SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "500000")) -SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.15")) - -# 스무딩 파라미터 -SURFACE_SMOOTHING_DTM_SIGMA_M = float(os.getenv("SURFACE_SMOOTHING_DTM_SIGMA_M", "0.5")) -SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH = float(os.getenv("SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH", "0.0")) -SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M = float( - os.getenv("SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M", "0.5") -) -SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS = int( - os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS", "10") -) -SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA", "0.5")) -SURFACE_SMOOTHING_TIN_TAUBIN_MU = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_MU", "-0.53")) - -# 등고선 파라미터 -SURFACE_CONTOUR_INTERVAL_M = float(os.getenv("SURFACE_CONTOUR_INTERVAL_M", "1.0")) -SURFACE_CONTOUR_GRID_RESOLUTION_M = float(os.getenv("SURFACE_CONTOUR_GRID_RESOLUTION_M", "1.0")) - -# 도엽등고선 3D 서피스 (LAS 없는 설계 — 2026-08-30 사용자 확정) -# 노선 XY bbox에 더하는 절취 여유(m). 300m = 횡단·코리도·성토면 여유(사용자 확정값). -SHEET_SURFACE_MARGIN_M = float(os.getenv("SHEET_SURFACE_MARGIN_M", "300.0")) -# 도엽등고선 DTM 격자 한 변(m). LAS DTM·등고선 캐시와 같은 1m(사용자 확정값). -SHEET_SURFACE_GRID_M = float(os.getenv("SHEET_SURFACE_GRID_M", "1.0")) -# 만들어 둘 등고선 보간 방식 — B04 화면에서 버튼으로 바꿔 가며 비교한다 -# (2026-08-30 사용자 지시). 정의는 B04_PreProcess_Engine_SheetMethods.py. -SHEET_SURFACE_METHODS = [ - method.strip() - for method in os.getenv( - "SHEET_SURFACE_METHODS", - "tin_sheet,tin,biharmonic,anudem,multires,laplace", - ).split(",") - if method.strip() -] -# 확정에 쓸 기본 방식 — 라플라스 (2026-08-30 사용자 확정). -SHEET_SURFACE_DEFAULT_METHOD = os.getenv("SHEET_SURFACE_DEFAULT_METHOD", "laplace") -# 자동 전처리가 만드는 도엽 방식 — 기본 하나뿐이다. 여섯 방식을 매번 다 만들면 -# WF1의 대부분(용화 실측 891초 중 655초)을 여기서 쓴다. 나머지는 관리자가 B04에서 -# 그 방식을 고를 때 만든다 (2026-09-01 사용자 확정). -SHEET_SURFACE_AUTO_METHODS = [ - method.strip() - for method in os.getenv("SHEET_SURFACE_AUTO_METHODS", SHEET_SURFACE_DEFAULT_METHOD).split(",") - if method.strip() -] - -# 일반 사용자 WF1 자동 확정 기본값 -SURFACE_CONFIRM_DEFAULT_FILTER = os.getenv("SURFACE_CONFIRM_DEFAULT_FILTER", "csf") -SURFACE_CONFIRM_DEFAULT_METHOD = os.getenv("SURFACE_CONFIRM_DEFAULT_METHOD", "dtm") -SURFACE_CONFIRM_DEFAULT_SMOOTH = ( - os.getenv("SURFACE_CONFIRM_DEFAULT_SMOOTH", "True").lower() == "true" -) - - -def build_surface_model_config() -> dict: - """지표면 모델 파이프라인이 사용하는 config dict를 조립한다.""" - return { - "source_filters": list(SURFACE_MODEL_SOURCE_FILTERS), - "precompute": list(SURFACE_MODEL_PRECOMPUTE), - "smoothing_methods": tuple(SURFACE_MODEL_SMOOTHING_METHODS), - "sync_timeout_seconds": SURFACE_MODEL_SYNC_TIMEOUT_SECONDS, - "footprint_resolution_meters": SURFACE_FOOTPRINT_RESOLUTION_M, - "footprint_gap_close_meters": SURFACE_FOOTPRINT_GAP_CLOSE_M, - "boundary_inset_meters": SURFACE_BOUNDARY_INSET_M, - "keep_largest_footprint": SURFACE_KEEP_LARGEST_FOOTPRINT, - "tile_size_meters": SURFACE_TILE_SIZE_M, - "max_preview_vertices": SURFACE_MAX_PREVIEW_VERTICES, - "tin_max_input_points": SURFACE_TIN_MAX_INPUT_POINTS, - "dtm_grid_resolution_meters": SURFACE_DTM_GRID_RESOLUTION_M, - "nurbs_degree": SURFACE_NURBS_DEGREE, - "nurbs_patch_size_meters": SURFACE_NURBS_PATCH_SIZE_M, - "nurbs_control_points_per_axis": SURFACE_NURBS_CONTROL_POINTS_PER_AXIS, - "implicit_max_points_per_tile": SURFACE_IMPLICIT_MAX_POINTS_PER_TILE, - "implicit_smoothing": SURFACE_IMPLICIT_SMOOTHING, - "meshfree_max_model_points": SURFACE_MESHFREE_MAX_MODEL_POINTS, - "meshfree_point_radius_meters": SURFACE_MESHFREE_POINT_RADIUS_M, - "smoothing_dtm_sigma_meters": SURFACE_SMOOTHING_DTM_SIGMA_M, - "smoothing_dtm_spline_smooth": SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH, - "smoothing_dtm_preview_resolution_meters": SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M, - "smoothing_tin_taubin_iterations": SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS, - "smoothing_tin_taubin_lambda": SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA, - "smoothing_tin_taubin_mu": SURFACE_SMOOTHING_TIN_TAUBIN_MU, - "contour_interval_meters": SURFACE_CONTOUR_INTERVAL_M, - "contour_grid_resolution_meters": SURFACE_CONTOUR_GRID_RESOLUTION_M, - } - - -# ───────────────────────────────────────────────────────────────────────── -# 5-3. 경로 설계 파라미터 (B05 WF2) -# ───────────────────────────────────────────────────────────────────────── -# 비용 함수 가중치 -ROUTE_W_DIST = float(os.getenv("ROUTE_W_DIST", "1.0")) -ROUTE_W_GRADE = float(os.getenv("ROUTE_W_GRADE", "2.0")) -ROUTE_W_SIDE = float(os.getenv("ROUTE_W_SIDE", "1.5")) -ROUTE_W_CURVE = float(os.getenv("ROUTE_W_CURVE", "0.5")) -ROUTE_W_AVOID = float(os.getenv("ROUTE_W_AVOID", "10.0")) -ROUTE_WEIGHT_MAX = float(os.getenv("ROUTE_WEIGHT_MAX", "1000.0")) - -# 격자·경사·비용 제약 -ROUTE_MAX_GRADE = float(os.getenv("ROUTE_MAX_GRADE", "0.14")) -ROUTE_MAX_GRADE_PAVED = float(os.getenv("ROUTE_MAX_GRADE_PAVED", "0.18")) -ROUTE_GRID_RES_M = float(os.getenv("ROUTE_GRID_RES_M", "2.0")) -ROUTE_AVOID_DEFAULT_RADIUS_M = float(os.getenv("ROUTE_AVOID_DEFAULT_RADIUS_M", "25.0")) -ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk") -ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000")) -ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0")) -# 제어점 쌍이 이 배수×비용면 셀보다 가까우면 격자 탐색 없이 직결한다 — 원청 계획노선 -# 보존 (2026-08-30 사용자 확정. 조밀 기준선은 격자 중간점이 못 끼어들어 평면 불변). -ROUTE_DIRECT_LINK_CELL_FACTOR = float(os.getenv("ROUTE_DIRECT_LINK_CELL_FACTOR", "2.0")) -# 예정노선을 자동 체인에 넘기기 전, 직결 문턱의 이 비율까지 정점 간격을 좁힌다. -# 문턱과 정확히 같게 두면 부동소수 오차 한 번에 탐색으로 넘어가 노선이 바뀐다. -ROUTE_PLANNED_DENSIFY_SAFETY = float(os.getenv("ROUTE_PLANNED_DENSIFY_SAFETY", "0.9")) -# 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존 -# 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19). -ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch") - -# 임도 등급별 종단경사 한계 및 최소 곡선반지름 (경로탐색 제약 — 계획선 법정 기준과 별개) -FOREST_ROAD_MAX_GRADE = {"trunk": 0.26, "fire": 0.26, "branch": 0.28, "work": 0.40} -FOREST_ROAD_MIN_CURVE_R_M = {"trunk": 12.0, "fire": 12.0, "branch": 10.0, "work": 6.0} - -# 대안(정속경사) 파라미터 -ROUTE_ALT_MIN_GRADE = float(os.getenv("ROUTE_ALT_MIN_GRADE", "0.08")) -ROUTE_ALT_MAX_GRADE = float(os.getenv("ROUTE_ALT_MAX_GRADE", "0.14")) -ROUTE_ALT_GRADE_TOLERANCE = float(os.getenv("ROUTE_ALT_GRADE_TOLERANCE", "0.005")) - -# 지형 skeleton (능선/계곡) 파라미터 -SKELETON_VALLEY_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_VALLEY_ACC_THRESHOLD_CELLS", "500")) -SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS = int( - os.getenv("SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS", "5000") -) -SKELETON_RIDGE_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_RIDGE_ACC_THRESHOLD_CELLS", "500")) -SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS = int( - os.getenv("SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS", "5000") -) -SKELETON_NODE_SPACING_M = float(os.getenv("SKELETON_NODE_SPACING_M", "10.0")) - - -# ───────────────────────────────────────────────────────────────────────── -# 5-3-1. 배수유역 격자 해석 파라미터 (B05 WF2 — 2026-07-31 전면 재설계) -# -# 도엽 등고선 TIN 보간 → 웅덩이 채움 → D8 물 방향 → 도로에서 상류 BFS(포인터 더블링) -# 순서로 유역을 정한다. 능선을 따로 탐지하지 않는다 — 도로로 물이 도달하는지 여부가 -# 유일한 판정 기준이며, 그 경계가 곧 능선이다. -# 라이다 DEM은 노선 주변만 커버해 유역 산정에 부족하므로 쓰지 않는다(사용자 지시). -# ───────────────────────────────────────────────────────────────────────── -# 해석 격자 한 변(m). 작을수록 정밀하나 셀 수가 제곱으로 늘어난다. -DRAINAGE_GRID_SIZE_M = float(os.getenv("DRAINAGE_GRID_SIZE_M", "1.0")) -# 1차 배수유역 반경(m). 도로 교차점 상류로 이어진 세류망과 계획 노선을 각각 이 반경으로 -# 버퍼해 합친 범위가 1차 영역이며, 그 bbox가 해석 격자다. -# 노선 버퍼가 필요한 이유: 세류 교차가 없는 구간의 도로도 격자 안에 있어야 그 구간 사면이 -# 유역으로 잡힌다. 세류망 선정이 정확해진 뒤 다시 포함했다(2026-07-31 사용자 지시). -DRAINAGE_INITIAL_RADIUS_M = float(os.getenv("DRAINAGE_INITIAL_RADIUS_M", "100.0")) -# 활성 셀이 격자 최외곽에 닿았을 때 한 번에 넓히는 폭(m). -DRAINAGE_EXPAND_STEP_M = float(os.getenv("DRAINAGE_EXPAND_STEP_M", "200.0")) -# 확장 반복 상한. 경계 링이 전부 비활성이 되면 그 전에 스스로 멈춘다(안전핀). -DRAINAGE_MAX_EXPAND_ROUNDS = int(os.getenv("DRAINAGE_MAX_EXPAND_ROUNDS", "6")) -# 최외곽 적색 셀 주변을 한 회차에 넓히는 폭(m). 좁을수록 유역 경계가 정밀하나 회차가 늘어난다. -DRAINAGE_RED_EXPAND_BAND_M = float(os.getenv("DRAINAGE_RED_EXPAND_BAND_M", "50.0")) -# 적색 확장 반복 상한. 새로 추가한 셀에 적색이 없으면 그 전에 스스로 멈춘다(안전핀). -DRAINAGE_RED_EXPAND_MAX_ROUNDS = int(os.getenv("DRAINAGE_RED_EXPAND_MAX_ROUNDS", "20")) -# 격자 셀 수 권장 상한. 넘으면 **경고만** 남기고 그대로 계산한다 — 격자 크기 자동 조절은 -# 하지 않는다(2026-07-31 사용자 지시). 느리면 위 DRAINAGE_GRID_SIZE_M을 직접 올린다. -DRAINAGE_MAX_GRID_CELLS = int(os.getenv("DRAINAGE_MAX_GRID_CELLS", "16000000")) -# 해석용 **도로 굽기 폭**(m) — 설계 도로폭이 아니다. -# -# 설계 도로폭은 등급별로 따로 있다(B05 `/sections/road-widths`: 간선 3.0 / 지선 3.0 / 작업로 2.5m). -# 여기 값은 노선을 격자에 구울 때만 쓰는 해석 파라미터다. 1m 격자에서 도로를 1셀 선으로 구우면 -# D8 대각 이동이 도로를 건너뛰어 상류 물이 도로를 지나쳐 버리므로, 3셀(=3m) 이상 두께가 필요하다. -# 그래서 설계폭과 연동하지 않고 4m로 둔다(2026-08-01 사용자 확인). 값을 바꾸면 재분석해야 한다. -DRAINAGE_ROAD_WIDTH_M = float(os.getenv("DRAINAGE_ROAD_WIDTH_M", "4.0")) -# 계획선 최저점보다 이만큼 아래인 등고선은 상류 기여가 불가능하므로 보간에서 제외한다. -DRAINAGE_CONTOUR_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_MARGIN_M", "10.0")) -# 이보다 짧은 등고선 파편은 노이즈로 보고 버린다. 봉우리 폐합 등고선은 이 값 이상이면 남는다. -DRAINAGE_CONTOUR_MIN_LENGTH_M = float(os.getenv("DRAINAGE_CONTOUR_MIN_LENGTH_M", "20.0")) -# 등고선 정점 재샘플 간격(m). 조밀할수록 TIN이 정확하나 Delaunay 비용이 커진다. -DRAINAGE_CONTOUR_RESAMPLE_M = float(os.getenv("DRAINAGE_CONTOUR_RESAMPLE_M", "5.0")) -# TIN 삼각망을 만들 때 격자 범위 밖으로 남길 여유(m). 도엽 전체 등고선을 다 물면 삼각망 -# 비용만 커지고 결과는 같다. 여유가 0이면 격자 가장자리가 TIN 밖으로 나가 NaN이 된다. -DRAINAGE_CONTOUR_CLIP_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_CLIP_MARGIN_M", "100.0")) -# 평탄면 해소용 미세 경사(m/셀). 채움 후 흐름 방향이 없는 셀에 출구 쪽 경사를 만들어 준다. -DRAINAGE_FLAT_EPSILON_M = float(os.getenv("DRAINAGE_FLAT_EPSILON_M", "0.001")) -# 관 매설 최대 간격(m). 이 간격을 넘으면 흐름 강도가 가장 큰 지점에 관을 보충한다. -DRAINAGE_PIPE_MAX_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MAX_SPACING_M", "100.0")) -# 관끼리 이보다 가까우면 같은 계곡으로 보고 하나로 합친다. -DRAINAGE_PIPE_MIN_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MIN_SPACING_M", "5.0")) -# 측구 흐름(도로 셀 → 담당 관) 판정용 종단 계획선 샘플 간격(m). -DRAINAGE_DITCH_SAMPLE_M = float(os.getenv("DRAINAGE_DITCH_SAMPLE_M", "1.0")) -# 유역 폴리곤 단순화 허용오차(m). 격자 계단 경계를 매끄럽게 줄여 응답 크기를 낮춘다. -# 격자가 1m(DRAINAGE_GRID_SIZE_M)라 경계는 1m 계단이다 — 허용오차를 격자 한 칸에 맞추면 -# 형상 손실이 격자 오차 안에 들어간다. 2.0m는 유역별로 따로 단순화하는 탓에 공유 경계가 -# 크게 어긋났다(2026-09-03 실측, 용화: 중첩 1,043㎡·틈 1,184㎡ → 1.0m에서 243㎡·369㎡, -# 면적오차 1.86%→1.01%, 좌표점 622→1,749). 0.5m 이하는 계단 점을 못 지워 좌표점만 6배로 -# 튄다(10,215). -DRAINAGE_POLYGON_SIMPLIFY_M = float(os.getenv("DRAINAGE_POLYGON_SIMPLIFY_M", "1.0")) -# 이 면적(㎡) 미만의 유역 조각은 버린다(격자 노이즈 제거). -DRAINAGE_MIN_BASIN_AREA_M2 = float(os.getenv("DRAINAGE_MIN_BASIN_AREA_M2", "100.0")) -# 격자 해석 결과 캐시 파일명. 프로젝트 저장소의 B04_PreProcess/drainage/ 아래에 놓인다. -DRAINAGE_CACHE_DIRNAME = "drainage" -DRAINAGE_CACHE_FILENAME = "watershed_grid.npz" -# 분석 응답 자체를 그대로 담아 두는 파일. 재산정하지 않는 한 이걸 그대로 돌려준다 — -# 저장 배열에서 응답을 다시 조립하면 원본과 어긋날 여지가 생긴다(2026-07-31 사용자 지시). -DRAINAGE_RESPONSE_FILENAME = "00_watershed_response.json" -# 세부유역 산출물. 관 목록이 정해져야 나오므로 해석 산출물(01~03)과 번호를 이어 붙인다. -DRAINAGE_DETAIL_FILENAME = "04_detailed_basins.geojson" - -# ── 관 매설 지점 편집분 ── -# 해석 산출물(01~03)은 다시 돌리면 덮어써도 되지만 사용자가 찍은 관은 그러면 안 된다. -# 같은 폴더 아래 편집분 전용 칸을 따로 두고 B04(관리자)·B05(사용자)가 같은 파일을 본다. -DRAINAGE_EDITS_DIRNAME = "edits" -DRAINAGE_PIPE_POINTS_FILENAME = "pipe_points.json" - -# ── B05용 평균 흐름 화살표 ── -# 셀 화살표는 1m라 축소하면 경향이 안 보인다. 이 크기의 블록으로 묶어 방향을 평균한다. -DRAINAGE_ARROW_BLOCK_M = float(os.getenv("DRAINAGE_ARROW_BLOCK_M", "10.0")) -# 화살표끼리 최소 이 간격을 두고 솎아낸다. 촘촘하면 도면이 지저분해진다. -DRAINAGE_ARROW_SPACING_M = float(os.getenv("DRAINAGE_ARROW_SPACING_M", "40.0")) -# 블록 안에서 화살표를 낼 수 있는 셀이 이 비율 미만이면 건너뛴다(가장자리 조각 방지). -DRAINAGE_ARROW_MIN_COVERAGE = float(os.getenv("DRAINAGE_ARROW_MIN_COVERAGE", "0.5")) -# 방향 일치도 하한(원형 평균 결과 길이 0~1). 블록 안 방향이 제각각이면 평균이 무의미하므로 버린다. -DRAINAGE_ARROW_MIN_AGREEMENT = float(os.getenv("DRAINAGE_ARROW_MIN_AGREEMENT", "0.7")) -# 단계별 검증 산출물은 같은 폴더에 `{번호}_{단계}.geojson` + `manifest.json`으로 쌓인다. -# 파일명 규칙은 B05_Profile_Engine_Watershed_Export.STAGES가 유일한 정의처다. - -# ── 확률강우량 (map.wamis.go.kr 등우선도 — 제주 전용) ── -# 등우선 서버 원본은 제주도만 커버한다(2026-08-13 실측) — 제주 공사지에서만 쓴다. -# 근거·검증: docs/raw/guidelines/2026-08-05_임도_배수설계_규정_조사.md 5절, PLAN.md A. -WAMIS_CONTOUR_CACHE_DIR = PROJECT_ROOT / "resources" / "data_rainfall_idf_cache" -# 프로젝트별 결과 파일명. drainage/ 아래에 놓인다. -DRAINAGE_RAINFALL_FILENAME = "rainfall_table.json" - -# ── 확률강우량 (관측소 방식 — 본토, 2026-08-13 사용자 확정 로직) ── -# 최근접 관측소 + 이 반경 안 관측소를 모두 받아 100년/24hr 최대 지점을 선정한다. -WAMIS_STATION_RADIUS_M = float(os.getenv("WAMIS_STATION_RADIUS_M", "10000")) -# 관측소 목록·확률강우량 xlsx의 프로젝트별 보존 폴더명. drainage/ 아래에 놓인다 -# (원천이 갱신되는 자료라 프로젝트 시점 스냅샷 — 전역 공통 캐시 아님). -DRAINAGE_RAINFALL_STATION_DIRNAME = "rainfall_stations" -# 설계빈도 강우강도 산출 방식: "mononobe"(기본, 실무 수리계산서 방식) | "general" -DRAINAGE_RAINFALL_IDF_METHOD = os.getenv("DRAINAGE_RAINFALL_IDF_METHOD", "mononobe") - -# ── 배수 유효직경 계산 계수 (2026-08-05 사용자 확정, 위 규정 조사 문서 6절) ── -# 합리식 유출계수 C. 산악지 준용 0.8 — 필요 시 사용자가 이 값을 고친다. -DRAINAGE_RUNOFF_COEFFICIENT = float(os.getenv("DRAINAGE_RUNOFF_COEFFICIENT", "0.8")) -# 설계유량 배수. 시행규칙 별표2 (가): 최대홍수유출량의 2.0배 이상. -DRAINAGE_DESIGN_FLOW_FACTOR = float(os.getenv("DRAINAGE_DESIGN_FLOW_FACTOR", "2.0")) -# 설계빈도(년). 별표2 (가) 1호: 100년빈도 확률강우량. -DRAINAGE_DESIGN_RETURN_PERIOD_YR = int(os.getenv("DRAINAGE_DESIGN_RETURN_PERIOD_YR", "100")) -# 홍수도달시간 하한(분). 국도건설공사 설계실무요령·도로배수지침 "강우지속기간 5분 원칙". -DRAINAGE_TC_MIN_MINUTES = float(os.getenv("DRAINAGE_TC_MIN_MINUTES", "5.0")) -# 관 경사(도). 시공 중 수시 변경되므로 지형 산출 대신 실무 대푯값 10도로 고정(사용자 제시). -DRAINAGE_PIPE_SLOPE_DEG = float(os.getenv("DRAINAGE_PIPE_SLOPE_DEG", "10.0")) -# Manning 조도계수(파형강관). -DRAINAGE_MANNING_N = float(os.getenv("DRAINAGE_MANNING_N", "0.024")) -# 관내 유속 허용범위(m/s). 도로배수지침 0.8~3.0 — 상한 클램프가 안전측(관이 커진다). -DRAINAGE_VELOCITY_MIN_MS = float(os.getenv("DRAINAGE_VELOCITY_MIN_MS", "0.8")) -DRAINAGE_VELOCITY_MAX_MS = float(os.getenv("DRAINAGE_VELOCITY_MAX_MS", "3.0")) -# 원형관 통수단면 비율. 도로설계요령: 관 단면의 70%만 통수 고려. -DRAINAGE_FLOW_AREA_RATIO = float(os.getenv("DRAINAGE_FLOW_AREA_RATIO", "0.7")) -# 세월교 검토 문턱(mm). 유효직경이 관 최대 규격(파형강관 D2000)을 넘으면 관이 아니라 -# 세월교·물넘이·교량 대상이다 — 「임도설치 및 관리 등에 관한 규정」 제12조: 계류 횡단 -# 구간은 배수구 막힘 우려가 없는 물넘이 포장(세월교) 또는 교량으로 설계(2026-08-05 조사). -DRAINAGE_BRIDGE_THRESHOLD_MM = float(os.getenv("DRAINAGE_BRIDGE_THRESHOLD_MM", "2000")) -# BOX암거 전환 문턱(mm). 교본 "수리계산 Ø1,500㎜ 이상 유역·협곡·횡단경사 40% 이내"에서 -# 유량 조건만 취한 값이다 — 횡단경사·협곡 판정은 지형 계산이 필요해 화면이 "현장 확인"으로 -# 안내한다 (2026-08-17 사용자 확정: 추천은 유량 근거만). -DRAINAGE_BOX_THRESHOLD_MM = float(os.getenv("DRAINAGE_BOX_THRESHOLD_MM", "1500")) -# 추천 관경 규격(mm) — B05 레지스트리 `pipe_diameter_mm` 선택지와 **같아야 한다**. -# 폼에서 고를 수 없는 값을 추천하면 사용자가 그대로 확정하지 못한다. -DRAINAGE_RECOMMEND_DIAMETERS_MM = (800, 1000, 1200, 1500) - -# ── 물넘이포장·세월교 개략 단면 (2026-08-17 실무문서 역산 확인) ── -# 물넘이는 노면 개수로다. 조도계수 0.017은 KDS 표 2.7-1 콘크리트 수로 "보통"값이자 -# 실무 물넘이 관측치와 일치하고, 경사 10%는 실무 수리계산서 역산값이다 — 울진1공구 -# 물넘이 시트(B=20m·h=0.27m·n=0.017 → V=7.636 m/s)와 같은 공사지 관 시트(Ø1000·240° -# 유효단면·n=0.025 → V=5.694 m/s)가 둘 다 S=0.10에서 검산이 맞는다 -# (original/실무문서/_숨김탭분석.md §6-1·§6-2). -FORD_MANNING_N = float(os.getenv("FORD_MANNING_N", "0.017")) -FORD_SLOPE = float(os.getenv("FORD_SLOPE", "0.10")) - -# ── B05 구조물 옵션 (드롭다운 목록·기본값, 2026-08-05 사용자 확정) ── -# 프론트는 이 값을 /api 설정 응답 또는 빌드타임 복사로 받아 쓴다. 수정은 여기서만 한다. -STRUCTURE_TYPES = ("배관", "기성막이", "대피로", "기타") -STRUCTURE_DEFAULT_TYPE = "배관" -PIPE_DIAMETERS_MM = { - "이중벽관": (150, 200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500), - "삼중벽관": (200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500), - "파형강관": ( - 150, - 200, - 250, - 300, - 350, - 400, - 450, - 500, - 600, - 700, - 800, - 900, - 1000, - 1200, - 1350, - 1500, - 1650, - 1800, - 2000, - ), -} -PIPE_DEFAULT_TYPE = "파형강관" -# 자동 지정 기본 관경. 별표2 (나) 예외 하한 800mm(사용자 결정 — 계산값이 더 크면 바로 위 규격). -PIPE_DEFAULT_DIAMETER_MM = int(os.getenv("PIPE_DEFAULT_DIAMETER_MM", "800")) -ESCAPE_ROUTE_WIDTHS_M = (1.5, 2.0, 2.5, 3.0) -ESCAPE_ROUTE_DEFAULT_WIDTH_M = 2.0 -STRUCTURE_ETC_DEFAULT_NAME = "기타 구조물" - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4. 종횡단 생성 파라미터 (B06 WF3) -# ───────────────────────────────────────────────────────────────────────── -SECTION_STATION_INTERVAL_M = float(os.getenv("SECTION_STATION_INTERVAL_M", "20.0")) -# 초기 파이프라인(파일입력→B06) 샘플 반폭 — 표시 반폭보다 넉넉히 뽑아 두면 사용자가 -# 표시 반폭을 이 안에서 바꿀 때 재계산이 필요 없다(2026-08-06 사용자 확정, 기준 20m). -# 표시 반폭이 이 값을 넘을 때만 B05부터 재생성한다. 향후 사용자 평균 설정 보고 조정. -SECTION_CROSS_HALF_WIDTH_M = float(os.getenv("SECTION_CROSS_HALF_WIDTH_M", "20.0")) -SECTION_CROSS_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_CROSS_SAMPLE_INTERVAL_M", "0.5")) -SECTION_LONG_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_LONG_SAMPLE_INTERVAL_M", "1.0")) -SECTION_VERTICAL_EXAGGERATION = float(os.getenv("SECTION_VERTICAL_EXAGGERATION", "1.0")) -SECTION_INCLUDE_ENDPOINT = os.getenv("SECTION_INCLUDE_ENDPOINT", "True").lower() == "true" -FOREST_ROAD_MIN_WIDTH_M = {"trunk": 3.0, "branch": 3.0, "work": 2.5} - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4-1. 표준 횡단면 단일 진실 원천 (도면 판독값, 2026-07-24 사용자 확정) -# -# 출처: `08 표준횡단도면(울진 울진 대흥 산65 외2(3공구)).bmp` 좌측 영역 판독값. -# 지반그룹 3종: soil(토사) / rock(암, 리핑·발파 공유) / paved(포장). -# 단위: 길이 m, 경사비 수평:수직=ratio:1, 횡단경사 %. -# -# 이 상수가 **표준 횡단면 기하의 유일한 정의처**다. B06 설정 패널 기본값과 -# 절·성토 단면적 엔진(B06_Section_Engine_Design)이 모두 여기를 읽으며, -# 아래 5-4-2의 파생 상수 외에 같은 값을 별도로 정의하지 않는다. -# ───────────────────────────────────────────────────────────────────────── -STANDARD_CROSS_SECTION = { - "soil": { - "road_width_m": 3.0, # 노폭(차도) - "shoulder_left_m": 0.5, # 노견(좌) - "shoulder_right_m": 0.5, # 노견(우) - # 측구(상단폭/저폭/깊이). 토사 = 900/300/300mm. - "ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3}, - "cross_slope_pct": {"min": 3.0, "max": 5.0}, # 횡단경사(측구방향) - "fill_slope_ratio": 1.2, # 성토 1:1.2 - "cut_slope_ratio": 1.0, # 절토 1:1.0 - }, - "rock": { # 리핑암·발파암 공유 - "road_width_m": 3.0, - "shoulder_left_m": 0.5, - "shoulder_right_m": 0.5, - # 일반 측구 = 690/300/300mm. - "ditch": {"top_width_m": 0.69, "bottom_width_m": 0.3, "depth_m": 0.3}, - # L형 측구 = 폭500 × 깊이100mm (횡단면도에서 일반/L형 중 선택). - "ditch_l_type": {"width_m": 0.5, "depth_m": 0.1}, - "cross_slope_pct": {"min": 3.0, "max": 3.0}, - "fill_slope_ratio": 1.2, - "cut_slope_ratio": 0.4, # 절토(암) 1:0.4 (규정 1:0.3 이상) - }, - "paved": { # 포장: 토사와 동일 기하 + 횡단경사만 다름 - "road_width_m": 3.0, - "shoulder_left_m": 0.5, - "shoulder_right_m": 0.5, - "ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3}, - "cross_slope_pct": {"min": 1.5, "max": 2.0}, - "fill_slope_ratio": 1.2, - "cut_slope_ratio": 1.0, - # 포장층 두께(도면 미표기 — 임도 콘크리트 포장 실무 표준 0.2m, 패널에서 편집 가능). - "pavement_thickness_m": 0.2, - }, -} -# 암 경계선(지면선 복사 — 계획선 아님) 기본 오프셋과 상/하 제어 스텝(m). -STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M = -0.5 -STANDARD_ROCK_BOUNDARY_STEP_M = 0.1 - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4-2. 횡단 표준단면 엔진 파생 상수 (B06 WF3) -# -# 전부 위 5-4-1 STANDARD_CROSS_SECTION에서 파생한다(중복 정의 금지). -# 지반유형 라벨은 3종(토사/리핑암/발파암)이나, 표준단면 기하는 토사/암반 2종 -# 프리셋만 존재한다(리핑암·발파암은 절토경사·측구 동일, 단가만 B08/B09에서 구분). -# ───────────────────────────────────────────────────────────────────────── -_SCS_SOIL = STANDARD_CROSS_SECTION["soil"] -# 노면폭(노견 포함) = 0.5 + 3.0 + 0.5 = 4.0m. env로만 개별 재정의 가능. -SECTION_ROADBED_WIDTH_M = float( - os.getenv( - "SECTION_ROADBED_WIDTH_M", - str( - _SCS_SOIL["road_width_m"] + _SCS_SOIL["shoulder_left_m"] + _SCS_SOIL["shoulder_right_m"] - ), - ) -) -SECTION_CARRIAGEWAY_WIDTH_M = float( - os.getenv("SECTION_CARRIAGEWAY_WIDTH_M", str(_SCS_SOIL["road_width_m"])) -) -# 성토 경사(수평:수직 = ratio:1). 지반유형 무관 고정. -SECTION_FILL_SLOPE_RATIO = float( - os.getenv("SECTION_FILL_SLOPE_RATIO", str(_SCS_SOIL["fill_slope_ratio"])) -) -# 지반유형(저장 라벨) → 기하 프리셋 키. 견적 단가 구분은 라벨 자체로 유지한다. -SECTION_GROUND_TYPE_PRESET = { - "soil": "soil", - "ripping_rock": "rock", - "blasting_rock": "rock", -} -# 단면유형: 좌절/우절(편절편성), 양절, 양성. 좌=양(+)offset, 우=음(-)offset. -SECTION_MODES = ("left_cut", "right_cut", "both_cut", "both_fill") -SECTION_DITCH_SIDES = ("left", "right") -# 측구 형식: 일반(사다리꼴) / L형(암 구간 전용 선택지). -SECTION_DITCH_TYPES = ("standard", "l_type") - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4-3. 토량환산계수 (B06 유토곡선) -# -# loose(L) = 흐트러진 상태 토량 / 자연상태 토량 (팽창률) -# compacted(C) = 다져진 상태 토량 / 자연상태 토량 (다짐률) -# -# 유토곡선은 운반계획 도면이므로 **다짐상태 기준**으로 계산한다. -# "운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고, -# 내역서에 적용하는 수량은 자연상태로 한다." -# (2021년도 국도건설공사 설계실무 요령 / 2016 건설공사 표준품셈 계열) -# -# 아래 값은 표준품셈 토량환산계수표의 암종별 범위를 B06 지반유형 3종에 대응시킨 -# 제안값이며 임도 전용 고시 수치가 아니다. -# 토사 ← 풍화토(L 1.10~1.25 / C 0.80~0.90) ~ 점토(L 1.20~1.35 / C 0.75~0.90) -# 리핑암 ← 풍화암(L 1.30~1.35 / C 1.00~1.15) ~ 연암(L 1.30~1.50 / C 1.00~1.30) 하단 -# 발파암 ← 보통암(L 1.55~1.70 / C 1.20~1.40) 중앙 -# 표준품셈도 "토질 시험하여 적용하는 것을 원칙으로 하되 소량인 경우 환산계수표에 -# 따를 수 있다"고 하므로 현장별 조정이 전제다. 값을 바꾸려면 여기만 고치면 된다. -# -# 이 상수가 **토량환산계수의 유일한 정의처**다(타 파일 중복 정의 금지). -# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 8절 -# ───────────────────────────────────────────────────────────────────────── -EARTHWORK_CONVERSION_FACTORS = { - "soil": {"loose": 1.25, "compacted": 0.90}, - "ripping_rock": {"loose": 1.35, "compacted": 1.15}, - "blasting_rock": {"loose": 1.60, "compacted": 1.30}, -} - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4-5. 토공 운반장비 선정 거리 경계 (B06 유토곡선 운반계획) -# -# 평균운반거리(m)로 운반장비를 고른다. 위에서부터 순서대로 검사해 `max_distance_m` -# **이하**면 그 장비를 쓰고, 마지막 항목(None)이 나머지를 전부 받는다. -# -# 종무대(무대운반) ≤ 20m < 도쟈(불도저 압토) ≤ 70m < 덤프트럭 -# -# 2026-08-02 사용자 확정값(도쟈 50 → 70m). 유토곡선의 장비 경계현은 이 값을 -# **수평 현의 길이**로 읽는다 — 현 길이가 20m가 되는 높이 위쪽이 종무대 몫, -# 70m가 되는 높이까지가 도쟈 몫, 그 아래가 덤프 몫이다. -# 현장·발주처 기준에 따라 달라질 수 있으므로 -# **이 상수가 거리 경계의 유일한 정의처**이며 여기만 고치면 된다. -# -# key는 도면 표기와 다음과 같이 대응한다: free_haul=종무대, dozer=도쟈, dump_truck=덤프. -# 화면 표기 문구는 하드코딩 금지 규칙에 따라 ui_locales에서 따로 관리한다. -# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md -# ───────────────────────────────────────────────────────────────────────── -EARTHWORK_HAUL_EQUIPMENT_LIMITS_M = ( - ("free_haul", 20.0), - ("dozer", 70.0), - ("dump_truck", None), -) - - -# ───────────────────────────────────────────────────────────────────────── -# 5-4-2. 자연방토 판정 경사 (B06 유토곡선) -# -# 자연방토 = 남는 흙을 성토사면 아래로 흘려보내 **운반비를 세지 않는** 처리. -# 판정 기준은 **성토측 자연 지반의 경사**다(2026-08-02 사용자 확정). 지반이 이보다 가파르면 -# 부어 놓은 흙이 쌓이지 않고 스스로 흘러내리므로 따로 운반하지 않는다. -# -# 값은 rise/run(무차원). 1/1.5 = 0.667 ≈ 33.7°로, 토사 안식각 상한이자 표준 성토 경사 1:1.5와 -# 같은 자리다. 현장·발주처 기준에 따라 달라질 수 있으므로 **이 상수가 유일한 정의처**다. -# ───────────────────────────────────────────────────────────────────────── -NATURAL_SPOIL_MIN_GROUND_SLOPE = 1.0 / 1.5 - - -# ───────────────────────────────────────────────────────────────────────── -# 5-5. 종단 계획선(계획고) 설계 기준 (B05 WF2) -# -# 출처: 「임도설치 및 관리 등에 관한 규정」[별표 1-2] 임도의 설계 및 시설기준. -# 기준은 임도등급이 아니라 `설계속도 × 지형구분(일반/특수)`으로 규정되어 있어 -# 등급은 grade_to_design_speed로 간접 매핑한다. -# 위 5-3의 경로탐색(평면) 기준(FOREST_ROAD_MAX_GRADE 등)과는 별개의 값이므로 -# 서로 혼용하지 않는다. -# ───────────────────────────────────────────────────────────────────────── -FOREST_ROAD_PROFILE_CRITERIA = { - # 설계속도(km/h)별 법정 기준 - "design_speed": { - 40: { - "max_grade_pct": {"normal": 7.0, "special": 10.0}, - "max_reverse_grade_pct": 5.0, - "min_vertical_radius_m": 450.0, - "min_curve_length_m": 40.0, - }, - 30: { - "max_grade_pct": {"normal": 8.0, "special": 12.0}, - "max_reverse_grade_pct": 5.0, - "min_vertical_radius_m": 250.0, - "min_curve_length_m": 30.0, - }, - 20: { - "max_grade_pct": {"normal": 9.0, "special": 14.0}, - "max_reverse_grade_pct": 5.0, - "min_vertical_radius_m": 100.0, - "min_curve_length_m": 20.0, - }, - }, - # 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이 - # 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서 - # 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른 - # 설계속도가 오면 그 값이 우선한다(design_speed_kph). - # trunk = 간선임도 / fire = 산불진화임도 / work = 작업임도 - # branch(지선)는 현행 규칙에서 폐지 — 기존 데이터 호환용으로만 남긴다. - "grade_to_design_speed": {"trunk": 20, "fire": 20, "work": 20, "branch": 20}, - # 화면에서 고를 수 있는 설계속도 — 작업임도는 20만 허용된다(별표2 Ⅰ.3). - "selectable_design_speeds": {"trunk": (20, 30, 40), "fire": (20, 30, 40), "work": (20,)}, - # 특수지형에서 기준 적용이 어려운 경우 노면포장 시에 한하여 허용되는 상한 - "paved_exception_grade_pct": 18.0, - # 비포장 도로이면서 종단기울기 대수차가 이 값 이하이면 종단곡선을 두지 않는다 - "vertical_curve_skip_delta_pct": 5.0, - # 곡선 중첩 방지용 최소 직선(tangent) 길이 (규정 외 실무 기본값) - "min_tangent_length_m": 20.0, - # 절·성토 균형 구역 기본 길이 (None이면 노선 전체를 1구역으로 본다) - "balance_segment_length_m": None, - # 주 진행방향 자동 판정 기준: |시종점 고도차| / (최고−최저) 가 이 값 이상이면 - # 한 방향으로 오르내리는 노선으로 보고 반대 방향에 역기울기 상한을 적용한다. - # V자(계곡 횡단)·Λ자(능선 통과) 노선은 이 비율이 낮아 역기울기 판정을 하지 않는다. - "main_direction_monotone_ratio": 0.5, -} -GRADE_TERRAIN_TYPES = ("normal", "special") -# 주 진행방향: auto(자동 판정) / ascending(상행) / descending(하행) / none(역기울기 미적용) -GRADE_MAIN_DIRECTIONS = ("auto", "ascending", "descending", "none") - - -# ───────────────────────────────────────────────────────────────────────── -# 5-6. 종단 계획선 선형(직선 + 측점 위 종단곡선) 및 편집 정책 (B05 WF2) -# -# 위 5-5의 FOREST_ROAD_PROFILE_CRITERIA는 법정 기준값이고, 여기는 계획선을 -# "지반 추종 직선 분할 + 측점 위 종단곡선"으로 만들고 사용자가 0.1m 단위로 -# 편집하는 동작을 제어하는 실무 정책값이다. 서로 혼용하지 않는다. -# -# 변화점(PVI)은 반드시 기준 측점 위에만 놓이며, 종단곡선은 그 측점을 중심으로 -# 대칭 배치되어 곡선 좌우에 직선 구간이 반드시 남는다. -# ───────────────────────────────────────────────────────────────────────── -FOREST_ROAD_PROFILE_ALIGNMENT = { - # 절·성토 균형 허용 오차: |절토 − 성토| / max(절토, 성토) (%) - # 5% 미만이면 지반 추종이 왜곡되어 불필요한 변화점이 늘고, 15%를 넘으면 - # 사토·객토 운반 물량 부담이 커진다. 10%를 실무 균형점으로 둔다. - "balance_tolerance_percent": 10.0, - # 종단곡선 기본 **반경** R = 측점간격 × 이 비율 (변화점 대칭 배치). - # R을 1차 값으로 두어야 계획고를 편집해도 R이 흔들리지 않는다(곡선길이 L이 대신 변한다). - # L = R × |기울기 대수차 A(비율)| - # 주의: 측점간격 20m 기준 R=8m이면 A=5%일 때 L=0.4m, A=15%일 때 L=1.2m로 - # 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다 - # (예: 20.0 → R=400m, A=10%일 때 L=40m). - "curve_radius_ratio": 0.40, - # **기본 종단곡선 길이 L(m) — 모든 변화점의 1차 기준값.** - # 임도 계획선은 변화점 사이 대수차 A가 작아(1~3%가 흔하다) 반경 R을 기준으로 잡으면 - # L = R × A 가 변화점마다 3~50m로 널뛰고, 작은 쪽은 도면에서 직선과 구분되지 않는다 - # (2026-08-03 사용자 지적). 그래서 **L을 기준값으로 고정**하고 R = L / A 로 역산한다. - # 결과적으로 호 길이가 변화점마다 같아 도면이 고르게 읽힌다. - # 인접 직선이 짧아 `curve_tangent_max_ratio`에 걸리면 넣을 수 있는 **최대 L**까지만 - # 줄이고 경고를 남긴다(0으로 죽이지 않는다 — 2026-08-08 사용자 지시). - # 사용자가 그 변화점 R을 직접 지정하면 그쪽이 이긴다. 호를 더/덜 길게 보이려면 여기만 고친다. - "default_curve_length_m": 15.0, - # 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율. - # 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다. - "curve_tangent_max_ratio": 0.45, - # 법정 예외(비포장 & 대수차 5% 이하)를 실제 기하에서도 곡선 생략으로 적용할지. - # 규정 다-(3)-(다)는 "두지 않을 수 있다"는 허용 조항이며, 임도 실무 도면은 대수차가 - # 작아도 변화점을 원곡선으로 처리한다. 기본값 False = 곡선을 항상 삽입하고 - # 해당 구간에는 "생략 가능" 표시만 남긴다(True로 바꾸면 곡선을 실제로 뺀다). - "curve_skip_legal_exception": False, - # 변화점(PVI) 추가 페널티 (직선 분할 DP 목적함수, 단위 m²). - # 변화점 하나를 늘리려면 잔차제곱합이 이 값 이상 개선되어야 채택된다. - "pvi_penalty_m2": 25.0, - # 직선 분할 시 한 구간이 가질 수 있는 최소 측점 개수 (짧은 토막 방지) - "min_segment_stations": 2, - # 사용자 편집 스텝(m). 그래프 상·하단 버튼 1클릭당 계획고 이동량. - "edit_step_m": 0.1, - # 법정 종단기울기 상한 초과 시 동작: "warn"(경고만) | "block"(편집 차단) - "grade_violation_policy": "warn", -} - +# 5장(지형·설계 파라미터)은 파일이 700줄을 넘어 떼어냈다(2026-09-04). +# 여기서 그대로 다시 내보내므로 `from config.config_system import …` 호출부는 불변이다. +from config.config_system_design import * # noqa: E402,F401,F403 +from config.config_system_terrain import * # noqa: E402,F401,F403 # ───────────────────────────────────────────────────────────────────────── # 6. 저장소 경로 diff --git a/config/config_system_design.py b/config/config_system_design.py new file mode 100644 index 00000000..881ce871 --- /dev/null +++ b/config/config_system_design.py @@ -0,0 +1,520 @@ +"""경로·종횡단·계획선 설계 파라미터 (5-3장 ~ 5-6장). + +`config_system.py` 가 700줄을 넘어 떼어냈다(2026-09-04). 값·이름은 그대로이고, +`config_system` 이 다시 내보내므로 기존 `from config.config_system import …` 는 불변이다. +""" + +import os +from pathlib import Path + +# 지형 파라미터를 참조하는 값이 있어 함께 가져온다. +from config.config_system_terrain import * # noqa: F403 + +# 저장소 뿌리 — 본체(`config_system`)를 되부르면 순환이라 여기서 같은 식으로 잡는다. +PROJECT_ROOT = Path(__file__).resolve().parent.parent + +# ───────────────────────────────────────────────────────────────────────── +# 5-3. 경로 설계 파라미터 (B05 WF2) +# ───────────────────────────────────────────────────────────────────────── +# 비용 함수 가중치 +ROUTE_W_DIST = float(os.getenv("ROUTE_W_DIST", "1.0")) +ROUTE_W_GRADE = float(os.getenv("ROUTE_W_GRADE", "2.0")) +ROUTE_W_SIDE = float(os.getenv("ROUTE_W_SIDE", "1.5")) +ROUTE_W_CURVE = float(os.getenv("ROUTE_W_CURVE", "0.5")) +ROUTE_W_AVOID = float(os.getenv("ROUTE_W_AVOID", "10.0")) +ROUTE_WEIGHT_MAX = float(os.getenv("ROUTE_WEIGHT_MAX", "1000.0")) + +# 격자·경사·비용 제약 +ROUTE_MAX_GRADE = float(os.getenv("ROUTE_MAX_GRADE", "0.14")) +ROUTE_MAX_GRADE_PAVED = float(os.getenv("ROUTE_MAX_GRADE_PAVED", "0.18")) +ROUTE_GRID_RES_M = float(os.getenv("ROUTE_GRID_RES_M", "2.0")) +ROUTE_AVOID_DEFAULT_RADIUS_M = float(os.getenv("ROUTE_AVOID_DEFAULT_RADIUS_M", "25.0")) +ROUTE_DEFAULT_GRADE_CLASS = os.getenv("ROUTE_DEFAULT_GRADE_CLASS", "trunk") +ROUTE_MAX_COST_CELLS = int(os.getenv("ROUTE_MAX_COST_CELLS", "4000000")) +ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERANCE_M", "1.0")) +# 제어점 쌍이 이 배수×비용면 셀보다 가까우면 격자 탐색 없이 직결한다 — 원청 계획노선 +# 보존 (2026-08-30 사용자 확정. 조밀 기준선은 격자 중간점이 못 끼어들어 평면 불변). +ROUTE_DIRECT_LINK_CELL_FACTOR = float(os.getenv("ROUTE_DIRECT_LINK_CELL_FACTOR", "2.0")) +# 예정노선을 자동 체인에 넘기기 전, 직결 문턱의 이 비율까지 정점 간격을 좁힌다. +# 문턱과 정확히 같게 두면 부동소수 오차 한 번에 탐색으로 넘어가 노선이 바뀐다. +ROUTE_PLANNED_DENSIFY_SAFETY = float(os.getenv("ROUTE_PLANNED_DENSIFY_SAFETY", "0.9")) +# 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존 +# 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19). +ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch") + +# 임도 등급별 종단경사 한계 및 최소 곡선반지름 (경로탐색 제약 — 계획선 법정 기준과 별개) +FOREST_ROAD_MAX_GRADE = {"trunk": 0.26, "fire": 0.26, "branch": 0.28, "work": 0.40} +FOREST_ROAD_MIN_CURVE_R_M = {"trunk": 12.0, "fire": 12.0, "branch": 10.0, "work": 6.0} + +# 대안(정속경사) 파라미터 +ROUTE_ALT_MIN_GRADE = float(os.getenv("ROUTE_ALT_MIN_GRADE", "0.08")) +ROUTE_ALT_MAX_GRADE = float(os.getenv("ROUTE_ALT_MAX_GRADE", "0.14")) +ROUTE_ALT_GRADE_TOLERANCE = float(os.getenv("ROUTE_ALT_GRADE_TOLERANCE", "0.005")) + +# 지형 skeleton (능선/계곡) 파라미터 +SKELETON_VALLEY_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_VALLEY_ACC_THRESHOLD_CELLS", "500")) +SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS = int( + os.getenv("SKELETON_MAIN_VALLEY_ACC_THRESHOLD_CELLS", "5000") +) +SKELETON_RIDGE_ACC_THRESHOLD_CELLS = int(os.getenv("SKELETON_RIDGE_ACC_THRESHOLD_CELLS", "500")) +SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS = int( + os.getenv("SKELETON_MAIN_RIDGE_ACC_THRESHOLD_CELLS", "5000") +) +SKELETON_NODE_SPACING_M = float(os.getenv("SKELETON_NODE_SPACING_M", "10.0")) + + +# ───────────────────────────────────────────────────────────────────────── +# 5-3-1. 배수유역 격자 해석 파라미터 (B05 WF2 — 2026-07-31 전면 재설계) +# +# 도엽 등고선 TIN 보간 → 웅덩이 채움 → D8 물 방향 → 도로에서 상류 BFS(포인터 더블링) +# 순서로 유역을 정한다. 능선을 따로 탐지하지 않는다 — 도로로 물이 도달하는지 여부가 +# 유일한 판정 기준이며, 그 경계가 곧 능선이다. +# 라이다 DEM은 노선 주변만 커버해 유역 산정에 부족하므로 쓰지 않는다(사용자 지시). +# ───────────────────────────────────────────────────────────────────────── +# 해석 격자 한 변(m). 작을수록 정밀하나 셀 수가 제곱으로 늘어난다. +DRAINAGE_GRID_SIZE_M = float(os.getenv("DRAINAGE_GRID_SIZE_M", "1.0")) +# 1차 배수유역 반경(m). 도로 교차점 상류로 이어진 세류망과 계획 노선을 각각 이 반경으로 +# 버퍼해 합친 범위가 1차 영역이며, 그 bbox가 해석 격자다. +# 노선 버퍼가 필요한 이유: 세류 교차가 없는 구간의 도로도 격자 안에 있어야 그 구간 사면이 +# 유역으로 잡힌다. 세류망 선정이 정확해진 뒤 다시 포함했다(2026-07-31 사용자 지시). +DRAINAGE_INITIAL_RADIUS_M = float(os.getenv("DRAINAGE_INITIAL_RADIUS_M", "100.0")) +# 활성 셀이 격자 최외곽에 닿았을 때 한 번에 넓히는 폭(m). +DRAINAGE_EXPAND_STEP_M = float(os.getenv("DRAINAGE_EXPAND_STEP_M", "200.0")) +# 확장 반복 상한. 경계 링이 전부 비활성이 되면 그 전에 스스로 멈춘다(안전핀). +DRAINAGE_MAX_EXPAND_ROUNDS = int(os.getenv("DRAINAGE_MAX_EXPAND_ROUNDS", "6")) +# 최외곽 적색 셀 주변을 한 회차에 넓히는 폭(m). 좁을수록 유역 경계가 정밀하나 회차가 늘어난다. +DRAINAGE_RED_EXPAND_BAND_M = float(os.getenv("DRAINAGE_RED_EXPAND_BAND_M", "50.0")) +# 적색 확장 반복 상한. 새로 추가한 셀에 적색이 없으면 그 전에 스스로 멈춘다(안전핀). +DRAINAGE_RED_EXPAND_MAX_ROUNDS = int(os.getenv("DRAINAGE_RED_EXPAND_MAX_ROUNDS", "20")) +# 격자 셀 수 권장 상한. 넘으면 **경고만** 남기고 그대로 계산한다 — 격자 크기 자동 조절은 +# 하지 않는다(2026-07-31 사용자 지시). 느리면 위 DRAINAGE_GRID_SIZE_M을 직접 올린다. +DRAINAGE_MAX_GRID_CELLS = int(os.getenv("DRAINAGE_MAX_GRID_CELLS", "16000000")) +# 해석용 **도로 굽기 폭**(m) — 설계 도로폭이 아니다. +# +# 설계 도로폭은 등급별로 따로 있다(B05 `/sections/road-widths`: 간선 3.0 / 지선 3.0 / 작업로 2.5m). +# 여기 값은 노선을 격자에 구울 때만 쓰는 해석 파라미터다. 1m 격자에서 도로를 1셀 선으로 구우면 +# D8 대각 이동이 도로를 건너뛰어 상류 물이 도로를 지나쳐 버리므로, 3셀(=3m) 이상 두께가 필요하다. +# 그래서 설계폭과 연동하지 않고 4m로 둔다(2026-08-01 사용자 확인). 값을 바꾸면 재분석해야 한다. +DRAINAGE_ROAD_WIDTH_M = float(os.getenv("DRAINAGE_ROAD_WIDTH_M", "4.0")) +# 계획선 최저점보다 이만큼 아래인 등고선은 상류 기여가 불가능하므로 보간에서 제외한다. +DRAINAGE_CONTOUR_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_MARGIN_M", "10.0")) +# 이보다 짧은 등고선 파편은 노이즈로 보고 버린다. 봉우리 폐합 등고선은 이 값 이상이면 남는다. +DRAINAGE_CONTOUR_MIN_LENGTH_M = float(os.getenv("DRAINAGE_CONTOUR_MIN_LENGTH_M", "20.0")) +# 등고선 정점 재샘플 간격(m). 조밀할수록 TIN이 정확하나 Delaunay 비용이 커진다. +DRAINAGE_CONTOUR_RESAMPLE_M = float(os.getenv("DRAINAGE_CONTOUR_RESAMPLE_M", "5.0")) +# TIN 삼각망을 만들 때 격자 범위 밖으로 남길 여유(m). 도엽 전체 등고선을 다 물면 삼각망 +# 비용만 커지고 결과는 같다. 여유가 0이면 격자 가장자리가 TIN 밖으로 나가 NaN이 된다. +DRAINAGE_CONTOUR_CLIP_MARGIN_M = float(os.getenv("DRAINAGE_CONTOUR_CLIP_MARGIN_M", "100.0")) +# 평탄면 해소용 미세 경사(m/셀). 채움 후 흐름 방향이 없는 셀에 출구 쪽 경사를 만들어 준다. +DRAINAGE_FLAT_EPSILON_M = float(os.getenv("DRAINAGE_FLAT_EPSILON_M", "0.001")) +# 관 매설 최대 간격(m). 이 간격을 넘으면 흐름 강도가 가장 큰 지점에 관을 보충한다. +DRAINAGE_PIPE_MAX_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MAX_SPACING_M", "100.0")) +# 관끼리 이보다 가까우면 같은 계곡으로 보고 하나로 합친다. +DRAINAGE_PIPE_MIN_SPACING_M = float(os.getenv("DRAINAGE_PIPE_MIN_SPACING_M", "5.0")) +# 측구 흐름(도로 셀 → 담당 관) 판정용 종단 계획선 샘플 간격(m). +DRAINAGE_DITCH_SAMPLE_M = float(os.getenv("DRAINAGE_DITCH_SAMPLE_M", "1.0")) +# 유역 폴리곤 단순화 허용오차(m). 격자 계단 경계를 매끄럽게 줄여 응답 크기를 낮춘다. +# 격자가 1m(DRAINAGE_GRID_SIZE_M)라 경계는 1m 계단이다 — 허용오차를 격자 한 칸에 맞추면 +# 형상 손실이 격자 오차 안에 들어간다. 2.0m는 유역별로 따로 단순화하는 탓에 공유 경계가 +# 크게 어긋났다(2026-09-03 실측, 용화: 중첩 1,043㎡·틈 1,184㎡ → 1.0m에서 243㎡·369㎡, +# 면적오차 1.86%→1.01%, 좌표점 622→1,749). 0.5m 이하는 계단 점을 못 지워 좌표점만 6배로 +# 튄다(10,215). +DRAINAGE_POLYGON_SIMPLIFY_M = float(os.getenv("DRAINAGE_POLYGON_SIMPLIFY_M", "1.0")) +# 이 면적(㎡) 미만의 유역 조각은 버린다(격자 노이즈 제거). +DRAINAGE_MIN_BASIN_AREA_M2 = float(os.getenv("DRAINAGE_MIN_BASIN_AREA_M2", "100.0")) +# 격자 해석 결과 캐시 파일명. 프로젝트 저장소의 B04_PreProcess/drainage/ 아래에 놓인다. +DRAINAGE_CACHE_DIRNAME = "drainage" +DRAINAGE_CACHE_FILENAME = "watershed_grid.npz" +# 분석 응답 자체를 그대로 담아 두는 파일. 재산정하지 않는 한 이걸 그대로 돌려준다 — +# 저장 배열에서 응답을 다시 조립하면 원본과 어긋날 여지가 생긴다(2026-07-31 사용자 지시). +DRAINAGE_RESPONSE_FILENAME = "00_watershed_response.json" +# 세부유역 산출물. 관 목록이 정해져야 나오므로 해석 산출물(01~03)과 번호를 이어 붙인다. +DRAINAGE_DETAIL_FILENAME = "04_detailed_basins.geojson" + +# ── 관 매설 지점 편집분 ── +# 해석 산출물(01~03)은 다시 돌리면 덮어써도 되지만 사용자가 찍은 관은 그러면 안 된다. +# 같은 폴더 아래 편집분 전용 칸을 따로 두고 B04(관리자)·B05(사용자)가 같은 파일을 본다. +DRAINAGE_EDITS_DIRNAME = "edits" +DRAINAGE_PIPE_POINTS_FILENAME = "pipe_points.json" + +# ── B05용 평균 흐름 화살표 ── +# 셀 화살표는 1m라 축소하면 경향이 안 보인다. 이 크기의 블록으로 묶어 방향을 평균한다. +DRAINAGE_ARROW_BLOCK_M = float(os.getenv("DRAINAGE_ARROW_BLOCK_M", "10.0")) +# 화살표끼리 최소 이 간격을 두고 솎아낸다. 촘촘하면 도면이 지저분해진다. +DRAINAGE_ARROW_SPACING_M = float(os.getenv("DRAINAGE_ARROW_SPACING_M", "40.0")) +# 블록 안에서 화살표를 낼 수 있는 셀이 이 비율 미만이면 건너뛴다(가장자리 조각 방지). +DRAINAGE_ARROW_MIN_COVERAGE = float(os.getenv("DRAINAGE_ARROW_MIN_COVERAGE", "0.5")) +# 방향 일치도 하한(원형 평균 결과 길이 0~1). 블록 안 방향이 제각각이면 평균이 무의미하므로 버린다. +DRAINAGE_ARROW_MIN_AGREEMENT = float(os.getenv("DRAINAGE_ARROW_MIN_AGREEMENT", "0.7")) +# 단계별 검증 산출물은 같은 폴더에 `{번호}_{단계}.geojson` + `manifest.json`으로 쌓인다. +# 파일명 규칙은 B05_Profile_Engine_Watershed_Export.STAGES가 유일한 정의처다. + +# ── 확률강우량 (map.wamis.go.kr 등우선도 — 제주 전용) ── +# 등우선 서버 원본은 제주도만 커버한다(2026-08-13 실측) — 제주 공사지에서만 쓴다. +# 근거·검증: docs/raw/guidelines/2026-08-05_임도_배수설계_규정_조사.md 5절, PLAN.md A. +WAMIS_CONTOUR_CACHE_DIR = PROJECT_ROOT / "resources" / "data_rainfall_idf_cache" +# 프로젝트별 결과 파일명. drainage/ 아래에 놓인다. +DRAINAGE_RAINFALL_FILENAME = "rainfall_table.json" + +# ── 확률강우량 (관측소 방식 — 본토, 2026-08-13 사용자 확정 로직) ── +# 최근접 관측소 + 이 반경 안 관측소를 모두 받아 100년/24hr 최대 지점을 선정한다. +WAMIS_STATION_RADIUS_M = float(os.getenv("WAMIS_STATION_RADIUS_M", "10000")) +# 관측소 목록·확률강우량 xlsx의 프로젝트별 보존 폴더명. drainage/ 아래에 놓인다 +# (원천이 갱신되는 자료라 프로젝트 시점 스냅샷 — 전역 공통 캐시 아님). +DRAINAGE_RAINFALL_STATION_DIRNAME = "rainfall_stations" +# 설계빈도 강우강도 산출 방식: "mononobe"(기본, 실무 수리계산서 방식) | "general" +DRAINAGE_RAINFALL_IDF_METHOD = os.getenv("DRAINAGE_RAINFALL_IDF_METHOD", "mononobe") + +# ── 배수 유효직경 계산 계수 (2026-08-05 사용자 확정, 위 규정 조사 문서 6절) ── +# 합리식 유출계수 C. 산악지 준용 0.8 — 필요 시 사용자가 이 값을 고친다. +DRAINAGE_RUNOFF_COEFFICIENT = float(os.getenv("DRAINAGE_RUNOFF_COEFFICIENT", "0.8")) +# 설계유량 배수. 시행규칙 별표2 (가): 최대홍수유출량의 2.0배 이상. +DRAINAGE_DESIGN_FLOW_FACTOR = float(os.getenv("DRAINAGE_DESIGN_FLOW_FACTOR", "2.0")) +# 설계빈도(년). 별표2 (가) 1호: 100년빈도 확률강우량. +DRAINAGE_DESIGN_RETURN_PERIOD_YR = int(os.getenv("DRAINAGE_DESIGN_RETURN_PERIOD_YR", "100")) +# 홍수도달시간 하한(분). 국도건설공사 설계실무요령·도로배수지침 "강우지속기간 5분 원칙". +DRAINAGE_TC_MIN_MINUTES = float(os.getenv("DRAINAGE_TC_MIN_MINUTES", "5.0")) +# 관 경사(도). 시공 중 수시 변경되므로 지형 산출 대신 실무 대푯값 10도로 고정(사용자 제시). +DRAINAGE_PIPE_SLOPE_DEG = float(os.getenv("DRAINAGE_PIPE_SLOPE_DEG", "10.0")) +# Manning 조도계수(파형강관). +DRAINAGE_MANNING_N = float(os.getenv("DRAINAGE_MANNING_N", "0.024")) +# 관내 유속 허용범위(m/s). 도로배수지침 0.8~3.0 — 상한 클램프가 안전측(관이 커진다). +DRAINAGE_VELOCITY_MIN_MS = float(os.getenv("DRAINAGE_VELOCITY_MIN_MS", "0.8")) +DRAINAGE_VELOCITY_MAX_MS = float(os.getenv("DRAINAGE_VELOCITY_MAX_MS", "3.0")) +# 원형관 통수단면 비율. 도로설계요령: 관 단면의 70%만 통수 고려. +DRAINAGE_FLOW_AREA_RATIO = float(os.getenv("DRAINAGE_FLOW_AREA_RATIO", "0.7")) +# 세월교 검토 문턱(mm). 유효직경이 관 최대 규격(파형강관 D2000)을 넘으면 관이 아니라 +# 세월교·물넘이·교량 대상이다 — 「임도설치 및 관리 등에 관한 규정」 제12조: 계류 횡단 +# 구간은 배수구 막힘 우려가 없는 물넘이 포장(세월교) 또는 교량으로 설계(2026-08-05 조사). +DRAINAGE_BRIDGE_THRESHOLD_MM = float(os.getenv("DRAINAGE_BRIDGE_THRESHOLD_MM", "2000")) +# BOX암거 전환 문턱(mm). 교본 "수리계산 Ø1,500㎜ 이상 유역·협곡·횡단경사 40% 이내"에서 +# 유량 조건만 취한 값이다 — 횡단경사·협곡 판정은 지형 계산이 필요해 화면이 "현장 확인"으로 +# 안내한다 (2026-08-17 사용자 확정: 추천은 유량 근거만). +DRAINAGE_BOX_THRESHOLD_MM = float(os.getenv("DRAINAGE_BOX_THRESHOLD_MM", "1500")) +# 추천 관경 규격(mm) — B05 레지스트리 `pipe_diameter_mm` 선택지와 **같아야 한다**. +# 폼에서 고를 수 없는 값을 추천하면 사용자가 그대로 확정하지 못한다. +DRAINAGE_RECOMMEND_DIAMETERS_MM = (800, 1000, 1200, 1500) + +# ── 물넘이포장·세월교 개략 단면 (2026-08-17 실무문서 역산 확인) ── +# 물넘이는 노면 개수로다. 조도계수 0.017은 KDS 표 2.7-1 콘크리트 수로 "보통"값이자 +# 실무 물넘이 관측치와 일치하고, 경사 10%는 실무 수리계산서 역산값이다 — 울진1공구 +# 물넘이 시트(B=20m·h=0.27m·n=0.017 → V=7.636 m/s)와 같은 공사지 관 시트(Ø1000·240° +# 유효단면·n=0.025 → V=5.694 m/s)가 둘 다 S=0.10에서 검산이 맞는다 +# (original/실무문서/_숨김탭분석.md §6-1·§6-2). +FORD_MANNING_N = float(os.getenv("FORD_MANNING_N", "0.017")) +FORD_SLOPE = float(os.getenv("FORD_SLOPE", "0.10")) + +# ── B05 구조물 옵션 (드롭다운 목록·기본값, 2026-08-05 사용자 확정) ── +# 프론트는 이 값을 /api 설정 응답 또는 빌드타임 복사로 받아 쓴다. 수정은 여기서만 한다. +STRUCTURE_TYPES = ("배관", "기성막이", "대피로", "기타") +STRUCTURE_DEFAULT_TYPE = "배관" +PIPE_DIAMETERS_MM = { + "이중벽관": (150, 200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500), + "삼중벽관": (200, 250, 300, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1500), + "파형강관": ( + 150, + 200, + 250, + 300, + 350, + 400, + 450, + 500, + 600, + 700, + 800, + 900, + 1000, + 1200, + 1350, + 1500, + 1650, + 1800, + 2000, + ), +} +PIPE_DEFAULT_TYPE = "파형강관" +# 자동 지정 기본 관경. 별표2 (나) 예외 하한 800mm(사용자 결정 — 계산값이 더 크면 바로 위 규격). +PIPE_DEFAULT_DIAMETER_MM = int(os.getenv("PIPE_DEFAULT_DIAMETER_MM", "800")) +ESCAPE_ROUTE_WIDTHS_M = (1.5, 2.0, 2.5, 3.0) +ESCAPE_ROUTE_DEFAULT_WIDTH_M = 2.0 +STRUCTURE_ETC_DEFAULT_NAME = "기타 구조물" + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4. 종횡단 생성 파라미터 (B06 WF3) +# ───────────────────────────────────────────────────────────────────────── +SECTION_STATION_INTERVAL_M = float(os.getenv("SECTION_STATION_INTERVAL_M", "20.0")) +# 초기 파이프라인(파일입력→B06) 샘플 반폭 — 표시 반폭보다 넉넉히 뽑아 두면 사용자가 +# 표시 반폭을 이 안에서 바꿀 때 재계산이 필요 없다(2026-08-06 사용자 확정, 기준 20m). +# 표시 반폭이 이 값을 넘을 때만 B05부터 재생성한다. 향후 사용자 평균 설정 보고 조정. +SECTION_CROSS_HALF_WIDTH_M = float(os.getenv("SECTION_CROSS_HALF_WIDTH_M", "20.0")) +SECTION_CROSS_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_CROSS_SAMPLE_INTERVAL_M", "0.5")) +SECTION_LONG_SAMPLE_INTERVAL_M = float(os.getenv("SECTION_LONG_SAMPLE_INTERVAL_M", "1.0")) +SECTION_VERTICAL_EXAGGERATION = float(os.getenv("SECTION_VERTICAL_EXAGGERATION", "1.0")) +SECTION_INCLUDE_ENDPOINT = os.getenv("SECTION_INCLUDE_ENDPOINT", "True").lower() == "true" +FOREST_ROAD_MIN_WIDTH_M = {"trunk": 3.0, "branch": 3.0, "work": 2.5} + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4-1. 표준 횡단면 단일 진실 원천 (도면 판독값, 2026-07-24 사용자 확정) +# +# 출처: `08 표준횡단도면(울진 울진 대흥 산65 외2(3공구)).bmp` 좌측 영역 판독값. +# 지반그룹 3종: soil(토사) / rock(암, 리핑·발파 공유) / paved(포장). +# 단위: 길이 m, 경사비 수평:수직=ratio:1, 횡단경사 %. +# +# 이 상수가 **표준 횡단면 기하의 유일한 정의처**다. B06 설정 패널 기본값과 +# 절·성토 단면적 엔진(B06_Section_Engine_Design)이 모두 여기를 읽으며, +# 아래 5-4-2의 파생 상수 외에 같은 값을 별도로 정의하지 않는다. +# ───────────────────────────────────────────────────────────────────────── +STANDARD_CROSS_SECTION = { + "soil": { + "road_width_m": 3.0, # 노폭(차도) + "shoulder_left_m": 0.5, # 노견(좌) + "shoulder_right_m": 0.5, # 노견(우) + # 측구(상단폭/저폭/깊이). 토사 = 900/300/300mm. + "ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3}, + "cross_slope_pct": {"min": 3.0, "max": 5.0}, # 횡단경사(측구방향) + "fill_slope_ratio": 1.2, # 성토 1:1.2 + "cut_slope_ratio": 1.0, # 절토 1:1.0 + }, + "rock": { # 리핑암·발파암 공유 + "road_width_m": 3.0, + "shoulder_left_m": 0.5, + "shoulder_right_m": 0.5, + # 일반 측구 = 690/300/300mm. + "ditch": {"top_width_m": 0.69, "bottom_width_m": 0.3, "depth_m": 0.3}, + # L형 측구 = 폭500 × 깊이100mm (횡단면도에서 일반/L형 중 선택). + "ditch_l_type": {"width_m": 0.5, "depth_m": 0.1}, + "cross_slope_pct": {"min": 3.0, "max": 3.0}, + "fill_slope_ratio": 1.2, + "cut_slope_ratio": 0.4, # 절토(암) 1:0.4 (규정 1:0.3 이상) + }, + "paved": { # 포장: 토사와 동일 기하 + 횡단경사만 다름 + "road_width_m": 3.0, + "shoulder_left_m": 0.5, + "shoulder_right_m": 0.5, + "ditch": {"top_width_m": 0.9, "bottom_width_m": 0.3, "depth_m": 0.3}, + "cross_slope_pct": {"min": 1.5, "max": 2.0}, + "fill_slope_ratio": 1.2, + "cut_slope_ratio": 1.0, + # 포장층 두께(도면 미표기 — 임도 콘크리트 포장 실무 표준 0.2m, 패널에서 편집 가능). + "pavement_thickness_m": 0.2, + }, +} +# 암 경계선(지면선 복사 — 계획선 아님) 기본 오프셋과 상/하 제어 스텝(m). +STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M = -0.5 +STANDARD_ROCK_BOUNDARY_STEP_M = 0.1 + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4-2. 횡단 표준단면 엔진 파생 상수 (B06 WF3) +# +# 전부 위 5-4-1 STANDARD_CROSS_SECTION에서 파생한다(중복 정의 금지). +# 지반유형 라벨은 3종(토사/리핑암/발파암)이나, 표준단면 기하는 토사/암반 2종 +# 프리셋만 존재한다(리핑암·발파암은 절토경사·측구 동일, 단가만 B08/B09에서 구분). +# ───────────────────────────────────────────────────────────────────────── +_SCS_SOIL = STANDARD_CROSS_SECTION["soil"] +# 노면폭(노견 포함) = 0.5 + 3.0 + 0.5 = 4.0m. env로만 개별 재정의 가능. +SECTION_ROADBED_WIDTH_M = float( + os.getenv( + "SECTION_ROADBED_WIDTH_M", + str( + _SCS_SOIL["road_width_m"] + _SCS_SOIL["shoulder_left_m"] + _SCS_SOIL["shoulder_right_m"] + ), + ) +) +SECTION_CARRIAGEWAY_WIDTH_M = float( + os.getenv("SECTION_CARRIAGEWAY_WIDTH_M", str(_SCS_SOIL["road_width_m"])) +) +# 성토 경사(수평:수직 = ratio:1). 지반유형 무관 고정. +SECTION_FILL_SLOPE_RATIO = float( + os.getenv("SECTION_FILL_SLOPE_RATIO", str(_SCS_SOIL["fill_slope_ratio"])) +) +# 지반유형(저장 라벨) → 기하 프리셋 키. 견적 단가 구분은 라벨 자체로 유지한다. +SECTION_GROUND_TYPE_PRESET = { + "soil": "soil", + "ripping_rock": "rock", + "blasting_rock": "rock", +} +# 단면유형: 좌절/우절(편절편성), 양절, 양성. 좌=양(+)offset, 우=음(-)offset. +SECTION_MODES = ("left_cut", "right_cut", "both_cut", "both_fill") +SECTION_DITCH_SIDES = ("left", "right") +# 측구 형식: 일반(사다리꼴) / L형(암 구간 전용 선택지). +SECTION_DITCH_TYPES = ("standard", "l_type") + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4-3. 토량환산계수 (B06 유토곡선) +# +# loose(L) = 흐트러진 상태 토량 / 자연상태 토량 (팽창률) +# compacted(C) = 다져진 상태 토량 / 자연상태 토량 (다짐률) +# +# 유토곡선은 운반계획 도면이므로 **다짐상태 기준**으로 계산한다. +# "운반거리의 산정 시에 모든 수량은 다짐상태로 환산하여 계산하고, +# 내역서에 적용하는 수량은 자연상태로 한다." +# (2021년도 국도건설공사 설계실무 요령 / 2016 건설공사 표준품셈 계열) +# +# 아래 값은 표준품셈 토량환산계수표의 암종별 범위를 B06 지반유형 3종에 대응시킨 +# 제안값이며 임도 전용 고시 수치가 아니다. +# 토사 ← 풍화토(L 1.10~1.25 / C 0.80~0.90) ~ 점토(L 1.20~1.35 / C 0.75~0.90) +# 리핑암 ← 풍화암(L 1.30~1.35 / C 1.00~1.15) ~ 연암(L 1.30~1.50 / C 1.00~1.30) 하단 +# 발파암 ← 보통암(L 1.55~1.70 / C 1.20~1.40) 중앙 +# 표준품셈도 "토질 시험하여 적용하는 것을 원칙으로 하되 소량인 경우 환산계수표에 +# 따를 수 있다"고 하므로 현장별 조정이 전제다. 값을 바꾸려면 여기만 고치면 된다. +# +# 이 상수가 **토량환산계수의 유일한 정의처**다(타 파일 중복 정의 금지). +# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md 8절 +# ───────────────────────────────────────────────────────────────────────── +EARTHWORK_CONVERSION_FACTORS = { + "soil": {"loose": 1.25, "compacted": 0.90}, + "ripping_rock": {"loose": 1.35, "compacted": 1.15}, + "blasting_rock": {"loose": 1.60, "compacted": 1.30}, +} + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4-5. 토공 운반장비 선정 거리 경계 (B06 유토곡선 운반계획) +# +# 평균운반거리(m)로 운반장비를 고른다. 위에서부터 순서대로 검사해 `max_distance_m` +# **이하**면 그 장비를 쓰고, 마지막 항목(None)이 나머지를 전부 받는다. +# +# 종무대(무대운반) ≤ 20m < 도쟈(불도저 압토) ≤ 70m < 덤프트럭 +# +# 2026-08-02 사용자 확정값(도쟈 50 → 70m). 유토곡선의 장비 경계현은 이 값을 +# **수평 현의 길이**로 읽는다 — 현 길이가 20m가 되는 높이 위쪽이 종무대 몫, +# 70m가 되는 높이까지가 도쟈 몫, 그 아래가 덤프 몫이다. +# 현장·발주처 기준에 따라 달라질 수 있으므로 +# **이 상수가 거리 경계의 유일한 정의처**이며 여기만 고치면 된다. +# +# key는 도면 표기와 다음과 같이 대응한다: free_haul=종무대, dozer=도쟈, dump_truck=덤프. +# 화면 표기 문구는 하드코딩 금지 규칙에 따라 ui_locales에서 따로 관리한다. +# 분석 근거: docs/raw/2026-08-02_유토곡선_3공구_분석.md +# ───────────────────────────────────────────────────────────────────────── +EARTHWORK_HAUL_EQUIPMENT_LIMITS_M = ( + ("free_haul", 20.0), + ("dozer", 70.0), + ("dump_truck", None), +) + + +# ───────────────────────────────────────────────────────────────────────── +# 5-4-2. 자연방토 판정 경사 (B06 유토곡선) +# +# 자연방토 = 남는 흙을 성토사면 아래로 흘려보내 **운반비를 세지 않는** 처리. +# 판정 기준은 **성토측 자연 지반의 경사**다(2026-08-02 사용자 확정). 지반이 이보다 가파르면 +# 부어 놓은 흙이 쌓이지 않고 스스로 흘러내리므로 따로 운반하지 않는다. +# +# 값은 rise/run(무차원). 1/1.5 = 0.667 ≈ 33.7°로, 토사 안식각 상한이자 표준 성토 경사 1:1.5와 +# 같은 자리다. 현장·발주처 기준에 따라 달라질 수 있으므로 **이 상수가 유일한 정의처**다. +# ───────────────────────────────────────────────────────────────────────── +NATURAL_SPOIL_MIN_GROUND_SLOPE = 1.0 / 1.5 + + +# ───────────────────────────────────────────────────────────────────────── +# 5-5. 종단 계획선(계획고) 설계 기준 (B05 WF2) +# +# 출처: 「임도설치 및 관리 등에 관한 규정」[별표 1-2] 임도의 설계 및 시설기준. +# 기준은 임도등급이 아니라 `설계속도 × 지형구분(일반/특수)`으로 규정되어 있어 +# 등급은 grade_to_design_speed로 간접 매핑한다. +# 위 5-3의 경로탐색(평면) 기준(FOREST_ROAD_MAX_GRADE 등)과는 별개의 값이므로 +# 서로 혼용하지 않는다. +# ───────────────────────────────────────────────────────────────────────── +FOREST_ROAD_PROFILE_CRITERIA = { + # 설계속도(km/h)별 법정 기준 + "design_speed": { + 40: { + "max_grade_pct": {"normal": 7.0, "special": 10.0}, + "max_reverse_grade_pct": 5.0, + "min_vertical_radius_m": 450.0, + "min_curve_length_m": 40.0, + }, + 30: { + "max_grade_pct": {"normal": 8.0, "special": 12.0}, + "max_reverse_grade_pct": 5.0, + "min_vertical_radius_m": 250.0, + "min_curve_length_m": 30.0, + }, + 20: { + "max_grade_pct": {"normal": 9.0, "special": 14.0}, + "max_reverse_grade_pct": 5.0, + "min_vertical_radius_m": 100.0, + "min_curve_length_m": 20.0, + }, + }, + # 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이 + # 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서 + # 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른 + # 설계속도가 오면 그 값이 우선한다(design_speed_kph). + # trunk = 간선임도 / fire = 산불진화임도 / work = 작업임도 + # branch(지선)는 현행 규칙에서 폐지 — 기존 데이터 호환용으로만 남긴다. + "grade_to_design_speed": {"trunk": 20, "fire": 20, "work": 20, "branch": 20}, + # 화면에서 고를 수 있는 설계속도 — 작업임도는 20만 허용된다(별표2 Ⅰ.3). + "selectable_design_speeds": {"trunk": (20, 30, 40), "fire": (20, 30, 40), "work": (20,)}, + # 특수지형에서 기준 적용이 어려운 경우 노면포장 시에 한하여 허용되는 상한 + "paved_exception_grade_pct": 18.0, + # 비포장 도로이면서 종단기울기 대수차가 이 값 이하이면 종단곡선을 두지 않는다 + "vertical_curve_skip_delta_pct": 5.0, + # 곡선 중첩 방지용 최소 직선(tangent) 길이 (규정 외 실무 기본값) + "min_tangent_length_m": 20.0, + # 절·성토 균형 구역 기본 길이 (None이면 노선 전체를 1구역으로 본다) + "balance_segment_length_m": None, + # 주 진행방향 자동 판정 기준: |시종점 고도차| / (최고−최저) 가 이 값 이상이면 + # 한 방향으로 오르내리는 노선으로 보고 반대 방향에 역기울기 상한을 적용한다. + # V자(계곡 횡단)·Λ자(능선 통과) 노선은 이 비율이 낮아 역기울기 판정을 하지 않는다. + "main_direction_monotone_ratio": 0.5, +} +GRADE_TERRAIN_TYPES = ("normal", "special") +# 주 진행방향: auto(자동 판정) / ascending(상행) / descending(하행) / none(역기울기 미적용) +GRADE_MAIN_DIRECTIONS = ("auto", "ascending", "descending", "none") + + +# ───────────────────────────────────────────────────────────────────────── +# 5-6. 종단 계획선 선형(직선 + 측점 위 종단곡선) 및 편집 정책 (B05 WF2) +# +# 위 5-5의 FOREST_ROAD_PROFILE_CRITERIA는 법정 기준값이고, 여기는 계획선을 +# "지반 추종 직선 분할 + 측점 위 종단곡선"으로 만들고 사용자가 0.1m 단위로 +# 편집하는 동작을 제어하는 실무 정책값이다. 서로 혼용하지 않는다. +# +# 변화점(PVI)은 반드시 기준 측점 위에만 놓이며, 종단곡선은 그 측점을 중심으로 +# 대칭 배치되어 곡선 좌우에 직선 구간이 반드시 남는다. +# ───────────────────────────────────────────────────────────────────────── +FOREST_ROAD_PROFILE_ALIGNMENT = { + # 절·성토 균형 허용 오차: |절토 − 성토| / max(절토, 성토) (%) + # 5% 미만이면 지반 추종이 왜곡되어 불필요한 변화점이 늘고, 15%를 넘으면 + # 사토·객토 운반 물량 부담이 커진다. 10%를 실무 균형점으로 둔다. + "balance_tolerance_percent": 10.0, + # 종단곡선 기본 **반경** R = 측점간격 × 이 비율 (변화점 대칭 배치). + # R을 1차 값으로 두어야 계획고를 편집해도 R이 흔들리지 않는다(곡선길이 L이 대신 변한다). + # L = R × |기울기 대수차 A(비율)| + # 주의: 측점간격 20m 기준 R=8m이면 A=5%일 때 L=0.4m, A=15%일 때 L=1.2m로 + # 도면상 곡선이 거의 드러나지 않는다. 곡선을 뚜렷하게 보려면 이 비율을 크게 올린다 + # (예: 20.0 → R=400m, A=10%일 때 L=40m). + "curve_radius_ratio": 0.40, + # **기본 종단곡선 길이 L(m) — 모든 변화점의 1차 기준값.** + # 임도 계획선은 변화점 사이 대수차 A가 작아(1~3%가 흔하다) 반경 R을 기준으로 잡으면 + # L = R × A 가 변화점마다 3~50m로 널뛰고, 작은 쪽은 도면에서 직선과 구분되지 않는다 + # (2026-08-03 사용자 지적). 그래서 **L을 기준값으로 고정**하고 R = L / A 로 역산한다. + # 결과적으로 호 길이가 변화점마다 같아 도면이 고르게 읽힌다. + # 인접 직선이 짧아 `curve_tangent_max_ratio`에 걸리면 넣을 수 있는 **최대 L**까지만 + # 줄이고 경고를 남긴다(0으로 죽이지 않는다 — 2026-08-08 사용자 지시). + # 사용자가 그 변화점 R을 직접 지정하면 그쪽이 이긴다. 호를 더/덜 길게 보이려면 여기만 고친다. + "default_curve_length_m": 15.0, + # 인접 직선 길이 대비 곡선 반쪽이 점유할 수 있는 최대 비율. + # 0.45면 짧은 쪽 직선의 55%가 항상 직선으로 남아 좌우 곡선이 겹치지 않는다. + "curve_tangent_max_ratio": 0.45, + # 법정 예외(비포장 & 대수차 5% 이하)를 실제 기하에서도 곡선 생략으로 적용할지. + # 규정 다-(3)-(다)는 "두지 않을 수 있다"는 허용 조항이며, 임도 실무 도면은 대수차가 + # 작아도 변화점을 원곡선으로 처리한다. 기본값 False = 곡선을 항상 삽입하고 + # 해당 구간에는 "생략 가능" 표시만 남긴다(True로 바꾸면 곡선을 실제로 뺀다). + "curve_skip_legal_exception": False, + # 변화점(PVI) 추가 페널티 (직선 분할 DP 목적함수, 단위 m²). + # 변화점 하나를 늘리려면 잔차제곱합이 이 값 이상 개선되어야 채택된다. + "pvi_penalty_m2": 25.0, + # 직선 분할 시 한 구간이 가질 수 있는 최소 측점 개수 (짧은 토막 방지) + "min_segment_stations": 2, + # 사용자 편집 스텝(m). 그래프 상·하단 버튼 1클릭당 계획고 이동량. + "edit_step_m": 0.1, + # 법정 종단기울기 상한 초과 시 동작: "warn"(경고만) | "block"(편집 차단) + "grade_violation_policy": "warn", +} diff --git a/config/config_system_terrain.py b/config/config_system_terrain.py new file mode 100644 index 00000000..a44511b1 --- /dev/null +++ b/config/config_system_terrain.py @@ -0,0 +1,186 @@ +"""지형 분석·지표면 모델 생성 파라미터 (5장·5-2장). + +`config_system.py` 가 700줄을 넘어 떼어냈다(2026-09-04). 값·이름은 그대로이고, +`config_system` 이 다시 내보내므로 기존 `from config.config_system import …` 는 불변이다. +""" + +import os + +# 5. 지형 분석 알고리즘 파라미터 +# ───────────────────────────────────────────────────────────────────────── +# Trimesh 메쉬 생성 +MESH_GRID_SIZE = float(os.getenv("MESH_GRID_SIZE", "1.0")) # 미터 단위 +MESH_SMOOTHING_ITERATIONS = int(os.getenv("MESH_SMOOTHING_ITERATIONS", "0")) +SURFACE_LAS_CHUNK_SIZE = int(os.getenv("SURFACE_LAS_CHUNK_SIZE", "500000")) +SURFACE_DEFAULT_RGB_VALUE = int(os.getenv("SURFACE_DEFAULT_RGB_VALUE", "128")) +SURFACE_GRID_CELL_SIZE_M = float(os.getenv("SURFACE_GRID_CELL_SIZE_M", "2.0")) +SURFACE_GRID_HEIGHT_THRESHOLD_M = float(os.getenv("SURFACE_GRID_HEIGHT_THRESHOLD_M", "1.5")) + +# CSF (Cloth Simulation Filter) 지면 분류 파라미터 +SURFACE_CSF_CLOTH_RESOLUTION_M = float(os.getenv("SURFACE_CSF_CLOTH_RESOLUTION_M", "1.5")) +SURFACE_CSF_RIGIDNESS = int(os.getenv("SURFACE_CSF_RIGIDNESS", "1")) +SURFACE_CSF_TIME_STEP = float(os.getenv("SURFACE_CSF_TIME_STEP", "0.65")) +SURFACE_CSF_CLASS_THRESHOLD_M = float(os.getenv("SURFACE_CSF_CLASS_THRESHOLD_M", "0.5")) +# 낙하 후 스프링 정착에 쓰는 여유 반복 수 (실제 낙하 구간은 현장 기복에서 계산한다) +SURFACE_CSF_ITERATIONS = int(os.getenv("SURFACE_CSF_ITERATIONS", "150")) +# 낙하+정착 반복 상한 — 기복 1500m(0.3185m/회)까지 커버한다 +SURFACE_CSF_MAX_ITERATIONS = int(os.getenv("SURFACE_CSF_MAX_ITERATIONS", "5000")) +SURFACE_CSF_SLOPE_SMOOTH = os.getenv("SURFACE_CSF_SLOPE_SMOOTH", "True").lower() == "true" +SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M = float( + os.getenv("SURFACE_CSF_SLOPE_SMOOTH_THRESHOLD_M", "1.8") +) + +# PMF (Progressive Morphological Filter) 지면 분류 파라미터 +SURFACE_PMF_CELL_SIZE_M = float(os.getenv("SURFACE_PMF_CELL_SIZE_M", "2.0")) +SURFACE_PMF_MAX_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_MAX_WINDOW_SIZE", "40")) +SURFACE_PMF_INITIAL_WINDOW_SIZE = int(os.getenv("SURFACE_PMF_INITIAL_WINDOW_SIZE", "3")) +SURFACE_PMF_SLOPE = float(os.getenv("SURFACE_PMF_SLOPE", "1.0")) +SURFACE_PMF_MAX_DISTANCE_M = float(os.getenv("SURFACE_PMF_MAX_DISTANCE_M", "2.5")) + +# RANSAC (Local plane fitting) 지면 분류 파라미터 +SURFACE_RANSAC_DISTANCE_THRESHOLD_M = float(os.getenv("SURFACE_RANSAC_DISTANCE_THRESHOLD_M", "0.3")) +SURFACE_RANSAC_N = int(os.getenv("SURFACE_RANSAC_N", "3")) +SURFACE_RANSAC_ITERATIONS = int(os.getenv("SURFACE_RANSAC_ITERATIONS", "100")) +SURFACE_RANSAC_LOCAL_GRID_SIZE_M = float(os.getenv("SURFACE_RANSAC_LOCAL_GRID_SIZE_M", "10.0")) +SURFACE_RANSAC_SEED = int(os.getenv("SURFACE_RANSAC_SEED", "42")) + +# LAS 자체 지면분류(class 2) 필터 — 이 비율 이상 분류돼 있을 때만 필터로 제공한다. +# 미분류 LAS는 classification이 전부 0이라 마스크가 비어 버린다. +SURFACE_CLASSIFIED_GROUND_MIN_RATIO = float( + os.getenv("SURFACE_CLASSIFIED_GROUND_MIN_RATIO", "0.002") +) +# 지면점 비율이 이 값 미만이면 필터가 사실상 실패한 것으로 보고 WARNING을 남긴다. +SURFACE_GROUND_RATIO_WARN = float(os.getenv("SURFACE_GROUND_RATIO_WARN", "0.01")) + +# 계획노선이 지표면 밖으로 나가 잘릴 때, 잘린 쪽 끝에서 더 깎을 길이(m). +# 서피스 가장자리는 점 밀도가 떨어져 외곽선이 불규칙하다 — 경계에 딱 붙여 자르면 +# 그 구간 지반고가 못 미덥다 (2026-09-01 사용자 확정). +SURFACE_ROUTE_EDGE_TRIM_M = float(os.getenv("SURFACE_ROUTE_EDGE_TRIM_M", "30.0")) + +# ───────────────────────────────────────────────────────────────────────── +# 5-2. 지표면 모델 생성 파라미터 (TIN/DTM/NURBS/implicit/meshfree) +# ───────────────────────────────────────────────────────────────────────── +# 고를 수 있는 지면 필터 전체 — 관리자가 B04 드롭다운에서 요청할 때 쓰인다. +# 자동 전처리가 미리 만드는 조합이 아니다(그건 SURFACE_AUTO_* 가 정한다). +SURFACE_MODEL_SOURCE_FILTERS = tuple( + os.getenv("SURFACE_MODEL_SOURCE_FILTERS", "classification,grid_min_z,csf,pmf").split(",") +) +# 자동 전처리가 만드는 조합 — 기본 필터 1종 × 아래 표현. 나머지는 관리자 요청 시 만든다. +SURFACE_AUTO_METHODS = tuple(os.getenv("SURFACE_AUTO_METHODS", "dtm").split(",")) +# 기본 필터는 입력 LAS를 보고 정한다 — 지면분류(class 2)가 있으면 그것을, 없으면 아래 값. +SURFACE_AUTO_FALLBACK_FILTER = os.getenv("SURFACE_AUTO_FALLBACK_FILTER", "csf") +# dtm을 먼저 빌드해야 TIN 등고선 사전 캐시가 dtm footprint를 참조할 수 있다 (PLAN D-6) +SURFACE_MODEL_PRECOMPUTE = tuple( + os.getenv("SURFACE_MODEL_PRECOMPUTE", "dtm,tin,nurbs,implicit,meshfree").split(",") +) +SURFACE_MODEL_SMOOTHING_METHODS = tuple( + os.getenv("SURFACE_MODEL_SMOOTHING_METHODS", "dtm,tin").split(",") +) +SURFACE_MODEL_SYNC_TIMEOUT_SECONDS = int(os.getenv("SURFACE_MODEL_SYNC_TIMEOUT_SECONDS", "0")) + +# footprint(외곽) 산출 +SURFACE_FOOTPRINT_RESOLUTION_M = float(os.getenv("SURFACE_FOOTPRINT_RESOLUTION_M", "1.0")) +SURFACE_FOOTPRINT_GAP_CLOSE_M = float(os.getenv("SURFACE_FOOTPRINT_GAP_CLOSE_M", "1.0")) +SURFACE_BOUNDARY_INSET_M = float(os.getenv("SURFACE_BOUNDARY_INSET_M", "1.0")) +SURFACE_KEEP_LARGEST_FOOTPRINT = ( + os.getenv("SURFACE_KEEP_LARGEST_FOOTPRINT", "True").lower() == "true" +) +SURFACE_TILE_SIZE_M = float(os.getenv("SURFACE_TILE_SIZE_M", "50.0")) +SURFACE_MAX_PREVIEW_VERTICES = int(os.getenv("SURFACE_MAX_PREVIEW_VERTICES", "500000")) + +# 표현별 파라미터 (old 버전 검증값 기준 — PLAN F) +SURFACE_TIN_MAX_INPUT_POINTS = int(os.getenv("SURFACE_TIN_MAX_INPUT_POINTS", "500000")) +SURFACE_DTM_GRID_RESOLUTION_M = float(os.getenv("SURFACE_DTM_GRID_RESOLUTION_M", "1.0")) +SURFACE_NURBS_DEGREE = int(os.getenv("SURFACE_NURBS_DEGREE", "3")) +SURFACE_NURBS_PATCH_SIZE_M = float(os.getenv("SURFACE_NURBS_PATCH_SIZE_M", "50.0")) +SURFACE_NURBS_CONTROL_POINTS_PER_AXIS = int( + os.getenv("SURFACE_NURBS_CONTROL_POINTS_PER_AXIS", "16") +) +SURFACE_IMPLICIT_MAX_POINTS_PER_TILE = int( + os.getenv("SURFACE_IMPLICIT_MAX_POINTS_PER_TILE", "10000") +) +SURFACE_IMPLICIT_SMOOTHING = float(os.getenv("SURFACE_IMPLICIT_SMOOTHING", "0.1")) +SURFACE_MESHFREE_MAX_MODEL_POINTS = int(os.getenv("SURFACE_MESHFREE_MAX_MODEL_POINTS", "500000")) +SURFACE_MESHFREE_POINT_RADIUS_M = float(os.getenv("SURFACE_MESHFREE_POINT_RADIUS_M", "0.15")) + +# 스무딩 파라미터 +SURFACE_SMOOTHING_DTM_SIGMA_M = float(os.getenv("SURFACE_SMOOTHING_DTM_SIGMA_M", "0.5")) +SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH = float(os.getenv("SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH", "0.0")) +SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M = float( + os.getenv("SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M", "0.5") +) +SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS = int( + os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS", "10") +) +SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA", "0.5")) +SURFACE_SMOOTHING_TIN_TAUBIN_MU = float(os.getenv("SURFACE_SMOOTHING_TIN_TAUBIN_MU", "-0.53")) + +# 등고선 파라미터 +SURFACE_CONTOUR_INTERVAL_M = float(os.getenv("SURFACE_CONTOUR_INTERVAL_M", "1.0")) +SURFACE_CONTOUR_GRID_RESOLUTION_M = float(os.getenv("SURFACE_CONTOUR_GRID_RESOLUTION_M", "1.0")) + +# 도엽등고선 3D 서피스 (LAS 없는 설계 — 2026-08-30 사용자 확정) +# 노선 XY bbox에 더하는 절취 여유(m). 300m = 횡단·코리도·성토면 여유(사용자 확정값). +SHEET_SURFACE_MARGIN_M = float(os.getenv("SHEET_SURFACE_MARGIN_M", "300.0")) +# 도엽등고선 DTM 격자 한 변(m). LAS DTM·등고선 캐시와 같은 1m(사용자 확정값). +SHEET_SURFACE_GRID_M = float(os.getenv("SHEET_SURFACE_GRID_M", "1.0")) +# 만들어 둘 등고선 보간 방식 — B04 화면에서 버튼으로 바꿔 가며 비교한다 +# (2026-08-30 사용자 지시). 정의는 B04_PreProcess_Engine_SheetMethods.py. +SHEET_SURFACE_METHODS = [ + method.strip() + for method in os.getenv( + "SHEET_SURFACE_METHODS", + "tin_sheet,tin,biharmonic,anudem,multires,laplace", + ).split(",") + if method.strip() +] +# 확정에 쓸 기본 방식 — 라플라스 (2026-08-30 사용자 확정). +SHEET_SURFACE_DEFAULT_METHOD = os.getenv("SHEET_SURFACE_DEFAULT_METHOD", "laplace") +# 자동 전처리가 만드는 도엽 방식 — 기본 하나뿐이다. 여섯 방식을 매번 다 만들면 +# WF1의 대부분(용화 실측 891초 중 655초)을 여기서 쓴다. 나머지는 관리자가 B04에서 +# 그 방식을 고를 때 만든다 (2026-09-01 사용자 확정). +SHEET_SURFACE_AUTO_METHODS = [ + method.strip() + for method in os.getenv("SHEET_SURFACE_AUTO_METHODS", SHEET_SURFACE_DEFAULT_METHOD).split(",") + if method.strip() +] + +# 일반 사용자 WF1 자동 확정 기본값 +SURFACE_CONFIRM_DEFAULT_FILTER = os.getenv("SURFACE_CONFIRM_DEFAULT_FILTER", "csf") +SURFACE_CONFIRM_DEFAULT_METHOD = os.getenv("SURFACE_CONFIRM_DEFAULT_METHOD", "dtm") +SURFACE_CONFIRM_DEFAULT_SMOOTH = ( + os.getenv("SURFACE_CONFIRM_DEFAULT_SMOOTH", "True").lower() == "true" +) + + +def build_surface_model_config() -> dict: + """지표면 모델 파이프라인이 사용하는 config dict를 조립한다.""" + return { + "source_filters": list(SURFACE_MODEL_SOURCE_FILTERS), + "precompute": list(SURFACE_MODEL_PRECOMPUTE), + "smoothing_methods": tuple(SURFACE_MODEL_SMOOTHING_METHODS), + "sync_timeout_seconds": SURFACE_MODEL_SYNC_TIMEOUT_SECONDS, + "footprint_resolution_meters": SURFACE_FOOTPRINT_RESOLUTION_M, + "footprint_gap_close_meters": SURFACE_FOOTPRINT_GAP_CLOSE_M, + "boundary_inset_meters": SURFACE_BOUNDARY_INSET_M, + "keep_largest_footprint": SURFACE_KEEP_LARGEST_FOOTPRINT, + "tile_size_meters": SURFACE_TILE_SIZE_M, + "max_preview_vertices": SURFACE_MAX_PREVIEW_VERTICES, + "tin_max_input_points": SURFACE_TIN_MAX_INPUT_POINTS, + "dtm_grid_resolution_meters": SURFACE_DTM_GRID_RESOLUTION_M, + "nurbs_degree": SURFACE_NURBS_DEGREE, + "nurbs_patch_size_meters": SURFACE_NURBS_PATCH_SIZE_M, + "nurbs_control_points_per_axis": SURFACE_NURBS_CONTROL_POINTS_PER_AXIS, + "implicit_max_points_per_tile": SURFACE_IMPLICIT_MAX_POINTS_PER_TILE, + "implicit_smoothing": SURFACE_IMPLICIT_SMOOTHING, + "meshfree_max_model_points": SURFACE_MESHFREE_MAX_MODEL_POINTS, + "meshfree_point_radius_meters": SURFACE_MESHFREE_POINT_RADIUS_M, + "smoothing_dtm_sigma_meters": SURFACE_SMOOTHING_DTM_SIGMA_M, + "smoothing_dtm_spline_smooth": SURFACE_SMOOTHING_DTM_SPLINE_SMOOTH, + "smoothing_dtm_preview_resolution_meters": SURFACE_SMOOTHING_DTM_PREVIEW_RESOLUTION_M, + "smoothing_tin_taubin_iterations": SURFACE_SMOOTHING_TIN_TAUBIN_ITERATIONS, + "smoothing_tin_taubin_lambda": SURFACE_SMOOTHING_TIN_TAUBIN_LAMBDA, + "smoothing_tin_taubin_mu": SURFACE_SMOOTHING_TIN_TAUBIN_MU, + "contour_interval_meters": SURFACE_CONTOUR_INTERVAL_M, + "contour_grid_resolution_meters": SURFACE_CONTOUR_GRID_RESOLUTION_M, + }