refactor(db)!: clean up business SQL access
- make realtime replacement and analysis result writes transactional\n- consolidate SCADA repositories and remove process-global project state\n- validate SCADA batches and use indexed GIS-backed business queries\n\nBREAKING CHANGE: remove the public analysis result writer and the pipeline-health network_name query parameter.
This commit is contained in:
@@ -11,13 +11,10 @@ import pandas as pd
|
||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.services.scheme_management import (
|
||||
query_burst_detection_scheme_detail,
|
||||
query_burst_detection_schemes,
|
||||
scheme_name_exists,
|
||||
store_scheme_info,
|
||||
)
|
||||
from app.services.tjnetwork import get_all_scada_info
|
||||
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
|
||||
|
||||
TARGET_DAY_COUNT = 15
|
||||
@@ -365,25 +362,6 @@ def _build_observed_pressure_from_simulation(
|
||||
return observation_df
|
||||
|
||||
|
||||
def list_burst_detection_schemes(
|
||||
network: str,
|
||||
query_date: datetime | str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
parsed_date = extract_date(query_date, field_name="query_date") if query_date is not None else None
|
||||
return query_burst_detection_schemes(
|
||||
name=network,
|
||||
network=network,
|
||||
query_date=parsed_date,
|
||||
)
|
||||
|
||||
|
||||
def get_burst_detection_scheme_detail(network: str, scheme_name: str) -> dict[str, Any]:
|
||||
result = query_burst_detection_scheme_detail(network, scheme_name)
|
||||
if not result:
|
||||
raise ValueError(f"未找到爆管侦测方案: {scheme_name}")
|
||||
return result
|
||||
|
||||
|
||||
def _store_burst_detection_scheme(
|
||||
*,
|
||||
network: str,
|
||||
@@ -394,9 +372,6 @@ def _store_burst_detection_scheme(
|
||||
points_per_day: int,
|
||||
iforest_params: dict[str, Any],
|
||||
) -> None:
|
||||
if scheme_name_exists(network, scheme_name):
|
||||
raise ValueError(f"方案名称已存在: {scheme_name}")
|
||||
|
||||
now_iso = utc_now().isoformat()
|
||||
scheme_detail = {
|
||||
"network": network,
|
||||
|
||||
@@ -10,14 +10,11 @@ import pandas as pd
|
||||
from app.algorithms.burst_location import run_burst_location
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.services.scheme_management import (
|
||||
query_burst_location_scheme_detail,
|
||||
query_burst_location_schemes,
|
||||
get_analysis_run,
|
||||
scheme_name_exists,
|
||||
store_scheme_info,
|
||||
)
|
||||
from app.services.tjnetwork import dump_inp, get_all_scada_info
|
||||
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
|
||||
SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]]
|
||||
FLOW_SCADA_TYPES = {"pipe_flow", "flow", "demand"}
|
||||
@@ -367,22 +364,6 @@ def run_burst_location_by_network(
|
||||
return payload
|
||||
|
||||
|
||||
def list_burst_location_schemes(
|
||||
network: str, query_date: datetime | str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
parsed_date = extract_date(query_date, field_name="query_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,
|
||||
@@ -393,9 +374,6 @@ def _store_burst_scheme(
|
||||
min_dpressure: float,
|
||||
basic_pressure: float,
|
||||
) -> None:
|
||||
if scheme_name_exists(network, scheme_name):
|
||||
raise ValueError(f"方案名称已存在: {scheme_name}")
|
||||
|
||||
now_iso = utc_now().isoformat()
|
||||
scheme_detail = {
|
||||
"network": network,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"""Mutable state used by the legacy synchronous simulation runner."""
|
||||
|
||||
RESERVOIR_BASIC_HEIGHT = 250.35
|
||||
PATTERN_TIME_STEP: float | None = None
|
||||
hydraulic_timestep: str | None = None
|
||||
|
||||
# Element ID -> SCADA api_query_id, loaded per project before simulation.
|
||||
reservoirs_id: dict[str, str] = {}
|
||||
tanks_id: dict[str, str] = {}
|
||||
fixed_pumps_id: dict[str, str] = {}
|
||||
variable_pumps_id: dict[str, str] = {}
|
||||
pressure_id: dict[str, str] = {}
|
||||
demand_id: dict[str, str] = {}
|
||||
quality_id: dict[str, str] = {}
|
||||
@@ -10,20 +10,14 @@ import wntr
|
||||
|
||||
from app.algorithms.leakage.identifier import LeakageIdentifier
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.services.scheme_management import (
|
||||
query_leakage_identify_scheme_detail,
|
||||
query_leakage_identify_schemes,
|
||||
scheme_name_exists,
|
||||
store_leakage_identify_result,
|
||||
store_scheme_info,
|
||||
)
|
||||
from app.services.scheme_management import store_analysis_run_with_result
|
||||
from app.services.tjnetwork import (
|
||||
dump_inp,
|
||||
get_all_scada_info,
|
||||
get_network_link_nodes,
|
||||
get_network_node_coords,
|
||||
)
|
||||
from app.services.time_api import extract_date, parse_utc_time, utc_now
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
|
||||
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
|
||||
|
||||
@@ -115,8 +109,6 @@ def run_leakage_identification(
|
||||
"rows": rows,
|
||||
}
|
||||
if scheme_name:
|
||||
if scheme_name_exists(network, scheme_name):
|
||||
raise ValueError(f"方案名称已存在: {scheme_name}")
|
||||
scheme_start_time = (
|
||||
_to_datetime(scada_start).isoformat()
|
||||
if scada_start is not None
|
||||
@@ -153,46 +145,29 @@ def run_leakage_identification(
|
||||
),
|
||||
},
|
||||
}
|
||||
store_scheme_info(
|
||||
store_analysis_run_with_result(
|
||||
name=network,
|
||||
scheme_name=scheme_name,
|
||||
scheme_type="dma_leak_identification",
|
||||
username=username,
|
||||
scheme_start_time=scheme_start_time,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
store_leakage_identify_result(
|
||||
name=network,
|
||||
scheme_name=scheme_name,
|
||||
network=network,
|
||||
sensor_nodes=selected_sensor_nodes,
|
||||
result_rows=rows,
|
||||
node_area_map=area_map,
|
||||
areas=areas,
|
||||
drawing_payload={},
|
||||
result_type="leakage_identification",
|
||||
result_payload={
|
||||
"network": network,
|
||||
"run_status": "completed",
|
||||
"error_message": None,
|
||||
"sensor_nodes": selected_sensor_nodes,
|
||||
"rows": rows,
|
||||
"node_area_map": area_map,
|
||||
"areas": areas,
|
||||
"drawing_payload": {},
|
||||
},
|
||||
)
|
||||
payload["scheme_name"] = scheme_name
|
||||
return payload
|
||||
|
||||
|
||||
def list_leakage_identify_schemes(
|
||||
network: str, query_date: datetime | str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
parsed_date = extract_date(query_date, field_name="query_date") if query_date is not None else None
|
||||
return query_leakage_identify_schemes(
|
||||
name=network, network=network, query_date=parsed_date
|
||||
)
|
||||
|
||||
|
||||
def get_leakage_identify_scheme_detail(
|
||||
network: str, scheme_name: str
|
||||
) -> dict[str, Any]:
|
||||
result = query_leakage_identify_scheme_detail(network, scheme_name)
|
||||
if not result:
|
||||
raise ValueError(f"未找到漏损识别方案: {scheme_name}")
|
||||
return result
|
||||
|
||||
|
||||
def _get_pressure_sensor_nodes(network: str) -> list[str]:
|
||||
scada_devices = get_all_scada_info(network)
|
||||
sensor_nodes: list[str] = []
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from app.native.wndb.core.connection import project_connection
|
||||
from app.infra.db.postgresql.analysis import AnalysisRepository
|
||||
from app.native.wndb.core.connection import project_connection, project_transaction
|
||||
from app.services.time_api import parse_utc_time
|
||||
|
||||
|
||||
def scheme_name_exists(name: str, scheme_name: str) -> bool:
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"select exists(select 1 from analysis.runs where name = %s)",
|
||||
(scheme_name,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return bool(row and row[0])
|
||||
|
||||
|
||||
def store_scheme_info(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
scheme_type: str,
|
||||
username: str,
|
||||
scheme_start_time: datetime | str,
|
||||
scheme_detail: dict,
|
||||
scheme_detail: dict[str, Any],
|
||||
) -> UUID:
|
||||
"""Create one completed, immutable analysis run."""
|
||||
"""Create one completed analysis run; its name remains a display label."""
|
||||
return create_analysis_run(
|
||||
name=name,
|
||||
scheme_name=scheme_name,
|
||||
@@ -44,29 +33,56 @@ def create_analysis_run(
|
||||
scheme_type: str,
|
||||
username: str,
|
||||
scheme_start_time: datetime | str,
|
||||
scheme_detail: dict,
|
||||
scheme_detail: dict[str, Any],
|
||||
*,
|
||||
status: str = "running",
|
||||
) -> UUID:
|
||||
"""Create a distinct execution record; names are labels, not identities."""
|
||||
started_at = parse_utc_time(scheme_start_time, field_name="scheme_start_time")
|
||||
run_id = uuid4()
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
insert into analysis.runs
|
||||
(run_id, name, run_type, created_by, created_at, started_at, status, parameters)
|
||||
values (%s, %s, %s, %s, now(), %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
run_id,
|
||||
scheme_name,
|
||||
scheme_type,
|
||||
username,
|
||||
started_at,
|
||||
status,
|
||||
Jsonb(scheme_detail),
|
||||
),
|
||||
with project_connection(name) as conn:
|
||||
AnalysisRepository.create_run_sync(
|
||||
conn,
|
||||
run_id=run_id,
|
||||
name=scheme_name,
|
||||
run_type=scheme_type,
|
||||
created_by=username,
|
||||
started_at=started_at,
|
||||
status=status,
|
||||
parameters=scheme_detail,
|
||||
)
|
||||
return run_id
|
||||
|
||||
|
||||
def store_analysis_run_with_result(
|
||||
*,
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
scheme_type: str,
|
||||
username: str,
|
||||
scheme_start_time: datetime | str,
|
||||
scheme_detail: dict[str, Any],
|
||||
result_type: str,
|
||||
result_payload: dict[str, Any],
|
||||
) -> UUID:
|
||||
"""Atomically persist one BizDB run and its non-timeseries result."""
|
||||
started_at = parse_utc_time(scheme_start_time, field_name="scheme_start_time")
|
||||
run_id = uuid4()
|
||||
with project_transaction(name) as conn:
|
||||
AnalysisRepository.create_run_sync(
|
||||
conn,
|
||||
run_id=run_id,
|
||||
name=scheme_name,
|
||||
run_type=scheme_type,
|
||||
created_by=username,
|
||||
started_at=started_at,
|
||||
status="completed",
|
||||
parameters=scheme_detail,
|
||||
)
|
||||
AnalysisRepository.insert_result_sync(
|
||||
conn,
|
||||
run_id,
|
||||
result_type=result_type,
|
||||
payload=result_payload,
|
||||
)
|
||||
return run_id
|
||||
|
||||
@@ -77,208 +93,24 @@ def update_analysis_run(
|
||||
*,
|
||||
status: str,
|
||||
username: str,
|
||||
scheme_detail: dict,
|
||||
scheme_detail: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update lifecycle state and metadata for one execution identity."""
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
update analysis.runs
|
||||
set created_by = %s, status = %s, parameters = %s
|
||||
where run_id = %s
|
||||
""",
|
||||
(username, status, Jsonb(scheme_detail), run_id),
|
||||
with project_connection(name) as conn:
|
||||
AnalysisRepository.update_run_sync(
|
||||
conn,
|
||||
run_id,
|
||||
status=status,
|
||||
created_by=username,
|
||||
parameters=scheme_detail,
|
||||
)
|
||||
if cur.rowcount != 1:
|
||||
raise LookupError(f"analysis run {run_id} does not exist")
|
||||
|
||||
|
||||
def _run_row(row: dict[str, Any]) -> dict[str, Any]:
|
||||
parameters = row.get("parameters") if isinstance(row.get("parameters"), dict) else {}
|
||||
return {
|
||||
"run_id": row["run_id"],
|
||||
"name": row["name"],
|
||||
"run_type": row["run_type"],
|
||||
"created_by": row["created_by"],
|
||||
"created_at": row["created_at"],
|
||||
"started_at": row["started_at"],
|
||||
"status": row["status"],
|
||||
"parameters": parameters,
|
||||
}
|
||||
|
||||
|
||||
def _list_runs(
|
||||
name: str,
|
||||
run_type: str | None = None,
|
||||
query_date: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
clauses: list[str] = []
|
||||
params: list[Any] = []
|
||||
if run_type:
|
||||
clauses.append("run_type = %s")
|
||||
params.append(run_type)
|
||||
if query_date is not None:
|
||||
clauses.append("created_at::date = %s")
|
||||
params.append(query_date)
|
||||
where = f"where {' and '.join(clauses)}" if clauses else ""
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"select run_id, name, run_type, created_by, created_at, started_at, status, parameters from analysis.runs {where} order by created_at desc",
|
||||
params,
|
||||
)
|
||||
return [_run_row(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def query_scheme_list(
|
||||
name: str,
|
||||
scheme_type: str | None = None,
|
||||
query_date: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return _list_runs(name, scheme_type, query_date)
|
||||
|
||||
|
||||
def _get_run_by_name(
|
||||
name: str,
|
||||
run_name: str,
|
||||
run_type: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
params: list[Any] = [run_name]
|
||||
type_clause = ""
|
||||
if run_type:
|
||||
type_clause = "and run_type = %s"
|
||||
params.append(run_type)
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
select run_id, name, run_type, created_by, created_at, started_at,
|
||||
status, parameters
|
||||
from analysis.runs
|
||||
where name = %s {type_clause}
|
||||
order by created_at desc
|
||||
limit 1
|
||||
""",
|
||||
params,
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return _run_row(row) if row else {}
|
||||
|
||||
|
||||
def get_analysis_run(name: str, run_id: UUID) -> dict[str, Any]:
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
select run_id, name, run_type, created_by, created_at, started_at,
|
||||
status, parameters
|
||||
from analysis.runs
|
||||
where run_id = %s
|
||||
""",
|
||||
(run_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return _run_row(row) if row else {}
|
||||
|
||||
|
||||
def query_scheme_detail(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
scheme_type: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return _get_run_by_name(name, scheme_name, scheme_type)
|
||||
|
||||
|
||||
def store_leakage_identify_result(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
network: str,
|
||||
sensor_nodes: list[str],
|
||||
result_rows: list[dict],
|
||||
node_area_map: dict[str, str],
|
||||
areas: list[dict],
|
||||
drawing_payload: dict | None = None,
|
||||
run_status: str = "completed",
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
run = _get_run_by_name(name, scheme_name, "dma_leak_identification")
|
||||
if not run:
|
||||
raise LookupError(f"analysis run {scheme_name!r} does not exist")
|
||||
payload = {
|
||||
"network": network,
|
||||
"run_status": run_status,
|
||||
"error_message": error_message,
|
||||
"sensor_nodes": sensor_nodes,
|
||||
"rows": result_rows,
|
||||
"node_area_map": node_area_map,
|
||||
"areas": areas,
|
||||
"drawing_payload": drawing_payload or {},
|
||||
}
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"insert into analysis.results (run_id, result_type, payload) values (%s, 'leakage_identification', %s)",
|
||||
(run["run_id"], Jsonb(payload)),
|
||||
)
|
||||
|
||||
|
||||
def _list_typed_runs(
|
||||
name: str,
|
||||
network: str,
|
||||
run_type: str,
|
||||
query_date: date | None,
|
||||
) -> list[dict[str, Any]]:
|
||||
rows = _list_runs(name, run_type, query_date)
|
||||
return [
|
||||
row
|
||||
for row in rows
|
||||
if not network or row["parameters"].get("network") in (None, network)
|
||||
]
|
||||
|
||||
|
||||
def _typed_run_detail(name: str, run_name: str, run_type: str) -> dict[str, Any]:
|
||||
run = _get_run_by_name(name, run_name, run_type)
|
||||
if not run:
|
||||
with project_connection(name) as conn:
|
||||
row = AnalysisRepository.get_run_sync(conn, run_id)
|
||||
if row is None:
|
||||
return {}
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"select result_type, payload, created_at from analysis.results where run_id = %s order by created_at, result_id",
|
||||
(run["run_id"],),
|
||||
)
|
||||
results = [dict(row) for row in cur.fetchall()]
|
||||
return run | {"results": results}
|
||||
|
||||
|
||||
def query_leakage_identify_schemes(
|
||||
name: str,
|
||||
network: str,
|
||||
scheme_type: str = "dma_leak_identification",
|
||||
query_date: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return _list_typed_runs(name, network, scheme_type, query_date)
|
||||
|
||||
|
||||
def query_leakage_identify_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
|
||||
return _typed_run_detail(name, scheme_name, "dma_leak_identification")
|
||||
|
||||
|
||||
def query_burst_location_schemes(
|
||||
name: str,
|
||||
network: str,
|
||||
scheme_type: str = "burst_location",
|
||||
query_date: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return _list_typed_runs(name, network, scheme_type, query_date)
|
||||
|
||||
|
||||
def query_burst_location_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
|
||||
return _typed_run_detail(name, scheme_name, "burst_location")
|
||||
|
||||
|
||||
def query_burst_detection_schemes(
|
||||
name: str,
|
||||
network: str,
|
||||
scheme_type: str = "burst_detection",
|
||||
query_date: date | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
return _list_typed_runs(name, network, scheme_type, query_date)
|
||||
|
||||
|
||||
def query_burst_detection_scheme_detail(name: str, scheme_name: str) -> dict[str, Any]:
|
||||
return _typed_run_detail(name, scheme_name, "burst_detection")
|
||||
parameters = row.get("parameters")
|
||||
return dict(row) | {
|
||||
"parameters": parameters if isinstance(parameters, dict) else {}
|
||||
}
|
||||
|
||||
@@ -61,11 +61,15 @@ def _normalize_locations(sensor_location: list[str]) -> list[str]:
|
||||
def _sensor_points(
|
||||
network: str,
|
||||
sensor_location: list[str],
|
||||
*,
|
||||
nodes_by_id: dict[str, dict[str, Any]] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
nodes = sensor_placement_repository.get_sensor_placement_nodes(
|
||||
network, sensor_location
|
||||
)
|
||||
by_id = {str(node["node_id"]): node for node in nodes}
|
||||
if nodes_by_id is None:
|
||||
nodes = sensor_placement_repository.get_sensor_placement_nodes(
|
||||
network, sensor_location
|
||||
)
|
||||
nodes_by_id = {str(node["node_id"]): node for node in nodes}
|
||||
by_id = nodes_by_id
|
||||
missing = [node_id for node_id in sensor_location if node_id not in by_id]
|
||||
if missing:
|
||||
raise SensorPlacementValidationError(
|
||||
@@ -131,12 +135,26 @@ def get_sensor_placement_run(network: str, run_id: UUID) -> dict[str, Any]:
|
||||
|
||||
|
||||
def list_sensor_placement_runs(network: str) -> list[dict[str, Any]]:
|
||||
runs = sensor_placement_repository.get_all_sensor_placements(network)
|
||||
node_ids = list(
|
||||
dict.fromkeys(
|
||||
node_id
|
||||
for run in runs
|
||||
for node_id in run["sensor_locations"]
|
||||
)
|
||||
)
|
||||
nodes = sensor_placement_repository.get_sensor_placement_nodes(network, node_ids)
|
||||
nodes_by_id = {str(node["node_id"]): node for node in nodes}
|
||||
return [
|
||||
{
|
||||
**run,
|
||||
"sensor_points": _sensor_points(network, run["sensor_locations"]),
|
||||
"sensor_points": _sensor_points(
|
||||
network,
|
||||
run["sensor_locations"],
|
||||
nodes_by_id=nodes_by_id,
|
||||
),
|
||||
}
|
||||
for run in sensor_placement_repository.get_all_sensor_placements(network)
|
||||
for run in runs
|
||||
]
|
||||
|
||||
|
||||
@@ -161,7 +179,10 @@ def update_sensor_placement_run(
|
||||
if sensor_placement_repository.get_sensor_placement(network, run_id) is None:
|
||||
raise SensorPlacementNotFoundError("监测点优化运行不存在")
|
||||
raise SensorPlacementConflictError("运行结果已被其他用户修改,请重新加载")
|
||||
return get_sensor_placement_run(network, run_id)
|
||||
return {
|
||||
**updated,
|
||||
"sensor_points": _sensor_points(network, updated["sensor_locations"]),
|
||||
}
|
||||
|
||||
|
||||
def can_edit_sensor_placement(user: Any, run: dict[str, Any]) -> bool:
|
||||
|
||||
+40
-59
@@ -30,10 +30,13 @@ from typing import Optional, Tuple
|
||||
from uuid import UUID
|
||||
import typing
|
||||
import logging
|
||||
import app.services.globals as globals
|
||||
import app.services.project_info as project_info
|
||||
from app.infra.db.postgresql.scada import (
|
||||
ScadaElementMappings,
|
||||
load_realtime_element_mappings,
|
||||
)
|
||||
from app.services.time_api import parse_beijing_time, parse_clock_duration_seconds
|
||||
from app.native.wndb.core.connection import project_connection, project_transaction
|
||||
from app.native.wndb.core.connection import project_transaction
|
||||
from app.native.wndb.core.database import refresh_materialized_views_after_commit
|
||||
from app.infra.db.timescaledb.internal_queries import (
|
||||
InternalQueries as TimescaleInternalQueries,
|
||||
@@ -47,6 +50,8 @@ logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
RESERVOIR_BASIC_HEIGHT = 250.35
|
||||
|
||||
|
||||
def _primary_demand(demand_set: dict) -> dict:
|
||||
"""Return sequence-zero demand, creating it when a junction has none."""
|
||||
@@ -73,39 +78,12 @@ def _primary_demand_pattern(demand_set: dict) -> str:
|
||||
return str(pattern)
|
||||
|
||||
|
||||
def query_corresponding_element_id_and_query_id(name: str) -> None:
|
||||
"""Load realtime device-to-element mappings from the new asset schema."""
|
||||
target_maps = {
|
||||
"reservoir_liquid_level": globals.reservoirs_id,
|
||||
"tank_liquid_level": globals.tanks_id,
|
||||
"fixed_pump": globals.fixed_pumps_id,
|
||||
"variable_pump": globals.variable_pumps_id,
|
||||
"pressure": globals.pressure_id,
|
||||
"demand": globals.demand_id,
|
||||
"quality": globals.quality_id,
|
||||
}
|
||||
for mapping in target_maps.values():
|
||||
mapping.clear()
|
||||
with project_connection(name) as conn, conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT device_type, COALESCE(node_id, link_id) AS element_id,
|
||||
api_query_id
|
||||
FROM asset.scada_devices
|
||||
WHERE transmission_mode = 'realtime'
|
||||
AND api_query_id IS NOT NULL
|
||||
"""
|
||||
)
|
||||
for record in cur.fetchall():
|
||||
device_type = record["device_type"]
|
||||
element_id = record["element_id"]
|
||||
api_query_id = record["api_query_id"]
|
||||
mapping = target_maps.get(str(device_type).lower())
|
||||
if mapping is not None:
|
||||
mapping[str(element_id)] = str(api_query_id)
|
||||
def query_corresponding_element_id_and_query_id(name: str) -> ScadaElementMappings:
|
||||
"""Return an immutable project-local SCADA-to-model mapping snapshot."""
|
||||
return load_realtime_element_mappings(name)
|
||||
|
||||
|
||||
def get_pattern_index(cur_datetime: str) -> int:
|
||||
def get_pattern_index(cur_datetime: str, pattern_time_step: float) -> int:
|
||||
"""
|
||||
根据给定的日期时间字符串,计算并返回对应的模式索引。
|
||||
:param cur_datetime: str, 当前的日期时间字符串,格式为“YYYY-MM-DD HH:MM:SS”。
|
||||
@@ -115,18 +93,18 @@ def get_pattern_index(cur_datetime: str) -> int:
|
||||
dt = datetime.strptime(cur_datetime, str_format)
|
||||
hr = dt.hour
|
||||
mnt = dt.minute
|
||||
i = int((hr * 60 + mnt) / globals.PATTERN_TIME_STEP)
|
||||
i = int((hr * 60 + mnt) / pattern_time_step)
|
||||
return i
|
||||
|
||||
|
||||
def get_pattern_index_str(current_time: str) -> str:
|
||||
def get_pattern_index_str(current_time: str, pattern_time_step: float) -> str:
|
||||
"""
|
||||
根据当前时间获取时间步长的模式索引,并将其格式化为“HH:MM:00”字符串。
|
||||
:param current_time: str, 当前时间,格式为"YYYY-MM-DD HH:MM:SS"
|
||||
:return: str, 以“HH:MM:00”格式返回
|
||||
"""
|
||||
i = get_pattern_index(current_time)
|
||||
[minN, hrN] = modf(i * globals.PATTERN_TIME_STEP / 60)
|
||||
i = get_pattern_index(current_time, pattern_time_step)
|
||||
[minN, hrN] = modf(i * pattern_time_step / 60)
|
||||
minN_str = str(int(minN * 60))
|
||||
minN_str = minN_str.zfill(2)
|
||||
hrN_str = str(int(hrN))
|
||||
@@ -204,6 +182,7 @@ def run_simulation(
|
||||
valve_control: dict[str, dict] = None,
|
||||
scheme_username: str = "system",
|
||||
scheme_detail: dict | None = None,
|
||||
scada_mappings: ScadaElementMappings | None = None,
|
||||
) -> UUID | None:
|
||||
"""
|
||||
传入需要修改的参数,改变数据库中对应位置的值,然后计算,返回结果
|
||||
@@ -257,19 +236,20 @@ def run_simulation(
|
||||
print(dic_time)
|
||||
|
||||
# 获取水力模拟步长,如’0:15:00‘
|
||||
globals.hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"]
|
||||
hydraulic_timestep = dic_time["HYDRAULIC TIMESTEP"]
|
||||
# 转换为分钟浮点数,兼容 EPANET 的 H:MM 和 H:MM:SS 写法
|
||||
globals.PATTERN_TIME_STEP = (
|
||||
pattern_time_step = (
|
||||
parse_clock_duration_seconds(
|
||||
globals.hydraulic_timestep,
|
||||
hydraulic_timestep,
|
||||
field_name="HYDRAULIC TIMESTEP",
|
||||
)
|
||||
/ 60
|
||||
)
|
||||
project_scada = scada_mappings or load_realtime_element_mappings(name_c)
|
||||
# 对输入的时间参数进行处理
|
||||
pattern_start_time = convert_time_format(modify_pattern_start_time)
|
||||
# 获取模拟开始时间是对应pattern的第几个数
|
||||
modify_index = get_pattern_index(pattern_start_time)
|
||||
modify_index = get_pattern_index(pattern_start_time, pattern_time_step)
|
||||
# 遍历水泵的pattern_id,并根据输入的pump_pattern修改pattern的值
|
||||
# for pump_pattern_id in pump_pattern_ids:
|
||||
# # 检查pump_pattern中pump_pattern_id对应的第一个频率值是否为有效数字(非空、非NaN)。如果该值有效,则继续执行代码块。
|
||||
@@ -284,7 +264,7 @@ def run_simulation(
|
||||
# set_pattern(name_c, cs)
|
||||
# 修改模拟开始的时间
|
||||
str_pattern_start = get_pattern_index_str(
|
||||
convert_time_format(modify_pattern_start_time)
|
||||
convert_time_format(modify_pattern_start_time), pattern_time_step
|
||||
)
|
||||
dic_time = get_time(name_c)
|
||||
dic_time["PATTERN START"] = str_pattern_start
|
||||
@@ -295,18 +275,18 @@ def run_simulation(
|
||||
cs.operations.append(dic_time)
|
||||
set_time(name_c, cs)
|
||||
# 根据SCADA实时数据进行修改,如果没有对应的SCADA数据,如未来的时间点,则不改变pg数据库的数据
|
||||
if globals.reservoirs_id:
|
||||
if project_scada.reservoirs:
|
||||
# reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'}
|
||||
# 1.获取reservoir的SCADA数据,形式如{'2497': '3.1231', '2571': '2.7387'}
|
||||
reservoir_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
|
||||
device_ids=list(globals.reservoirs_id.values()),
|
||||
device_ids=list(project_scada.reservoirs.values()),
|
||||
query_time=modify_pattern_start_time,
|
||||
db_name=name,
|
||||
)
|
||||
# 2.构建出新字典,形式如{'ZBBDJSCP000002': '3.1231', 'R00003': '2.7387'}
|
||||
reservoir_dict = {
|
||||
key: reservoir_SCADA_data_dict[value]
|
||||
for key, value in globals.reservoirs_id.items()
|
||||
for key, value in project_scada.reservoirs.items()
|
||||
}
|
||||
# 3.修改reservoir液位模式
|
||||
for reservoir_name, value in reservoir_dict.items():
|
||||
@@ -316,20 +296,21 @@ def run_simulation(
|
||||
name_c, get_reservoir(name_c, reservoir_name)["pattern"]
|
||||
)
|
||||
reservoir_pattern["factors"][modify_index] = (
|
||||
float(value) + globals.RESERVOIR_BASIC_HEIGHT
|
||||
float(value) + RESERVOIR_BASIC_HEIGHT
|
||||
)
|
||||
cs = ChangeSet()
|
||||
cs.append(reservoir_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if globals.tanks_id:
|
||||
if project_scada.tanks:
|
||||
# 修改tank初始液位
|
||||
tank_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
|
||||
device_ids=list(globals.tanks_id.values()),
|
||||
device_ids=list(project_scada.tanks.values()),
|
||||
query_time=modify_pattern_start_time,
|
||||
db_name=name,
|
||||
)
|
||||
tank_dict = {
|
||||
key: tank_SCADA_data_dict[value] for key, value in globals.tanks_id.items()
|
||||
key: tank_SCADA_data_dict[value]
|
||||
for key, value in project_scada.tanks.items()
|
||||
}
|
||||
for tank_name, value in tank_dict.items():
|
||||
if value and float(value) != 0:
|
||||
@@ -338,17 +319,17 @@ def run_simulation(
|
||||
cs = ChangeSet()
|
||||
cs.append(tank)
|
||||
set_tank(name_c, cs)
|
||||
if globals.fixed_pumps_id:
|
||||
if project_scada.fixed_pumps:
|
||||
# 修改工频泵的pattern
|
||||
fixed_pump_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
|
||||
device_ids=list(globals.fixed_pumps_id.values()),
|
||||
device_ids=list(project_scada.fixed_pumps.values()),
|
||||
query_time=modify_pattern_start_time,
|
||||
db_name=name,
|
||||
)
|
||||
# print(fixed_pump_SCADA_data_dict)
|
||||
fixed_pump_dict = {
|
||||
key: fixed_pump_SCADA_data_dict[value]
|
||||
for key, value in globals.fixed_pumps_id.items()
|
||||
for key, value in project_scada.fixed_pumps.items()
|
||||
}
|
||||
# print(fixed_pump_dict)
|
||||
for fixed_pump_name, value in fixed_pump_dict.items():
|
||||
@@ -362,18 +343,18 @@ def run_simulation(
|
||||
cs = ChangeSet()
|
||||
cs.append(pump_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if globals.variable_pumps_id:
|
||||
if project_scada.variable_pumps:
|
||||
# 修改变频泵的pattern
|
||||
variable_pump_SCADA_data_dict = (
|
||||
TimescaleInternalQueries.query_scada_by_ids_time(
|
||||
device_ids=list(globals.variable_pumps_id.values()),
|
||||
device_ids=list(project_scada.variable_pumps.values()),
|
||||
query_time=modify_pattern_start_time,
|
||||
db_name=name,
|
||||
)
|
||||
)
|
||||
variable_pump_dict = {
|
||||
key: variable_pump_SCADA_data_dict[value]
|
||||
for key, value in globals.variable_pumps_id.items()
|
||||
for key, value in project_scada.variable_pumps.items()
|
||||
}
|
||||
for variable_pump_name, value in variable_pump_dict.items():
|
||||
if value:
|
||||
@@ -384,16 +365,16 @@ def run_simulation(
|
||||
cs = ChangeSet()
|
||||
cs.append(pump_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if globals.demand_id:
|
||||
if project_scada.demand:
|
||||
# 基于实时数据,修改大用户节点的pattern
|
||||
demand_SCADA_data_dict = TimescaleInternalQueries.query_scada_by_ids_time(
|
||||
device_ids=list(globals.demand_id.values()),
|
||||
device_ids=list(project_scada.demand.values()),
|
||||
query_time=modify_pattern_start_time,
|
||||
db_name=name,
|
||||
)
|
||||
demand_dict = {
|
||||
key: demand_SCADA_data_dict[value]
|
||||
for key, value in globals.demand_id.items()
|
||||
for key, value in project_scada.demand.items()
|
||||
}
|
||||
for demand_name, value in demand_dict.items():
|
||||
if value is not None and not np.isnan(float(value)):
|
||||
@@ -421,7 +402,7 @@ def run_simulation(
|
||||
if not np.isnan(modify_reservoir_head_pattern[reservoir_name][0]):
|
||||
# 给 list 中的所有元素加上 RESERVOIR_BASIC_HEIGHT
|
||||
modified_values = [
|
||||
value + globals.RESERVOIR_BASIC_HEIGHT
|
||||
value + RESERVOIR_BASIC_HEIGHT
|
||||
for value in modify_reservoir_head_pattern[reservoir_name]
|
||||
]
|
||||
reservoir_pattern = get_pattern(
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.algorithms.water_demand import (
|
||||
calculate_demand_to_nodes,
|
||||
calculate_demand_to_region,
|
||||
)
|
||||
from app.infra.db.postgresql.scada_assets import (
|
||||
from app.infra.db.postgresql.scada import (
|
||||
get_all_scada_info,
|
||||
get_scada_info,
|
||||
get_scada_info_schema,
|
||||
|
||||
Reference in New Issue
Block a user