refactor(backend)!: separate algorithm and data layers
Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories. Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage. BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
import fcntl
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
@@ -8,8 +11,12 @@ from openpyxl.styles import Alignment, Font, PatternFill
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from openpyxl.utils import get_column_letter
|
||||
from pyproj import Transformer
|
||||
import wntr
|
||||
|
||||
from app.algorithms.pressure_sensor_placement import kmeans_placement
|
||||
from app.algorithms.pressure_sensor_placement import sensitivity_placement
|
||||
from app.infra.db.postgresql import sensor_placement as sensor_placement_repository
|
||||
from app.native.wndb.inp.exporter import dump_inp
|
||||
|
||||
|
||||
class SensorPlacementNotFoundError(LookupError):
|
||||
@@ -24,6 +31,107 @@ class SensorPlacementConflictError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _sensor_inp_path(project_code: str) -> Path:
|
||||
if (
|
||||
not project_code
|
||||
or project_code in {".", ".."}
|
||||
or "/" in project_code
|
||||
or "\\" in project_code
|
||||
or "\x00" in project_code
|
||||
):
|
||||
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
|
||||
return Path("db_inp") / f"{project_code}.db.inp"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _sensor_inp_lock(project_code: str):
|
||||
inp_path = _sensor_inp_path(project_code)
|
||||
inp_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock_path = inp_path.with_suffix(".sensor.lock")
|
||||
with lock_path.open("w", encoding="utf-8") as lock_file:
|
||||
try:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError as exc:
|
||||
raise SensorPlacementConflictError(
|
||||
"当前项目已有监测点优化任务正在运行,请稍后重试"
|
||||
) from exc
|
||||
try:
|
||||
yield inp_path
|
||||
finally:
|
||||
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _create_validated_placement(
|
||||
project_code: str,
|
||||
*,
|
||||
run_name: str,
|
||||
min_diameter: int,
|
||||
created_by: str,
|
||||
sensor_locations: list[str],
|
||||
) -> dict[str, Any]:
|
||||
validate_sensor_placement_nodes(project_code, sensor_locations)
|
||||
return sensor_placement_repository.create_sensor_placement(
|
||||
project_code,
|
||||
run_name=run_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=created_by,
|
||||
sensor_locations=sensor_locations,
|
||||
)
|
||||
|
||||
|
||||
def optimize_sensor_placement_by_sensitivity(
|
||||
project_code: str,
|
||||
run_name: str,
|
||||
sensor_count: int,
|
||||
min_diameter: int,
|
||||
created_by: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Run sensitivity placement and persist the validated result."""
|
||||
|
||||
with _sensor_inp_lock(project_code):
|
||||
inp_path = _sensor_inp_path(project_code)
|
||||
dump_inp(project_code, str(inp_path), "2")
|
||||
network_model = wntr.network.WaterNetworkModel(str(inp_path))
|
||||
sensor_locations = sensitivity_placement.optimize_sensor_placement(
|
||||
network_model,
|
||||
sensor_num=sensor_count,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
return _create_validated_placement(
|
||||
project_code,
|
||||
run_name=run_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=created_by,
|
||||
sensor_locations=sensor_locations,
|
||||
)
|
||||
|
||||
|
||||
def optimize_sensor_placement_by_kmeans(
|
||||
project_code: str,
|
||||
run_name: str,
|
||||
sensor_count: int,
|
||||
min_diameter: int,
|
||||
created_by: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Export the model, run K-means placement, and persist the result."""
|
||||
|
||||
with _sensor_inp_lock(project_code) as inp_path:
|
||||
dump_inp(project_code, str(inp_path), "2")
|
||||
network_model = wntr.network.WaterNetworkModel(str(inp_path))
|
||||
sensor_locations = kmeans_placement.optimize_sensor_placement(
|
||||
network_model,
|
||||
sensor_count=sensor_count,
|
||||
min_diameter_mm=min_diameter,
|
||||
)
|
||||
return _create_validated_placement(
|
||||
project_code,
|
||||
run_name=run_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=created_by,
|
||||
sensor_locations=sensor_locations,
|
||||
)
|
||||
|
||||
|
||||
_to_wgs84 = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True)
|
||||
_STATUS_LABELS = {
|
||||
"current": "当前方案",
|
||||
|
||||
Reference in New Issue
Block a user