SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,114 @@
|
||||
"""설정파일(config.yaml) 스키마와 로더.
|
||||
|
||||
사내 관행값·발주처 기준이 필요한 TODO 항목은 §9 표를 참고. 전부 이 모듈의
|
||||
기본값으로 노출되어 있으며, config.yaml로 덮어쓴다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionsPerBlock:
|
||||
min: int = 500 # TODO: 편람에 수치 규정 없음. 사내 관행값 확정 필요
|
||||
max: int = 3000
|
||||
|
||||
|
||||
@dataclass
|
||||
class DemandPerBlock:
|
||||
max_cmd: Optional[float] = None # m3/day, null이면 미적용
|
||||
|
||||
|
||||
@dataclass
|
||||
class PartitionConfig:
|
||||
level: str = "small" # large | medium | small
|
||||
k_min: int = 4
|
||||
k_max: int = 20
|
||||
method: str = "spectral" # spectral | louvain
|
||||
flow_weight_alpha: float = 0.7
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstraintsConfig:
|
||||
connections_per_block: ConnectionsPerBlock = field(default_factory=ConnectionsPerBlock)
|
||||
max_pipe_length_per_block: float = 30000.0 # m, TODO
|
||||
demand_per_block: DemandPerBlock = field(default_factory=DemandPerBlock)
|
||||
elevation_band_m: float = 30.0
|
||||
large_consumer_demand_lps: float = 5.0 # TODO
|
||||
large_consumer_policy: str = "isolate" # isolate | keep_together
|
||||
sources_in_separate_blocks: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class HydraulicsConfig:
|
||||
duration_hours: int = 24
|
||||
timestep_minutes: int = 60
|
||||
min_pressure_m: float = 15.0 # TODO: 수도법 시행규칙 / 상수도설계기준 확인 후 확정
|
||||
max_pressure_m: float = 71.0 # TODO
|
||||
max_water_age_hours: float = 48.0 # TODO
|
||||
allow_isolated_nodes: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class RefineConfig:
|
||||
max_iterations: int = 20
|
||||
moves_per_iteration: int = 50
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoringWeights:
|
||||
n_valves: float = 0.25
|
||||
n_meters: float = 0.15
|
||||
pressure_violations: float = 0.30
|
||||
max_water_age: float = 0.15
|
||||
boundary_pipe_length: float = 0.15
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoringConfig:
|
||||
weights: ScoringWeights = field(default_factory=ScoringWeights)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoundaryConfig:
|
||||
# 블록당 유입지점(유량계) 목표 개수. 편람에 수치 없음, 통상 1개 예비 포함 2개. TODO
|
||||
inlets_per_block: int = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
crs: str = "EPSG:5186"
|
||||
partition: PartitionConfig = field(default_factory=PartitionConfig)
|
||||
constraints: ConstraintsConfig = field(default_factory=ConstraintsConfig)
|
||||
hydraulics: HydraulicsConfig = field(default_factory=HydraulicsConfig)
|
||||
refine: RefineConfig = field(default_factory=RefineConfig)
|
||||
scoring: ScoringConfig = field(default_factory=ScoringConfig)
|
||||
boundary: BoundaryConfig = field(default_factory=BoundaryConfig)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: str | Path) -> "Config":
|
||||
raw = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
|
||||
return cls.from_dict(raw)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: dict[str, Any]) -> "Config":
|
||||
cfg = cls()
|
||||
_apply(cfg, raw)
|
||||
return cfg
|
||||
|
||||
|
||||
def _apply(instance: Any, data: dict[str, Any]) -> None:
|
||||
for key, value in data.items():
|
||||
if not hasattr(instance, key):
|
||||
continue
|
||||
current = getattr(instance, key)
|
||||
if dataclasses.is_dataclass(current) and isinstance(value, dict):
|
||||
_apply(current, value)
|
||||
else:
|
||||
setattr(instance, key, value)
|
||||
Reference in new issue
Block a user