重构爆管定位相关功能,优化输入验证与API接口
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
"""爆管定位部署入口(基于外部 SCADA 实测数据)。"""
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable
|
from typing import Any, Iterable
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -69,23 +67,11 @@ def _align_scada_series(
|
|||||||
return aligned
|
return aligned
|
||||||
|
|
||||||
|
|
||||||
def run_burst_location(
|
def _validate_flow_inputs(
|
||||||
wn_inp_path,
|
flow_scada_ids: list[str] | None,
|
||||||
pressure_scada_ids,
|
burst_flow: pd.Series | None,
|
||||||
burst_pressure,
|
normal_flow: pd.Series | None,
|
||||||
normal_pressure,
|
) -> tuple[bool, list[str]]:
|
||||||
burst_leakage,
|
|
||||||
flow_scada_ids=None,
|
|
||||||
burst_flow=None,
|
|
||||||
normal_flow=None,
|
|
||||||
min_dpressure=2.0,
|
|
||||||
basic_pressure=10.0,
|
|
||||||
):
|
|
||||||
if pressure_scada_ids is None or len(pressure_scada_ids) == 0:
|
|
||||||
raise ValueError("pressure_scada_ids cannot be empty.")
|
|
||||||
if burst_pressure is None or normal_pressure is None:
|
|
||||||
raise ValueError("burst_pressure and normal_pressure are required.")
|
|
||||||
|
|
||||||
has_any_flow = any(
|
has_any_flow = any(
|
||||||
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
|
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
|
||||||
)
|
)
|
||||||
@@ -97,6 +83,46 @@ def run_burst_location(
|
|||||||
"flow_scada_ids, burst_flow, and normal_flow must be provided together."
|
"flow_scada_ids, burst_flow, and normal_flow must be provided together."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not has_all_flow:
|
||||||
|
return False, []
|
||||||
|
|
||||||
|
flow_ids = [str(item) for item in (flow_scada_ids or [])]
|
||||||
|
if len(flow_ids) == 0:
|
||||||
|
raise ValueError("flow_scada_ids cannot be empty when flow data is provided.")
|
||||||
|
return True, flow_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _build_top_candidates(similarity_series: pd.Series) -> list[dict[str, Any]]:
|
||||||
|
top_series = similarity_series.iloc[:10]
|
||||||
|
return [
|
||||||
|
{"pipe_id": str(pipe_id), "similarity": float(score)}
|
||||||
|
for pipe_id, score in top_series.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_burst_location(
|
||||||
|
wn_inp_path: str,
|
||||||
|
pressure_scada_ids: list[str],
|
||||||
|
burst_pressure: pd.Series,
|
||||||
|
normal_pressure: pd.Series,
|
||||||
|
burst_leakage: float,
|
||||||
|
flow_scada_ids: list[str] | None = None,
|
||||||
|
burst_flow: pd.Series | None = None,
|
||||||
|
normal_flow: pd.Series | None = None,
|
||||||
|
min_dpressure: float = 2.0,
|
||||||
|
basic_pressure: float = 10.0,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if pressure_scada_ids is None or len(pressure_scada_ids) == 0:
|
||||||
|
raise ValueError("pressure_scada_ids cannot be empty.")
|
||||||
|
if burst_pressure is None or normal_pressure is None:
|
||||||
|
raise ValueError("burst_pressure and normal_pressure are required.")
|
||||||
|
|
||||||
|
has_all_flow, flow_ids = _validate_flow_inputs(
|
||||||
|
flow_scada_ids=flow_scada_ids,
|
||||||
|
burst_flow=burst_flow,
|
||||||
|
normal_flow=normal_flow,
|
||||||
|
)
|
||||||
|
|
||||||
inp_path = Path(wn_inp_path)
|
inp_path = Path(wn_inp_path)
|
||||||
wn = load_inp(
|
wn = load_inp(
|
||||||
inp_name=inp_path.name,
|
inp_name=inp_path.name,
|
||||||
@@ -131,11 +157,6 @@ def run_burst_location(
|
|||||||
timestep_list = list(pressure_normal.index)
|
timestep_list = list(pressure_normal.index)
|
||||||
|
|
||||||
if has_all_flow:
|
if has_all_flow:
|
||||||
flow_ids = [str(item) for item in flow_scada_ids]
|
|
||||||
if len(flow_ids) == 0:
|
|
||||||
raise ValueError(
|
|
||||||
"flow_scada_ids cannot be empty when flow data is provided."
|
|
||||||
)
|
|
||||||
normal_flow_aligned = _align_scada_series(normal_flow, flow_ids, "normal_flow")
|
normal_flow_aligned = _align_scada_series(normal_flow, flow_ids, "normal_flow")
|
||||||
burst_flow_aligned = _align_scada_series(burst_flow, flow_ids, "burst_flow")
|
burst_flow_aligned = _align_scada_series(burst_flow, flow_ids, "burst_flow")
|
||||||
flow_normal = normal_flow_aligned.to_frame().T
|
flow_normal = normal_flow_aligned.to_frame().T
|
||||||
@@ -190,7 +211,7 @@ def run_burst_location(
|
|||||||
"burst_leakage": float(burst_leakage),
|
"burst_leakage": float(burst_leakage),
|
||||||
"elapsed_seconds": elapsed_seconds,
|
"elapsed_seconds": elapsed_seconds,
|
||||||
"simulation_times": int(simulation_times),
|
"simulation_times": int(simulation_times),
|
||||||
"top_candidates": list(similarity_series.index[:10]),
|
"top_candidates": _build_top_candidates(similarity_series),
|
||||||
"similarity_mode": similarity_mode,
|
"similarity_mode": similarity_mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,62 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||||
from app.services.burst_location import run_burst_location_by_network
|
from app.services.burst_location import (
|
||||||
|
get_burst_location_scheme_detail,
|
||||||
|
list_burst_location_schemes,
|
||||||
|
run_burst_location_by_network,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class BurstLocationRequest(BaseModel):
|
class BurstLocationRequest(BaseModel):
|
||||||
network: str
|
network: str
|
||||||
pressure_scada_ids: list[str]
|
pressure_scada_ids: list[str] | None = None
|
||||||
burst_pressure: dict[str, float] | list[dict[str, Any]]
|
burst_pressure: dict[str, float] | list[dict[str, Any]] | None = None
|
||||||
normal_pressure: dict[str, float] | list[dict[str, Any]]
|
normal_pressure: dict[str, float] | list[dict[str, Any]] | None = None
|
||||||
burst_leakage: float
|
burst_leakage: float
|
||||||
flow_scada_ids: list[str] | None = None
|
flow_scada_ids: list[str] | None = None
|
||||||
burst_flow: dict[str, float] | list[dict[str, Any]] | None = None
|
burst_flow: dict[str, float] | list[dict[str, Any]] | None = None
|
||||||
normal_flow: dict[str, float] | list[dict[str, Any]] | None = None
|
normal_flow: dict[str, float] | list[dict[str, Any]] | None = None
|
||||||
min_dpressure: float = 2.0
|
min_dpressure: float = 2.0
|
||||||
basic_pressure: float = 10.0
|
basic_pressure: float = 10.0
|
||||||
|
scada_burst_start: datetime | None = None
|
||||||
|
scada_burst_end: datetime | None = None
|
||||||
|
scada_normal_start: datetime | None = None
|
||||||
|
scada_normal_end: datetime | None = None
|
||||||
|
use_scada_flow: bool = False
|
||||||
|
scheme_name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.post("/locate/")
|
@router.post("/locate/")
|
||||||
async def locate_burst(
|
async def locate_burst(
|
||||||
data: BurstLocationRequest,
|
data: BurstLocationRequest,
|
||||||
_username: str = Depends(get_current_keycloak_username),
|
username: str = Depends(get_current_keycloak_username),
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
return run_burst_location_by_network(**data.model_dump())
|
return run_burst_location_by_network(**data.model_dump(), username=username)
|
||||||
except (TypeError, ValueError) as exc:
|
except (TypeError, ValueError) as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc))
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schemes/")
|
||||||
|
async def query_burst_schemes(
|
||||||
|
network: str, query_date: datetime | None = None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
return list_burst_location_schemes(network=network, query_date=query_date)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schemes/{scheme_name}")
|
||||||
|
async def query_burst_scheme_detail(network: str, scheme_name: str) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
return get_burst_location_scheme_detail(network=network, scheme_name=scheme_name)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc))
|
||||||
|
|||||||
+332
-22
@@ -1,15 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
import os
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from app.algorithms.burst_location import run_burst_location
|
from app.algorithms.burst_location import run_burst_location
|
||||||
from app.services.tjnetwork import dump_inp
|
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||||
|
from app.services.scheme_management import (
|
||||||
|
query_burst_location_scheme_detail,
|
||||||
|
query_burst_location_schemes,
|
||||||
|
scheme_name_exists,
|
||||||
|
store_scheme_info,
|
||||||
|
)
|
||||||
|
from app.services.tjnetwork import dump_inp, get_all_scada_info
|
||||||
|
|
||||||
SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]]
|
SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]]
|
||||||
|
FLOW_SCADA_TYPES = {"pipe_flow", "flow", "demand"}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_series(data: SeriesInput, field_name: str) -> pd.Series:
|
def _normalize_series(data: SeriesInput, field_name: str) -> pd.Series:
|
||||||
@@ -36,27 +44,120 @@ def _normalize_series(data: SeriesInput, field_name: str) -> pd.Series:
|
|||||||
def run_burst_location_by_network(
|
def run_burst_location_by_network(
|
||||||
*,
|
*,
|
||||||
network: str,
|
network: str,
|
||||||
pressure_scada_ids: list[str],
|
|
||||||
burst_pressure: SeriesInput,
|
|
||||||
normal_pressure: SeriesInput,
|
|
||||||
burst_leakage: float,
|
burst_leakage: float,
|
||||||
|
pressure_scada_ids: list[str] | None = None,
|
||||||
|
burst_pressure: SeriesInput | None = None,
|
||||||
|
normal_pressure: SeriesInput | None = None,
|
||||||
flow_scada_ids: list[str] | None = None,
|
flow_scada_ids: list[str] | None = None,
|
||||||
burst_flow: SeriesInput | None = None,
|
burst_flow: SeriesInput | None = None,
|
||||||
normal_flow: SeriesInput | None = None,
|
normal_flow: SeriesInput | None = None,
|
||||||
min_dpressure: float = 2.0,
|
min_dpressure: float = 2.0,
|
||||||
basic_pressure: float = 10.0,
|
basic_pressure: float = 10.0,
|
||||||
|
scada_burst_start: datetime | str | None = None,
|
||||||
|
scada_burst_end: datetime | str | None = None,
|
||||||
|
scada_normal_start: datetime | str | None = None,
|
||||||
|
scada_normal_end: datetime | str | None = None,
|
||||||
|
use_scada_flow: bool = False,
|
||||||
|
scheme_name: str | None = None,
|
||||||
|
username: str = "admin",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
if not network:
|
if not network:
|
||||||
raise ValueError("network is required.")
|
raise ValueError("network is required.")
|
||||||
|
|
||||||
tmp_filename = f"burst_location_{network}_{uuid4().hex}.inp"
|
selected_pressure_ids = (
|
||||||
inp_path = Path.cwd() / tmp_filename
|
_dedupe_ids(pressure_scada_ids)
|
||||||
|
if pressure_scada_ids
|
||||||
try:
|
else _get_sensor_nodes(network, data_type="pressure")
|
||||||
dump_inp(network, tmp_filename)
|
)
|
||||||
|
if not selected_pressure_ids:
|
||||||
|
raise ValueError("未提供有效压力传感器,且系统未识别到可用压力传感器。")
|
||||||
|
|
||||||
|
use_scada_pressure = any(
|
||||||
|
value is not None
|
||||||
|
for value in [
|
||||||
|
scada_burst_start,
|
||||||
|
scada_burst_end,
|
||||||
|
scada_normal_start,
|
||||||
|
scada_normal_end,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if use_scada_pressure:
|
||||||
|
(
|
||||||
|
burst_start_dt,
|
||||||
|
burst_end_dt,
|
||||||
|
normal_start_dt,
|
||||||
|
normal_end_dt,
|
||||||
|
) = _validate_scada_windows(
|
||||||
|
scada_burst_start=scada_burst_start,
|
||||||
|
scada_burst_end=scada_burst_end,
|
||||||
|
scada_normal_start=scada_normal_start,
|
||||||
|
scada_normal_end=scada_normal_end,
|
||||||
|
)
|
||||||
|
burst_pressure_series, burst_pressure_samples = _build_observed_series_from_scada(
|
||||||
|
network=network,
|
||||||
|
sensor_ids=selected_pressure_ids,
|
||||||
|
start_dt=burst_start_dt,
|
||||||
|
end_dt=burst_end_dt,
|
||||||
|
data_type="pressure",
|
||||||
|
series_name="burst_pressure",
|
||||||
|
)
|
||||||
|
(
|
||||||
|
normal_pressure_series,
|
||||||
|
normal_pressure_samples,
|
||||||
|
) = _build_observed_series_from_scada(
|
||||||
|
network=network,
|
||||||
|
sensor_ids=selected_pressure_ids,
|
||||||
|
start_dt=normal_start_dt,
|
||||||
|
end_dt=normal_end_dt,
|
||||||
|
data_type="pressure",
|
||||||
|
series_name="normal_pressure",
|
||||||
|
)
|
||||||
|
observed_source = "backend_timerange"
|
||||||
|
else:
|
||||||
|
if burst_pressure is None or normal_pressure is None:
|
||||||
|
raise ValueError(
|
||||||
|
"未提供 burst_pressure/normal_pressure,且未提供完整 SCADA 时间窗参数。"
|
||||||
|
)
|
||||||
burst_pressure_series = _normalize_series(burst_pressure, "burst_pressure")
|
burst_pressure_series = _normalize_series(burst_pressure, "burst_pressure")
|
||||||
normal_pressure_series = _normalize_series(normal_pressure, "normal_pressure")
|
normal_pressure_series = _normalize_series(normal_pressure, "normal_pressure")
|
||||||
|
burst_pressure_samples = 1
|
||||||
|
normal_pressure_samples = 1
|
||||||
|
observed_source = "request_payload"
|
||||||
|
burst_start_dt = burst_end_dt = normal_start_dt = normal_end_dt = None
|
||||||
|
|
||||||
|
selected_flow_ids: list[str] | None = None
|
||||||
|
burst_flow_series: pd.Series | None = None
|
||||||
|
normal_flow_series: pd.Series | None = None
|
||||||
|
use_flow_scada_source = use_scada_pressure and (
|
||||||
|
use_scada_flow or flow_scada_ids is not None
|
||||||
|
)
|
||||||
|
if use_flow_scada_source:
|
||||||
|
selected_flow_ids = (
|
||||||
|
_dedupe_ids(flow_scada_ids)
|
||||||
|
if flow_scada_ids is not None
|
||||||
|
else _get_sensor_nodes(network, data_type="flow")
|
||||||
|
)
|
||||||
|
if not selected_flow_ids:
|
||||||
|
raise ValueError("未找到可用流量传感器,无法从 SCADA 查询流量数据。")
|
||||||
|
burst_flow_series, burst_flow_samples = _build_observed_series_from_scada(
|
||||||
|
network=network,
|
||||||
|
sensor_ids=selected_flow_ids,
|
||||||
|
start_dt=burst_start_dt,
|
||||||
|
end_dt=burst_end_dt,
|
||||||
|
data_type="flow",
|
||||||
|
series_name="burst_flow",
|
||||||
|
)
|
||||||
|
normal_flow_series, normal_flow_samples = _build_observed_series_from_scada(
|
||||||
|
network=network,
|
||||||
|
sensor_ids=selected_flow_ids,
|
||||||
|
start_dt=normal_start_dt,
|
||||||
|
end_dt=normal_end_dt,
|
||||||
|
data_type="flow",
|
||||||
|
series_name="normal_flow",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if flow_scada_ids is not None:
|
||||||
|
selected_flow_ids = _dedupe_ids(flow_scada_ids)
|
||||||
burst_flow_series = (
|
burst_flow_series = (
|
||||||
_normalize_series(burst_flow, "burst_flow") if burst_flow is not None else None
|
_normalize_series(burst_flow, "burst_flow") if burst_flow is not None else None
|
||||||
)
|
)
|
||||||
@@ -65,19 +166,228 @@ def run_burst_location_by_network(
|
|||||||
if normal_flow is not None
|
if normal_flow is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
burst_flow_samples = 1 if burst_flow_series is not None else 0
|
||||||
|
normal_flow_samples = 1 if normal_flow_series is not None else 0
|
||||||
|
|
||||||
return run_burst_location(
|
inp_path = _prepare_burst_inp(network)
|
||||||
wn_inp_path=str(inp_path),
|
result = run_burst_location(
|
||||||
pressure_scada_ids=pressure_scada_ids,
|
wn_inp_path=inp_path,
|
||||||
burst_pressure=burst_pressure_series,
|
pressure_scada_ids=selected_pressure_ids,
|
||||||
normal_pressure=normal_pressure_series,
|
burst_pressure=burst_pressure_series,
|
||||||
|
normal_pressure=normal_pressure_series,
|
||||||
|
burst_leakage=burst_leakage,
|
||||||
|
flow_scada_ids=selected_flow_ids,
|
||||||
|
burst_flow=burst_flow_series,
|
||||||
|
normal_flow=normal_flow_series,
|
||||||
|
min_dpressure=min_dpressure,
|
||||||
|
basic_pressure=basic_pressure,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload: dict[str, Any] = {
|
||||||
|
**result,
|
||||||
|
"network": network,
|
||||||
|
"pressure_scada_ids": selected_pressure_ids,
|
||||||
|
"flow_scada_ids": selected_flow_ids or [],
|
||||||
|
"observed_source": observed_source,
|
||||||
|
"pressure_samples": {
|
||||||
|
"burst": burst_pressure_samples,
|
||||||
|
"normal": normal_pressure_samples,
|
||||||
|
},
|
||||||
|
"flow_samples": {"burst": burst_flow_samples, "normal": normal_flow_samples},
|
||||||
|
}
|
||||||
|
if use_scada_pressure:
|
||||||
|
payload["scada_window"] = {
|
||||||
|
"burst_start": burst_start_dt.isoformat(),
|
||||||
|
"burst_end": burst_end_dt.isoformat(),
|
||||||
|
"normal_start": normal_start_dt.isoformat(),
|
||||||
|
"normal_end": normal_end_dt.isoformat(),
|
||||||
|
}
|
||||||
|
if scheme_name:
|
||||||
|
_store_burst_scheme(
|
||||||
|
network=network,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
username=username,
|
||||||
|
payload=payload,
|
||||||
burst_leakage=burst_leakage,
|
burst_leakage=burst_leakage,
|
||||||
flow_scada_ids=flow_scada_ids,
|
|
||||||
burst_flow=burst_flow_series,
|
|
||||||
normal_flow=normal_flow_series,
|
|
||||||
min_dpressure=min_dpressure,
|
min_dpressure=min_dpressure,
|
||||||
basic_pressure=basic_pressure,
|
basic_pressure=basic_pressure,
|
||||||
)
|
)
|
||||||
finally:
|
return payload
|
||||||
if inp_path.exists():
|
|
||||||
inp_path.unlink()
|
|
||||||
|
def list_burst_location_schemes(
|
||||||
|
network: str, query_date: datetime | str | None = None
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
parsed_date = _to_datetime(query_date).date() if query_date is not None else None
|
||||||
|
return query_burst_location_schemes(name=network, network=network, query_date=parsed_date)
|
||||||
|
|
||||||
|
|
||||||
|
def get_burst_location_scheme_detail(network: str, scheme_name: str) -> dict[str, Any]:
|
||||||
|
result = query_burst_location_scheme_detail(network, scheme_name)
|
||||||
|
if not result:
|
||||||
|
raise ValueError(f"未找到爆管定位方案: {scheme_name}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _store_burst_scheme(
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
scheme_name: str,
|
||||||
|
username: str,
|
||||||
|
payload: dict[str, Any],
|
||||||
|
burst_leakage: float,
|
||||||
|
min_dpressure: float,
|
||||||
|
basic_pressure: float,
|
||||||
|
) -> None:
|
||||||
|
if scheme_name_exists(network, scheme_name):
|
||||||
|
raise ValueError(f"方案名称已存在: {scheme_name}")
|
||||||
|
|
||||||
|
now_iso = datetime.now().isoformat()
|
||||||
|
scheme_detail = {
|
||||||
|
"network": network,
|
||||||
|
"pressure_scada_ids": payload.get("pressure_scada_ids", []),
|
||||||
|
"flow_scada_ids": payload.get("flow_scada_ids", []),
|
||||||
|
"observed_source": payload.get("observed_source"),
|
||||||
|
"algorithm_params": {
|
||||||
|
"burst_leakage": burst_leakage,
|
||||||
|
"min_dpressure": min_dpressure,
|
||||||
|
"basic_pressure": basic_pressure,
|
||||||
|
},
|
||||||
|
"scada_window": payload.get("scada_window"),
|
||||||
|
"result_summary": {
|
||||||
|
"located_pipe": payload.get("located_pipe"),
|
||||||
|
"simulation_times": payload.get("simulation_times"),
|
||||||
|
"similarity_mode": payload.get("similarity_mode"),
|
||||||
|
},
|
||||||
|
"result_payload": payload,
|
||||||
|
}
|
||||||
|
store_scheme_info(
|
||||||
|
name=network,
|
||||||
|
scheme_name=scheme_name,
|
||||||
|
scheme_type="burst_location",
|
||||||
|
username=username,
|
||||||
|
scheme_start_time=now_iso,
|
||||||
|
scheme_detail=scheme_detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_scada_windows(
|
||||||
|
*,
|
||||||
|
scada_burst_start: datetime | str | None,
|
||||||
|
scada_burst_end: datetime | str | None,
|
||||||
|
scada_normal_start: datetime | str | None,
|
||||||
|
scada_normal_end: datetime | str | None,
|
||||||
|
) -> tuple[datetime, datetime, datetime, datetime]:
|
||||||
|
values = [scada_burst_start, scada_burst_end, scada_normal_start, scada_normal_end]
|
||||||
|
if any(v is None for v in values):
|
||||||
|
raise ValueError(
|
||||||
|
"使用后端 SCADA 查询时,必须同时提供 scada_burst_start/scada_burst_end/scada_normal_start/scada_normal_end。"
|
||||||
|
)
|
||||||
|
burst_start_dt = _to_datetime(scada_burst_start)
|
||||||
|
burst_end_dt = _to_datetime(scada_burst_end)
|
||||||
|
normal_start_dt = _to_datetime(scada_normal_start)
|
||||||
|
normal_end_dt = _to_datetime(scada_normal_end)
|
||||||
|
if burst_start_dt >= burst_end_dt:
|
||||||
|
raise ValueError("爆管时段 SCADA 时间窗非法:scada_burst_start 必须早于 scada_burst_end。")
|
||||||
|
if normal_start_dt >= normal_end_dt:
|
||||||
|
raise ValueError(
|
||||||
|
"正常时段 SCADA 时间窗非法:scada_normal_start 必须早于 scada_normal_end。"
|
||||||
|
)
|
||||||
|
return burst_start_dt, burst_end_dt, normal_start_dt, normal_end_dt
|
||||||
|
|
||||||
|
|
||||||
|
def _build_observed_series_from_scada(
|
||||||
|
*,
|
||||||
|
network: str,
|
||||||
|
sensor_ids: list[str],
|
||||||
|
start_dt: datetime,
|
||||||
|
end_dt: datetime,
|
||||||
|
data_type: str,
|
||||||
|
series_name: str,
|
||||||
|
) -> tuple[pd.Series, int]:
|
||||||
|
scada_mapping = _build_scada_mapping(network=network, data_type=data_type)
|
||||||
|
missing_ids = [sensor_id for sensor_id in sensor_ids if sensor_id not in scada_mapping]
|
||||||
|
if missing_ids:
|
||||||
|
preview = ", ".join(missing_ids[:10])
|
||||||
|
raise ValueError(f"{series_name} 缺少可用 SCADA 映射: {preview}")
|
||||||
|
|
||||||
|
query_ids = [scada_mapping[sensor_id] for sensor_id in sensor_ids]
|
||||||
|
scada_data = InternalQueries.query_scada_by_ids_timerange(
|
||||||
|
db_name=network,
|
||||||
|
device_ids=query_ids,
|
||||||
|
start_time=start_dt.isoformat(),
|
||||||
|
end_time=end_dt.isoformat(),
|
||||||
|
)
|
||||||
|
values: dict[str, float] = {}
|
||||||
|
sample_counts: list[int] = []
|
||||||
|
for sensor_id, query_id in zip(sensor_ids, query_ids):
|
||||||
|
records = scada_data.get(query_id, [])
|
||||||
|
numeric_values = [
|
||||||
|
float(item["value"])
|
||||||
|
for item in records
|
||||||
|
if item.get("value") is not None
|
||||||
|
]
|
||||||
|
if not numeric_values:
|
||||||
|
raise ValueError(f"{series_name} 在时间窗内无有效数据: {sensor_id}")
|
||||||
|
values[sensor_id] = float(sum(numeric_values) / len(numeric_values))
|
||||||
|
sample_counts.append(len(numeric_values))
|
||||||
|
|
||||||
|
return pd.Series(values, dtype=float), min(sample_counts)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_scada_mapping(network: str, data_type: str) -> dict[str, str]:
|
||||||
|
mapping: dict[str, str] = {}
|
||||||
|
for item in get_all_scada_info(network):
|
||||||
|
scada_type = str(item.get("type", "")).lower()
|
||||||
|
if data_type == "pressure":
|
||||||
|
if scada_type != "pressure":
|
||||||
|
continue
|
||||||
|
elif data_type == "flow":
|
||||||
|
if scada_type not in FLOW_SCADA_TYPES:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported data_type: {data_type}")
|
||||||
|
node_id = item.get("associated_element_id")
|
||||||
|
query_id = item.get("api_query_id")
|
||||||
|
if (
|
||||||
|
isinstance(node_id, str)
|
||||||
|
and node_id
|
||||||
|
and isinstance(query_id, str)
|
||||||
|
and query_id
|
||||||
|
):
|
||||||
|
mapping[node_id] = query_id
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sensor_nodes(network: str, data_type: str) -> list[str]:
|
||||||
|
mapping = _build_scada_mapping(network=network, data_type=data_type)
|
||||||
|
sensor_ids = sorted(mapping.keys())
|
||||||
|
if not sensor_ids:
|
||||||
|
type_name = "压力" if data_type == "pressure" else "流量"
|
||||||
|
raise ValueError(f"未找到{type_name}传感器对应节点(scada_info.type)。")
|
||||||
|
return sensor_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_ids(ids: list[str] | None) -> list[str]:
|
||||||
|
if ids is None:
|
||||||
|
return []
|
||||||
|
return list(dict.fromkeys([str(item) for item in ids if item]))
|
||||||
|
|
||||||
|
|
||||||
|
def _to_datetime(value: datetime | str) -> datetime:
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
return datetime.fromisoformat(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_burst_inp(network: str) -> str:
|
||||||
|
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
db_inp_dir = os.path.join(project_root, "db_inp")
|
||||||
|
os.makedirs(db_inp_dir, exist_ok=True)
|
||||||
|
inp_path = os.path.join(db_inp_dir, f"{network}.burst.inp")
|
||||||
|
if os.path.isfile(inp_path) and os.path.getsize(inp_path) > 0:
|
||||||
|
return inp_path
|
||||||
|
dump_inp(network, inp_path, "2")
|
||||||
|
if not os.path.isfile(inp_path) or os.path.getsize(inp_path) <= 0:
|
||||||
|
raise ValueError(f"爆管定位 INP 文件无效: {inp_path}")
|
||||||
|
return inp_path
|
||||||
|
|||||||
@@ -171,47 +171,6 @@ def query_scheme_list(name: str) -> list:
|
|||||||
print(f"查询错误:{e}")
|
print(f"查询错误:{e}")
|
||||||
|
|
||||||
|
|
||||||
def ensure_leakage_identify_result_table(name: str) -> None:
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
|
||||||
with psycopg.connect(conn_string) as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
CREATE TABLE IF NOT EXISTS public.leakage_identify_result (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
scheme_name VARCHAR(255) NOT NULL,
|
|
||||||
network VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
run_status VARCHAR(32) NOT NULL DEFAULT 'completed',
|
|
||||||
error_message TEXT,
|
|
||||||
sensor_nodes JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
result_rows JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
node_area_map JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
||||||
areas JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
||||||
drawing_payload JSONB NOT NULL DEFAULT '{"type":"FeatureCollection","features":[]}'::jsonb,
|
|
||||||
CONSTRAINT uq_leakage_identify_result_scheme UNIQUE (scheme_name),
|
|
||||||
CONSTRAINT fk_leakage_identify_result_scheme
|
|
||||||
FOREIGN KEY (scheme_name)
|
|
||||||
REFERENCES public.scheme_list (scheme_name)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_leakage_identify_result_network ON public.leakage_identify_result (network);"
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_leakage_identify_result_created_at ON public.leakage_identify_result (created_at DESC);"
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_leakage_identify_result_run_status ON public.leakage_identify_result (run_status);"
|
|
||||||
)
|
|
||||||
cur.execute(
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_leakage_identify_result_rows_gin ON public.leakage_identify_result USING GIN (result_rows);"
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def store_leakage_identify_result(
|
def store_leakage_identify_result(
|
||||||
name: str,
|
name: str,
|
||||||
scheme_name: str,
|
scheme_name: str,
|
||||||
@@ -224,7 +183,6 @@ def store_leakage_identify_result(
|
|||||||
run_status: str = "completed",
|
run_status: str = "completed",
|
||||||
error_message: str | None = None,
|
error_message: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
ensure_leakage_identify_result_table(name)
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -313,7 +271,6 @@ def query_leakage_identify_schemes(
|
|||||||
|
|
||||||
|
|
||||||
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
|
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
|
||||||
ensure_leakage_identify_result_table(name)
|
|
||||||
conn_string = get_pgconn_string(db_name=name)
|
conn_string = get_pgconn_string(db_name=name)
|
||||||
with psycopg.connect(conn_string) as conn:
|
with psycopg.connect(conn_string) as conn:
|
||||||
with conn.cursor() as cur:
|
with conn.cursor() as cur:
|
||||||
@@ -358,11 +315,92 @@ def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict:
|
|||||||
"node_area_map": result_row[6] if isinstance(result_row[6], dict) else {},
|
"node_area_map": result_row[6] if isinstance(result_row[6], dict) else {},
|
||||||
"areas": result_row[7] if isinstance(result_row[7], list) else [],
|
"areas": result_row[7] if isinstance(result_row[7], list) else [],
|
||||||
"drawing_payload": (
|
"drawing_payload": (
|
||||||
result_row[8] if isinstance(result_row[8], dict) else {"type": "FeatureCollection", "features": []}
|
result_row[8]
|
||||||
|
if isinstance(result_row[8], dict)
|
||||||
|
else {"type": "FeatureCollection", "features": []}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def query_burst_location_schemes(
|
||||||
|
name: str,
|
||||||
|
network: str,
|
||||||
|
scheme_type: str = "burst_location",
|
||||||
|
query_date: date | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
conn_string = get_pgconn_string(db_name=name)
|
||||||
|
with psycopg.connect(conn_string) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
if query_date is None:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||||
|
FROM public.scheme_list
|
||||||
|
WHERE scheme_type = %s
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
""",
|
||||||
|
(scheme_type,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||||
|
FROM public.scheme_list
|
||||||
|
WHERE scheme_type = %s AND DATE(create_time) = %s
|
||||||
|
ORDER BY create_time DESC
|
||||||
|
""",
|
||||||
|
(scheme_type, query_date),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
detail = row[6] if isinstance(row[6], dict) else {}
|
||||||
|
if network and detail.get("network") not in (None, network):
|
||||||
|
continue
|
||||||
|
result.append(
|
||||||
|
{
|
||||||
|
"scheme_id": row[0],
|
||||||
|
"scheme_name": row[1],
|
||||||
|
"scheme_type": row[2],
|
||||||
|
"username": row[3],
|
||||||
|
"create_time": row[4],
|
||||||
|
"scheme_start_time": row[5],
|
||||||
|
"scheme_detail": detail,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict:
|
||||||
|
conn_string = get_pgconn_string(db_name=name)
|
||||||
|
with psycopg.connect(conn_string) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT scheme_id, scheme_name, scheme_type, username, create_time, scheme_start_time, scheme_detail
|
||||||
|
FROM public.scheme_list
|
||||||
|
WHERE scheme_name = %s
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(scheme_name,),
|
||||||
|
)
|
||||||
|
base_row = cur.fetchone()
|
||||||
|
if base_row is None:
|
||||||
|
return {}
|
||||||
|
detail = base_row[6] if isinstance(base_row[6], dict) else {}
|
||||||
|
return {
|
||||||
|
"scheme_id": base_row[0],
|
||||||
|
"scheme_name": base_row[1],
|
||||||
|
"scheme_type": base_row[2],
|
||||||
|
"username": base_row[3],
|
||||||
|
"create_time": base_row[4],
|
||||||
|
"scheme_start_time": base_row[5],
|
||||||
|
"scheme_detail": detail,
|
||||||
|
"network": detail.get("network"),
|
||||||
|
"result_payload": detail.get("result_payload", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# 2025/03/23
|
# 2025/03/23
|
||||||
def upload_shp_to_pg(name: str, table_name: str, role: str, shp_file_path: str):
|
def upload_shp_to_pg(name: str, table_name: str, role: str, shp_file_path: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user