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:
@@ -8,13 +8,13 @@ from uuid import UUID
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
||||
from app.algorithms.burst_detection.pressure_anomaly import PressureAnomalyDetector
|
||||
from app.infra.db.postgresql.scada import get_all_scada_info
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.services.scheme_management import (
|
||||
store_scheme_info,
|
||||
)
|
||||
from app.services.tjnetwork import get_all_scada_info
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
from app.domain.time import parse_utc_time, utc_now
|
||||
|
||||
|
||||
TARGET_DAY_COUNT = 15
|
||||
@@ -178,7 +178,7 @@ def run_burst_detection(
|
||||
observed_input = observed_pressure_data
|
||||
observed_source = "request_payload"
|
||||
|
||||
detector = BurstDetector(
|
||||
detector = PressureAnomalyDetector(
|
||||
mu=mu,
|
||||
points_per_day=points_per_day,
|
||||
iforest_params=iforest_params,
|
||||
|
||||
@@ -7,14 +7,15 @@ from uuid import UUID
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.algorithms.burst_location import run_burst_location
|
||||
from app.algorithms.burst_localization import run_burst_location
|
||||
from app.infra.db.postgresql.scada import get_all_scada_info
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.native.wndb.inp.exporter import dump_inp
|
||||
from app.services.scheme_management import (
|
||||
get_analysis_run,
|
||||
store_scheme_info,
|
||||
)
|
||||
from app.services.tjnetwork import dump_inp, get_all_scada_info
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
from app.domain.time import parse_utc_time, utc_now
|
||||
|
||||
SeriesInput = pd.Series | dict[str, Any] | list[dict[str, Any]]
|
||||
FLOW_SCADA_TYPES = {"pipe_flow", "flow", "demand"}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Application workflows for demand allocation."""
|
||||
|
||||
from app.algorithms.demand_allocation import allocate_demand_by_pipe_length
|
||||
from app.native.wndb.gis.region_geometry import Topology, get_nodes_in_region
|
||||
from app.native.wndb.model.elements import get_nodes
|
||||
|
||||
|
||||
def calculate_demand_to_nodes(
|
||||
project_code: str,
|
||||
demand: float,
|
||||
node_ids: list[str],
|
||||
) -> dict[str, float]:
|
||||
if not node_ids or demand == 0.0:
|
||||
return {}
|
||||
|
||||
topology = Topology(project_code, node_ids)
|
||||
return allocate_demand_by_pipe_length(
|
||||
demand,
|
||||
topology.nodes(),
|
||||
topology.links(),
|
||||
)
|
||||
|
||||
|
||||
def calculate_demand_to_region(
|
||||
project_code: str,
|
||||
demand: float,
|
||||
region_id: str,
|
||||
) -> dict[str, float]:
|
||||
node_ids = get_nodes_in_region(project_code, region_id)
|
||||
return calculate_demand_to_nodes(project_code, demand, node_ids)
|
||||
|
||||
|
||||
def calculate_demand_to_network(
|
||||
project_code: str,
|
||||
demand: float,
|
||||
) -> dict[str, float]:
|
||||
node_ids = get_nodes(project_code)
|
||||
return calculate_demand_to_nodes(project_code, demand, node_ids)
|
||||
@@ -1,23 +1,22 @@
|
||||
import math
|
||||
import os
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import wntr
|
||||
|
||||
from app.algorithms.leakage.identifier import LeakageIdentifier
|
||||
from app.algorithms.dma_leakage_estimation.genetic_optimizer import DmaLeakageOptimizer
|
||||
from app.algorithms.dma_leakage_estimation.topology_partitioning import (
|
||||
build_dma_partitions,
|
||||
)
|
||||
from app.infra.db.timescaledb.internal_queries import InternalQueries
|
||||
from app.services.scheme_management import store_analysis_run_with_result
|
||||
from app.services.tjnetwork import (
|
||||
dump_inp,
|
||||
get_all_scada_info,
|
||||
from app.infra.db.postgresql.scada import get_all_scada_info
|
||||
from app.native.wndb.gis.network_views import (
|
||||
get_network_link_nodes,
|
||||
get_network_node_coords,
|
||||
)
|
||||
from app.services.time_api import parse_utc_time, utc_now
|
||||
from app.native.wndb.inp.exporter import dump_inp
|
||||
from app.services.scheme_management import store_analysis_run_with_result
|
||||
from app.domain.time import parse_utc_time, utc_now
|
||||
|
||||
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
|
||||
|
||||
@@ -26,14 +25,13 @@ def run_leakage_identification(
|
||||
network: str,
|
||||
username: str,
|
||||
observed_pressure_data: (
|
||||
str | pd.DataFrame | dict[str, list[Any]] | list[dict[str, Any]] | None
|
||||
pd.DataFrame | dict[str, list[Any]] | list[dict[str, Any]] | None
|
||||
) = None,
|
||||
start_time: float = 0,
|
||||
duration: float = 24,
|
||||
timestep: float = 5,
|
||||
q_sum: float = 0.2,
|
||||
q_sum_unit: str = "m3/s",
|
||||
output_dir: str = "db_inp",
|
||||
pop_size: int = 50,
|
||||
max_gen: int = 100,
|
||||
n_workers: int = DEFAULT_N_WORKERS,
|
||||
@@ -44,7 +42,6 @@ def run_leakage_identification(
|
||||
sensor_nodes: list[str] | None = None,
|
||||
scheme_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
inp_path = _prepare_leakage_inp(network)
|
||||
|
||||
selected_sensor_nodes = (
|
||||
@@ -75,8 +72,8 @@ def run_leakage_identification(
|
||||
)
|
||||
observed_df = observed_pressure_data
|
||||
|
||||
q_sum_m3s = LeakageIdentifier._flow_to_m3s(q_sum, q_sum_unit)
|
||||
identifier = LeakageIdentifier(
|
||||
q_sum_m3s = DmaLeakageOptimizer._flow_to_m3s(q_sum, q_sum_unit)
|
||||
identifier = DmaLeakageOptimizer(
|
||||
inp_path=inp_path,
|
||||
sensor_nodes=selected_sensor_nodes,
|
||||
area_map=area_map,
|
||||
@@ -87,7 +84,6 @@ def run_leakage_identification(
|
||||
)
|
||||
result_df = identifier.run_identification(
|
||||
observed_pressure_data=observed_df,
|
||||
output_dir=output_dir,
|
||||
pop_size=pop_size,
|
||||
max_gen=max_gen,
|
||||
n_workers=n_workers,
|
||||
@@ -188,171 +184,15 @@ def _build_area_map_by_topology(
|
||||
network: str, sensor_nodes: list[str], dma_count: int | None
|
||||
) -> tuple[dict[str, str], list[dict[str, Any]], dict[str, dict[str, float]]]:
|
||||
node_coords = get_network_node_coords(network)
|
||||
all_nodes = list(node_coords.keys())
|
||||
if not all_nodes:
|
||||
raise ValueError("管网中未获取到可分区节点。")
|
||||
|
||||
available_sensors = [node for node in sensor_nodes if node in node_coords]
|
||||
if not available_sensors:
|
||||
raise ValueError("无可用压力传感器,无法生成虚拟分区。")
|
||||
area_count = _resolve_dma_count(dma_count, available_sensors, all_nodes)
|
||||
sensor_area_map = _cluster_sensors_to_areas(
|
||||
available_sensors, node_coords, area_count
|
||||
area_map, areas = build_dma_partitions(
|
||||
sensor_nodes,
|
||||
node_coords,
|
||||
get_network_link_nodes(network),
|
||||
dma_count,
|
||||
)
|
||||
adjacency = _build_adjacency(network, all_nodes)
|
||||
distance_by_sensor = {
|
||||
sensor: _bfs_distances(adjacency, sensor) for sensor in available_sensors
|
||||
}
|
||||
|
||||
assignment_count = {sensor: 0 for sensor in available_sensors}
|
||||
area_map: dict[str, str] = {}
|
||||
for node_id in sorted(all_nodes):
|
||||
sensor = _choose_sensor_for_node(
|
||||
node_id=node_id,
|
||||
sensors=available_sensors,
|
||||
node_coords=node_coords,
|
||||
distance_by_sensor=distance_by_sensor,
|
||||
assignment_count=assignment_count,
|
||||
)
|
||||
assignment_count[sensor] += 1
|
||||
area_map[node_id] = sensor_area_map[sensor]
|
||||
|
||||
if not area_map:
|
||||
raise ValueError("虚拟分区结果为空,无法生成节点区域映射。")
|
||||
|
||||
areas = _build_area_meta(area_map, sensor_area_map)
|
||||
return area_map, areas, node_coords
|
||||
|
||||
|
||||
def _resolve_dma_count(
|
||||
dma_count: int | None, sensor_nodes: list[str], all_nodes: list[str]
|
||||
) -> int:
|
||||
if dma_count is None:
|
||||
return min(len(sensor_nodes), len(all_nodes))
|
||||
if dma_count <= 0:
|
||||
raise ValueError("dma_count 必须大于 0。")
|
||||
if dma_count > len(all_nodes):
|
||||
raise ValueError("dma_count 不能大于可分区节点数量。")
|
||||
if dma_count > len(sensor_nodes):
|
||||
raise ValueError("dma_count 不能大于可用传感器数量。")
|
||||
return dma_count
|
||||
|
||||
|
||||
def _cluster_sensors_to_areas(
|
||||
sensor_nodes: list[str], node_coords: dict[str, dict[str, float]], area_count: int
|
||||
) -> dict[str, str]:
|
||||
if area_count >= len(sensor_nodes):
|
||||
return {sensor: str(i + 1) for i, sensor in enumerate(sensor_nodes)}
|
||||
|
||||
points = np.array(
|
||||
[
|
||||
[float(node_coords[s]["x"]), float(node_coords[s]["y"])]
|
||||
for s in sensor_nodes
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
centers = points[:area_count].copy()
|
||||
labels = np.zeros(points.shape[0], dtype=int)
|
||||
for _ in range(20):
|
||||
d2 = ((points[:, None, :] - centers[None, :, :]) ** 2).sum(axis=2)
|
||||
new_labels = d2.argmin(axis=1)
|
||||
if np.array_equal(labels, new_labels):
|
||||
break
|
||||
labels = new_labels
|
||||
for i in range(area_count):
|
||||
cluster_points = points[labels == i]
|
||||
if cluster_points.size > 0:
|
||||
centers[i] = cluster_points.mean(axis=0)
|
||||
return {
|
||||
sensor: str(int(labels[idx]) + 1) for idx, sensor in enumerate(sensor_nodes)
|
||||
}
|
||||
|
||||
|
||||
def _build_adjacency(network: str, all_nodes: list[str]) -> dict[str, set[str]]:
|
||||
adjacency: dict[str, set[str]] = {node: set() for node in all_nodes}
|
||||
for link in get_network_link_nodes(network):
|
||||
parts = str(link).split(":")
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
node1, node2 = parts[-2], parts[-1]
|
||||
if node1 in adjacency and node2 in adjacency:
|
||||
adjacency[node1].add(node2)
|
||||
adjacency[node2].add(node1)
|
||||
return adjacency
|
||||
|
||||
|
||||
def _bfs_distances(adjacency: dict[str, set[str]], start: str) -> dict[str, int]:
|
||||
distances: dict[str, int] = {start: 0}
|
||||
queue: deque[str] = deque([start])
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
for neighbor in adjacency.get(node, set()):
|
||||
if neighbor in distances:
|
||||
continue
|
||||
distances[neighbor] = distances[node] + 1
|
||||
queue.append(neighbor)
|
||||
return distances
|
||||
|
||||
|
||||
def _choose_sensor_for_node(
|
||||
node_id: str,
|
||||
sensors: list[str],
|
||||
node_coords: dict[str, dict[str, float]],
|
||||
distance_by_sensor: dict[str, dict[str, int]],
|
||||
assignment_count: dict[str, int],
|
||||
) -> str:
|
||||
min_distance = None
|
||||
candidates: list[str] = []
|
||||
for sensor in sensors:
|
||||
d = distance_by_sensor.get(sensor, {}).get(node_id)
|
||||
if d is None:
|
||||
continue
|
||||
if min_distance is None or d < min_distance:
|
||||
min_distance = d
|
||||
candidates = [sensor]
|
||||
elif d == min_distance:
|
||||
candidates.append(sensor)
|
||||
if not candidates:
|
||||
node_coord = node_coords[node_id]
|
||||
return min(
|
||||
sensors,
|
||||
key=lambda sensor: _euclidean_distance(
|
||||
node_coord, node_coords.get(sensor, node_coord)
|
||||
),
|
||||
)
|
||||
return min(candidates, key=lambda sensor: (assignment_count[sensor], sensor))
|
||||
|
||||
|
||||
def _euclidean_distance(a: dict[str, float], b: dict[str, float]) -> float:
|
||||
return math.hypot(float(a["x"]) - float(b["x"]), float(a["y"]) - float(b["y"]))
|
||||
|
||||
|
||||
def _build_area_meta(
|
||||
area_map: dict[str, str], sensor_area_map: dict[str, str]
|
||||
) -> list[dict[str, Any]]:
|
||||
nodes_by_area: dict[str, list[str]] = {}
|
||||
for node_id, area_id in area_map.items():
|
||||
nodes_by_area.setdefault(area_id, []).append(node_id)
|
||||
|
||||
sensors_by_area: dict[str, list[str]] = {}
|
||||
for sensor, area_id in sensor_area_map.items():
|
||||
sensors_by_area.setdefault(area_id, []).append(sensor)
|
||||
|
||||
areas: list[dict[str, Any]] = []
|
||||
for area_id in sorted(nodes_by_area.keys(), key=lambda x: int(x)):
|
||||
node_ids = sorted(nodes_by_area.get(area_id, []))
|
||||
sensor_nodes = sorted(sensors_by_area.get(area_id, []))
|
||||
areas.append(
|
||||
{
|
||||
"area_id": area_id,
|
||||
"sensor_nodes": sensor_nodes,
|
||||
"node_ids": node_ids,
|
||||
"node_count": len(node_ids),
|
||||
}
|
||||
)
|
||||
return areas
|
||||
|
||||
|
||||
def _build_area_node_map(area_map: dict[str, str]) -> dict[str, list[str]]:
|
||||
area_node_map: dict[str, list[str]] = {}
|
||||
for node_id, area_id in area_map.items():
|
||||
@@ -1,4 +1,4 @@
|
||||
from app.services.tjnetwork import read_inp
|
||||
from app.native.wndb.inp.importer import read_inp
|
||||
|
||||
|
||||
def network_update(file_path: str, project_code: str) -> None:
|
||||
|
||||
@@ -4,7 +4,7 @@ from uuid import UUID, uuid4
|
||||
|
||||
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
|
||||
from app.domain.time import parse_utc_time
|
||||
|
||||
|
||||
def store_scheme_info(
|
||||
|
||||
@@ -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": "当前方案",
|
||||
|
||||
+13
-26
@@ -1,41 +1,28 @@
|
||||
import numpy as np
|
||||
from app.services.tjnetwork import (
|
||||
ChangeSet,
|
||||
get_demand,
|
||||
get_option,
|
||||
get_pattern,
|
||||
get_pump,
|
||||
get_reservoir,
|
||||
get_status,
|
||||
get_tank,
|
||||
get_time,
|
||||
read_all,
|
||||
run_project,
|
||||
set_demand,
|
||||
set_pattern,
|
||||
set_status,
|
||||
set_tank,
|
||||
set_time,
|
||||
)
|
||||
from app.infra.epanet import run_project
|
||||
from app.native.wndb.core.database import ChangeSet
|
||||
from app.native.wndb.model.demands import get_demand, set_demand
|
||||
from app.native.wndb.model.options import get_option
|
||||
from app.native.wndb.model.patterns import get_pattern, set_pattern
|
||||
from app.native.wndb.model.pumps import get_pump
|
||||
from app.native.wndb.model.reservoirs import get_reservoir
|
||||
from app.native.wndb.model.status import get_status, set_status
|
||||
from app.native.wndb.model.tanks import get_tank, set_tank
|
||||
from app.native.wndb.model.times import get_time, set_time
|
||||
|
||||
# from get_real_status import *
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime
|
||||
from math import modf
|
||||
import os
|
||||
import json
|
||||
import pytz
|
||||
import requests
|
||||
import time
|
||||
from typing import Optional, Tuple
|
||||
from uuid import UUID
|
||||
import typing
|
||||
import logging
|
||||
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.domain.time import parse_beijing_time, parse_clock_duration_seconds
|
||||
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 (
|
||||
@@ -479,7 +466,7 @@ def run_simulation(
|
||||
cs = ChangeSet()
|
||||
cs.append(pump_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
# 显式阀门控制沿用 run_simulation_ex 的处理顺序和覆盖规则。
|
||||
# 显式阀门控制优先于旧的开度参数。
|
||||
if valve_control is not None:
|
||||
_apply_valve_control(name_c, valve_control)
|
||||
# 保留原开度参数逻辑,兼容现有方案调用。
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from math import pi
|
||||
|
||||
import pytz
|
||||
|
||||
from app.algorithms.simulation.runner import run_simulation_ex
|
||||
from app.native.wndb.core.projects import temporary_project_database
|
||||
from app.services.tjnetwork import (
|
||||
get_pipe,
|
||||
get_tank,
|
||||
)
|
||||
|
||||
|
||||
def _isolated_operation(purpose: str):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(prj_name: str, *args, **kwargs):
|
||||
with temporary_project_database(prj_name, purpose) as temporary:
|
||||
kwargs["_temporary_project"] = temporary
|
||||
return func(prj_name, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
############################################################
|
||||
# project management 07 ***暂时不使用,与业务需求无关***
|
||||
############################################################
|
||||
|
||||
|
||||
@_isolated_operation("project_management")
|
||||
def project_management(
|
||||
prj_name,
|
||||
start_datetime,
|
||||
pump_control,
|
||||
tank_initial_level_control=None,
|
||||
region_demand_control=None,
|
||||
_temporary_project=None,
|
||||
) -> str:
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Project-management isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
result = run_simulation_ex(
|
||||
name=new_name,
|
||||
simulation_type="realtime",
|
||||
start_datetime=start_datetime,
|
||||
duration=86400,
|
||||
pump_control=pump_control,
|
||||
tank_initial_level_control=tank_initial_level_control,
|
||||
region_demand_control=region_demand_control,
|
||||
downloading_prohibition=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
############################################################
|
||||
# scheduling analysis 08 ***暂时不使用,与业务需求无关***
|
||||
############################################################
|
||||
|
||||
|
||||
@_isolated_operation("scheduling")
|
||||
def scheduling_simulation(
|
||||
prj_name,
|
||||
start_time,
|
||||
pump_control,
|
||||
tank_id,
|
||||
water_plant_output_id,
|
||||
time_delta=300,
|
||||
_temporary_project=None,
|
||||
) -> str:
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Scheduling isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
|
||||
simulation_result = json.loads(
|
||||
run_simulation_ex(
|
||||
new_name, "realtime", start_time, duration=0, pump_control=pump_control
|
||||
)
|
||||
)
|
||||
|
||||
output_data = simulation_result.get("output")
|
||||
if not isinstance(output_data, dict):
|
||||
raise RuntimeError("run_simulation_ex did not return JSON output content")
|
||||
|
||||
tank = get_tank(new_name, tank_id) # 水塔信息
|
||||
tank_floor_space = pi * pow(tank["diameter"] / 2, 2) # 水塔底面积(m^2)
|
||||
tank_init_level = tank["init_level"] # 水塔初始水位(m)
|
||||
tank_pipes_id = tank["links"] # pipes list
|
||||
|
||||
tank_pipe_flow_direction = (
|
||||
{}
|
||||
) # 管道流向修正系数, 水塔为下游节点时为1, 水塔为上游节点时为-1
|
||||
for pipe_id in tank_pipes_id:
|
||||
if get_pipe(new_name, pipe_id)["node2"] == tank_id: # 水塔为下游节点
|
||||
tank_pipe_flow_direction[pipe_id] = 1
|
||||
else:
|
||||
tank_pipe_flow_direction[pipe_id] = -1
|
||||
|
||||
node_results = output_data.get("node_results") or [] # [{'node': str, 'result': [{'pressure': float}]}]
|
||||
water_plant_output_pressure = 0
|
||||
for node_result in node_results:
|
||||
if node_result["node"] == water_plant_output_id: # 水厂出水压力(m)
|
||||
water_plant_output_pressure = node_result["result"][-1]["pressure"]
|
||||
water_plant_output_pressure /= 100 # 预计水厂出水压力(Mpa)
|
||||
|
||||
pipe_results = output_data.get("link_results") or [] # [{'link': str, 'result': [{'flow': float}]}]
|
||||
tank_inflow = 0
|
||||
for pipe_result in pipe_results:
|
||||
for pipe_id in tank_pipes_id: # 遍历与水塔相连的管道
|
||||
if pipe_result["link"] == pipe_id: # 水塔入流流量(L/s)
|
||||
tank_inflow += (
|
||||
pipe_result["result"][-1]["flow"]
|
||||
* tank_pipe_flow_direction[pipe_id]
|
||||
)
|
||||
tank_inflow /= 1000 # 水塔入流流量(m^3/s)
|
||||
tank_level_delta = tank_inflow * time_delta / tank_floor_space # 水塔水位改变值(m)
|
||||
tank_level = tank_init_level + tank_level_delta # 预计水塔水位(m)
|
||||
|
||||
simulation_results = {
|
||||
"water_plant_output_pressure": water_plant_output_pressure,
|
||||
"tank_init_level": tank_init_level,
|
||||
"tank_level": tank_level,
|
||||
}
|
||||
|
||||
return json.dumps(simulation_results)
|
||||
|
||||
|
||||
@_isolated_operation("daily_scheduling")
|
||||
def daily_scheduling_simulation(
|
||||
prj_name,
|
||||
start_time,
|
||||
pump_control,
|
||||
reservoir_id,
|
||||
tank_id,
|
||||
water_plant_output_id,
|
||||
_temporary_project=None,
|
||||
) -> str:
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Daily-scheduling isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
|
||||
simulation_result = json.loads(
|
||||
run_simulation_ex(
|
||||
new_name,
|
||||
"realtime",
|
||||
start_time,
|
||||
duration=86400,
|
||||
pump_control=pump_control,
|
||||
)
|
||||
)
|
||||
|
||||
output_data = simulation_result.get("output")
|
||||
if not isinstance(output_data, dict):
|
||||
raise RuntimeError("run_simulation_ex did not return JSON output content")
|
||||
|
||||
node_results = output_data.get("node_results") or [] # [{'node': str, 'result': [{'pressure': float, 'head': float}]}]
|
||||
water_plant_output_pressure = []
|
||||
reservoir_level = []
|
||||
tank_level = []
|
||||
for node_result in node_results:
|
||||
if node_result["node"] == water_plant_output_id:
|
||||
for result in node_result["result"]:
|
||||
water_plant_output_pressure.append(
|
||||
result["pressure"] / 100
|
||||
) # 水厂出水压力(Mpa)
|
||||
elif node_result["node"] == reservoir_id:
|
||||
for result in node_result["result"]:
|
||||
reservoir_level.append(result["head"] - 250.35) # 清水池液位(m)
|
||||
elif node_result["node"] == tank_id:
|
||||
for result in node_result["result"]:
|
||||
tank_level.append(result["pressure"]) # 调节池液位(m)
|
||||
|
||||
simulation_results = {
|
||||
"water_plant_output_pressure": water_plant_output_pressure,
|
||||
"reservoir_level": reservoir_level,
|
||||
"tank_level": tank_level,
|
||||
}
|
||||
|
||||
return json.dumps(simulation_results)
|
||||
@@ -0,0 +1,661 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from math import pi, sqrt
|
||||
|
||||
import pytz
|
||||
|
||||
import app.services.simulation as simulation
|
||||
from app.native.wndb.core.projects import temporary_project_database
|
||||
from app.native.wndb.core.database import ChangeSet
|
||||
from app.native.wndb.model.demands import get_demand, set_demand
|
||||
from app.native.wndb.model.elements import get_node_links, is_junction
|
||||
from app.native.wndb.model.emitters import get_emitter, set_emitter
|
||||
from app.native.wndb.model.options import (
|
||||
OPTION_DEMAND_MODEL_PDA,
|
||||
OPTION_QUALITY_CHEMICAL,
|
||||
get_option,
|
||||
set_option,
|
||||
)
|
||||
from app.native.wndb.model.patterns import add_pattern, get_pattern
|
||||
from app.native.wndb.model.pipes import get_pipe
|
||||
from app.native.wndb.model.sources import (
|
||||
SOURCE_TYPE_SETPOINT,
|
||||
add_source,
|
||||
get_source,
|
||||
set_source,
|
||||
)
|
||||
from app.native.wndb.model.times import get_time, set_time
|
||||
from app.domain.time import parse_clock_duration_seconds
|
||||
|
||||
|
||||
def _isolated_analysis(purpose: str):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(name: str, *args, **kwargs):
|
||||
with temporary_project_database(name, purpose) as temporary:
|
||||
kwargs["_temporary_project"] = temporary
|
||||
return func(name, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
############################################################
|
||||
# burst analysis 01
|
||||
############################################################
|
||||
def convert_to_local_unit(proj: str, emitters: float) -> float:
|
||||
proj_opt = get_option(proj)
|
||||
str_unit = proj_opt.get("UNITS")
|
||||
|
||||
if str_unit == "CMH":
|
||||
return emitters * 3.6
|
||||
elif str_unit == "LPS":
|
||||
return emitters
|
||||
elif str_unit == "CMS":
|
||||
return emitters / 1000.0
|
||||
elif str_unit == "MGD":
|
||||
return emitters * 0.0438126
|
||||
|
||||
# Unknown unit: log and return original value
|
||||
print(str_unit)
|
||||
return emitters
|
||||
|
||||
|
||||
@_isolated_analysis("burst_analysis")
|
||||
def burst_analysis(
|
||||
name: str,
|
||||
modify_pattern_start_time: str,
|
||||
burst_ID: list | str = None,
|
||||
burst_size: list | float | int = None,
|
||||
modify_total_duration: int = 900,
|
||||
modify_fixed_pump_pattern: dict[str, list] = None,
|
||||
modify_variable_pump_pattern: dict[str, list] = None,
|
||||
modify_valve_opening: dict[str, float] = None,
|
||||
scheme_name: str = None,
|
||||
username: str | None = None,
|
||||
_temporary_project: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
爆管模拟
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param burst_ID: 爆管管道的ID,选取的是管道,单独传入一个爆管管道,可以是str或list,传入多个爆管管道是用list
|
||||
:param burst_size: 爆管管道破裂的孔口面积,和burst_ID列表各位置的ID对应,以cm*cm计算
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern
|
||||
:param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern
|
||||
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing burst analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"burst_ID": burst_ID,
|
||||
"burst_size": burst_size,
|
||||
"modify_total_duration": modify_total_duration,
|
||||
"modify_fixed_pump_pattern": modify_fixed_pump_pattern,
|
||||
"modify_variable_pump_pattern": modify_variable_pump_pattern,
|
||||
"modify_valve_opening": modify_valve_opening,
|
||||
}
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Burst analysis isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="manually_temporary",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
##step 1 set the emitter coefficient of end node of busrt pipe
|
||||
if isinstance(burst_ID, list):
|
||||
if (burst_size is not None) and (type(burst_size) is not list):
|
||||
return json.dumps("Type mismatch.")
|
||||
# 转化为列表形式
|
||||
elif isinstance(burst_ID, str):
|
||||
burst_ID = [burst_ID]
|
||||
if burst_size is not None:
|
||||
if isinstance(burst_size, float) or isinstance(burst_size, int):
|
||||
burst_size = [burst_size]
|
||||
else:
|
||||
return json.dumps("Type mismatch.")
|
||||
else:
|
||||
return json.dumps("Type mismatch.")
|
||||
if burst_size is None:
|
||||
burst_size = [-1] * len(burst_ID)
|
||||
elif len(burst_size) < len(burst_ID):
|
||||
burst_size += [-1] * (len(burst_ID) - len(burst_size))
|
||||
elif len(burst_size) > len(burst_ID):
|
||||
# burst_size = burst_size[:len(burst_ID)]
|
||||
return json.dumps("Length mismatch.")
|
||||
for burst_ID_, burst_size_ in zip(burst_ID, burst_size):
|
||||
pipe = get_pipe(new_name, burst_ID_)
|
||||
str_start_node = pipe["node1"]
|
||||
str_end_node = pipe["node2"]
|
||||
d_pipe = pipe["diameter"] / 1000.0
|
||||
if burst_size_ <= 0:
|
||||
burst_size_ = 3.14 * d_pipe * d_pipe / 4 / 8
|
||||
else:
|
||||
burst_size_ = burst_size_ / 10000
|
||||
emitter_coeff = (
|
||||
0.65 * burst_size_ * sqrt(19.6) * 1000
|
||||
) # 1/8开口面积作为coeff,单位 L/S
|
||||
emitter_coeff = convert_to_local_unit(new_name, emitter_coeff)
|
||||
emitter_node = ""
|
||||
if is_junction(new_name, str_end_node):
|
||||
emitter_node = str_end_node
|
||||
elif is_junction(new_name, str_start_node):
|
||||
emitter_node = str_start_node
|
||||
old_emitter = get_emitter(new_name, emitter_node)
|
||||
if old_emitter != None:
|
||||
old_emitter["coefficient"] = emitter_coeff # 爆管的emitter coefficient设置
|
||||
else:
|
||||
old_emitter = {"junction": emitter_node, "coefficient": emitter_coeff}
|
||||
new_emitter = ChangeSet()
|
||||
new_emitter.append(old_emitter)
|
||||
set_emitter(new_name, new_emitter)
|
||||
# step 2. run simulation
|
||||
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
|
||||
options = get_option(new_name)
|
||||
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
|
||||
options["REQUIRED PRESSURE"] = "10.0000"
|
||||
cs_options = ChangeSet()
|
||||
cs_options.append(options)
|
||||
set_option(new_name, cs_options)
|
||||
# valve_control = None
|
||||
# if modify_valve_opening is not None:
|
||||
# valve_control = {}
|
||||
# for valve in modify_valve_opening:
|
||||
# valve_control[valve] = {'status': 'CLOSED'}
|
||||
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time,
|
||||
# end_datetime=modify_pattern_start_time,
|
||||
# modify_total_duration=modify_total_duration,
|
||||
# modify_pump_pattern=modify_pump_pattern,
|
||||
# valve_control=valve_control,
|
||||
# downloading_prohibition=True)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="extended",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
modify_fixed_pump_pattern=modify_fixed_pump_pattern,
|
||||
modify_variable_pump_pattern=modify_variable_pump_pattern,
|
||||
modify_valve_opening=modify_valve_opening,
|
||||
scheme_type="burst_analysis",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
scheme_username=username,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
# step 3. restore the base model status
|
||||
# execute_undo(name) #有疑惑
|
||||
|
||||
|
||||
############################################################
|
||||
# valve closing analysis 02
|
||||
############################################################
|
||||
@_isolated_analysis("valve_close_analysis")
|
||||
def valve_close_analysis(
|
||||
name: str,
|
||||
modify_pattern_start_time: str,
|
||||
modify_total_duration: int = 900,
|
||||
modify_valve_opening: dict[str, float] = None,
|
||||
scheme_name: str = None,
|
||||
_temporary_project: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
关阀模拟
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Valve-close analysis isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
# step 1. change the valves status to 'closed'
|
||||
# for valve in valves:
|
||||
# if not is_valve(new_name,valve):
|
||||
# result='ID:{}is not a valve type'.format(valve)
|
||||
# return result
|
||||
# cs=ChangeSet()
|
||||
# status=get_status(new_name,valve)
|
||||
# status['status']='CLOSED'
|
||||
# cs.append(status)
|
||||
# set_status(new_name,cs)
|
||||
# step 2. run simulation
|
||||
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
|
||||
options = get_option(new_name)
|
||||
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
|
||||
options["REQUIRED PRESSURE"] = "20.0000"
|
||||
cs_options = ChangeSet()
|
||||
cs_options.append(options)
|
||||
set_option(new_name, cs_options)
|
||||
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
|
||||
# downloading_prohibition=True)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="extended",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
modify_valve_opening=modify_valve_opening,
|
||||
scheme_type="valve_close_analysis",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
)
|
||||
# step 3. restore the base model
|
||||
# for valve in valves:
|
||||
# execute_undo(name)
|
||||
# return result
|
||||
|
||||
|
||||
############################################################
|
||||
# flushing analysis 03
|
||||
# Pipe_Flushing_Analysis(prj_name,date_time, Valve_id_list, Drainage_Node_Id, Flushing_flow[opt], Flushing_duration[opt])->out_file:string
|
||||
############################################################
|
||||
@_isolated_analysis("flushing_analysis")
|
||||
def flushing_analysis(
|
||||
name: str,
|
||||
modify_pattern_start_time: str,
|
||||
modify_total_duration: int = 900,
|
||||
modify_valve_opening: dict[str, float] = None,
|
||||
drainage_node_ID: str = None,
|
||||
flushing_flow: float = 0,
|
||||
scheme_name: str = None,
|
||||
username: str | None = None,
|
||||
valve_control: dict[str, dict] = None,
|
||||
_temporary_project: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
管道冲洗模拟
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
|
||||
:param valve_control: dict中可分别指定阀门的status、setting和k
|
||||
:param drainage_node_ID: 冲洗排放口所在节点ID
|
||||
:param flushing_flow: 冲洗水量,传入参数单位为m3/h
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing flushing analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"duration": modify_total_duration,
|
||||
"valve_opening": modify_valve_opening,
|
||||
"valve_control": valve_control,
|
||||
"drainage_node_ID": drainage_node_ID,
|
||||
"flushing_flow": flushing_flow,
|
||||
}
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Flushing analysis isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
if not is_junction(new_name, drainage_node_ID):
|
||||
return "Wrong Drainage node type"
|
||||
# step 1. change the valves status to 'closed'
|
||||
# for valve, valve_k in zip(valves, valves_k):
|
||||
# cs=ChangeSet()
|
||||
# status=get_status(new_name,valve)
|
||||
# # status['status']='CLOSED'
|
||||
# if valve_k == 0:
|
||||
# status['status'] = 'CLOSED'
|
||||
# elif valve_k < 1:
|
||||
# status['status'] = 'OPEN'
|
||||
# status['setting'] = 0.1036 * pow(valve_k, -3.105)
|
||||
# cs.append(status)
|
||||
# set_status(new_name,cs)
|
||||
options = get_option(new_name)
|
||||
units = options["UNITS"]
|
||||
# step 2. set the emitter coefficient of drainage node or add flush flow to the drainage node
|
||||
# 新建 pattern
|
||||
time_option = get_time(new_name)
|
||||
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
|
||||
secs = parse_clock_duration_seconds(hydraulic_step)
|
||||
cs_pattern = ChangeSet()
|
||||
pt = {}
|
||||
factors = []
|
||||
tmp_duration = modify_total_duration
|
||||
while tmp_duration > 0:
|
||||
factors.append(1.0)
|
||||
tmp_duration = tmp_duration - secs
|
||||
pt["id"] = "flushing_pt"
|
||||
pt["factors"] = factors
|
||||
cs_pattern.append(pt)
|
||||
add_pattern(new_name, cs_pattern)
|
||||
# 为 emitter_demand 添加新的 pattern
|
||||
emitter_demand = get_demand(new_name, drainage_node_ID)
|
||||
cs = ChangeSet()
|
||||
if flushing_flow > 0:
|
||||
if units == "LPS":
|
||||
emitter_demand["demands"].append(
|
||||
{
|
||||
"demand": flushing_flow / 3.6,
|
||||
"pattern": "flushing_pt",
|
||||
"category": None,
|
||||
}
|
||||
)
|
||||
elif units == "CMH":
|
||||
emitter_demand["demands"].append(
|
||||
{"demand": flushing_flow, "pattern": "flushing_pt", "category": None}
|
||||
)
|
||||
cs.append(emitter_demand)
|
||||
set_demand(new_name, cs)
|
||||
else:
|
||||
pipes = get_node_links(new_name, drainage_node_ID)
|
||||
flush_diameter = 50
|
||||
for pipe in pipes:
|
||||
d = get_pipe(new_name, pipe)["diameter"]
|
||||
if flush_diameter < d:
|
||||
flush_diameter = d
|
||||
flush_diameter /= 1000
|
||||
emitter_coeff = (
|
||||
0.65 * 3.14 * (flush_diameter * flush_diameter / 4) * sqrt(19.6) * 1000
|
||||
) # 全开口面积作为coeff
|
||||
|
||||
old_emitter = get_emitter(new_name, drainage_node_ID)
|
||||
if old_emitter != None:
|
||||
old_emitter["coefficient"] = emitter_coeff # 爆管的emitter coefficient设置
|
||||
else:
|
||||
old_emitter = {"junction": drainage_node_ID, "coefficient": emitter_coeff}
|
||||
new_emitter = ChangeSet()
|
||||
new_emitter.append(old_emitter)
|
||||
set_emitter(new_name, new_emitter)
|
||||
# step 3. run simulation
|
||||
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
|
||||
options = get_option(new_name)
|
||||
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
|
||||
options["REQUIRED PRESSURE"] = "20.0000"
|
||||
cs_options = ChangeSet()
|
||||
cs_options.append(options)
|
||||
set_option(new_name, cs_options)
|
||||
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
|
||||
# downloading_prohibition=True)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="extended",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
modify_valve_opening=modify_valve_opening,
|
||||
valve_control=valve_control,
|
||||
scheme_type="flushing_analysis",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
scheme_username=username,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
# step 4. restore the base model
|
||||
# return result
|
||||
|
||||
|
||||
############################################################
|
||||
# Contaminant simulation 04
|
||||
#
|
||||
############################################################
|
||||
@_isolated_analysis("contaminant_simulation")
|
||||
def contaminant_simulation(
|
||||
name: str,
|
||||
modify_pattern_start_time: str, # 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
modify_total_duration: int, # 模拟总历时,秒
|
||||
source: str, # 污染源节点ID
|
||||
concentration: float, # 污染源浓度,单位mg/L
|
||||
scheme_name: str = None,
|
||||
source_pattern: str = None, # 污染源时间变化模式名称
|
||||
username: str | None = None,
|
||||
_temporary_project: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
污染模拟
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param source: 污染源所在的节点ID
|
||||
:param concentration: 污染源位置处的浓度,单位mg/L。默认的污染模拟setting为SOURCE_TYPE_CONCEN(改为SOURCE_TYPE_SETPOINT)
|
||||
:param source_pattern: 污染源的时间变化模式,若不传入则默认以恒定浓度持续模拟,时间长度等于duration;
|
||||
若传入,则格式为{1.0,0.5,1.1}等系数列表pattern_step模拟等于模型的hydraulic time step
|
||||
:param scheme_name: 方案名称
|
||||
:return:
|
||||
"""
|
||||
if not username:
|
||||
raise ValueError("username is required when storing contaminant analysis scheme")
|
||||
|
||||
scheme_detail: dict = {
|
||||
"source": source,
|
||||
"concentration": concentration,
|
||||
"duration": modify_total_duration,
|
||||
"pattern": source_pattern,
|
||||
}
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Contaminant simulation isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
dic_time = get_time(new_name)
|
||||
dic_time["QUALITY TIMESTEP"] = "0:05:00"
|
||||
cs = ChangeSet()
|
||||
cs.operations.append(dic_time)
|
||||
set_time(new_name, cs) # set QUALITY TIMESTEP
|
||||
time_option = get_time(new_name)
|
||||
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
|
||||
secs = parse_clock_duration_seconds(hydraulic_step)
|
||||
operation_step = 0
|
||||
# step 1. set duration
|
||||
if modify_total_duration == None:
|
||||
modify_total_duration = secs
|
||||
# step 2. set pattern
|
||||
if source_pattern != None:
|
||||
pt = get_pattern(new_name, source_pattern)
|
||||
if len(pt) == 0:
|
||||
str_response = str("cant find source_pattern")
|
||||
return str_response
|
||||
else:
|
||||
cs_pattern = ChangeSet()
|
||||
pt = {}
|
||||
factors = []
|
||||
tmp_duration = modify_total_duration
|
||||
while tmp_duration > 0:
|
||||
factors.append(1.0)
|
||||
tmp_duration = tmp_duration - secs
|
||||
pt["id"] = "contam_pt"
|
||||
pt["factors"] = factors
|
||||
cs_pattern.append(pt)
|
||||
add_pattern(new_name, cs_pattern)
|
||||
operation_step += 1
|
||||
# step 3. set source/initial quality
|
||||
# source quality
|
||||
cs_source = ChangeSet()
|
||||
source_schema = {
|
||||
"node": source,
|
||||
"s_type": SOURCE_TYPE_SETPOINT,
|
||||
"strength": concentration,
|
||||
"pattern": pt["id"],
|
||||
}
|
||||
cs_source.append(source_schema)
|
||||
source_node = get_source(new_name, source)
|
||||
if len(source_node) == 0:
|
||||
add_source(new_name, cs_source)
|
||||
else:
|
||||
set_source(new_name, cs_source)
|
||||
dict_demand = get_demand(new_name, source)
|
||||
for demands in dict_demand["demands"]:
|
||||
dict_demand["demands"][dict_demand["demands"].index(demands)]["demand"] = -1
|
||||
dict_demand["demands"][dict_demand["demands"].index(demands)]["pattern"] = None
|
||||
cs = ChangeSet()
|
||||
cs.append(dict_demand)
|
||||
set_demand(new_name, cs) # set inflow node
|
||||
# # initial quality
|
||||
# dict_quality = get_quality(new_name, source)
|
||||
# dict_quality['quality'] = concentration
|
||||
# cs = ChangeSet()
|
||||
# cs.append(dict_quality)
|
||||
# set_quality(new_name, cs)
|
||||
operation_step += 1
|
||||
# step 4 set option of quality to chemical
|
||||
opt = get_option(new_name)
|
||||
opt["QUALITY"] = OPTION_QUALITY_CHEMICAL
|
||||
cs_option = ChangeSet()
|
||||
cs_option.append(opt)
|
||||
set_option(new_name, cs_option)
|
||||
operation_step += 1
|
||||
# step 5. run simulation
|
||||
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
|
||||
# downloading_prohibition=True)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="extended",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
scheme_type="contaminant_analysis",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
scheme_username=username,
|
||||
scheme_detail=scheme_detail,
|
||||
)
|
||||
|
||||
# for i in range(1,operation_step):
|
||||
# execute_undo(name)
|
||||
|
||||
|
||||
############################################################
|
||||
# pressure regulation 06
|
||||
############################################################
|
||||
|
||||
|
||||
@_isolated_analysis("pressure_regulation")
|
||||
def pressure_regulation(
|
||||
name: str,
|
||||
modify_pattern_start_time: str,
|
||||
modify_total_duration: int = 900,
|
||||
modify_tank_initial_level: dict[str, float] = None,
|
||||
modify_fixed_pump_pattern: dict[str, list] = None,
|
||||
modify_variable_pump_pattern: dict[str, list] = None,
|
||||
scheme_name: str = None,
|
||||
scada_mappings: simulation.ScadaElementMappings | None = None,
|
||||
_temporary_project: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
区域调压模拟,用来模拟未来15分钟内,开关水泵对区域压力的影响
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:param modify_tank_initial_level: dict中包含多个水塔,str为水塔的id,float为修改后的initial_level
|
||||
:param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern
|
||||
:param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern
|
||||
:param scheme_name: 模拟方案名称
|
||||
:return:
|
||||
"""
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Pressure-regulation isolation was not prepared")
|
||||
new_name = _temporary_project
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Copying Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Opening Database."
|
||||
)
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Database Loading OK."
|
||||
)
|
||||
# 全部关泵后,压力计算不合理,改为压力驱动PDA
|
||||
options = get_option(new_name)
|
||||
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
|
||||
options["REQUIRED PRESSURE"] = "15.0000"
|
||||
cs_options = ChangeSet()
|
||||
cs_options.append(options)
|
||||
set_option(new_name, cs_options)
|
||||
# result = run_simulation_ex(name=new_name,
|
||||
# simulation_type='realtime',
|
||||
# start_datetime=start_datetime,
|
||||
# duration=900,
|
||||
# pump_control=pump_control,
|
||||
# tank_initial_level_control=tank_initial_level_control,
|
||||
# downloading_prohibition=True)
|
||||
simulation.run_simulation(
|
||||
name=new_name,
|
||||
simulation_type="extended",
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
modify_total_duration=modify_total_duration,
|
||||
modify_tank_initial_level=modify_tank_initial_level,
|
||||
modify_fixed_pump_pattern=modify_fixed_pump_pattern,
|
||||
modify_variable_pump_pattern=modify_variable_pump_pattern,
|
||||
scheme_type="pressure_regulation",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
scada_mappings=scada_mappings,
|
||||
)
|
||||
# return result
|
||||
@@ -1,189 +0,0 @@
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
|
||||
from dateutil import parser, tz
|
||||
|
||||
'''
|
||||
2025-02-09T15:45:00+00:00 采用的是 ISO 8601 国际标准日期时间格式,具体特点如下:
|
||||
|
||||
日期部分:YYYY-MM-DD(年-月-日),例如2025-02-09表示2025年2月9日。
|
||||
时间部分:HH:mm:ss(时:分:秒),例如15:45:00表示下午3点45分0秒。
|
||||
分隔符:日期与时间之间用字母T连接,表明这是一个完整的时间点。
|
||||
时区偏移:末尾的+00:00表示该时间基于协调世界时(UTC),即零时区。若使用Z替代+00:00(如2025-02-09T15:45:00Z),也符合ISO 8601标准,两者等价
|
||||
|
||||
北京时间格式
|
||||
2025-02-09T15:45:00+08:00
|
||||
|
||||
'''
|
||||
BG_TZ = tz.gettz("Asia/Shanghai")
|
||||
UTC_TZ = timezone.utc
|
||||
|
||||
TIMEZONE_REQUIRED_MESSAGE = (
|
||||
"Datetime values must include an explicit timezone offset, for example "
|
||||
"'2025-02-09T15:45:00Z' or '2025-02-09T23:45:00+08:00'."
|
||||
)
|
||||
|
||||
|
||||
def parse_aware_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
|
||||
"""
|
||||
解析时间并确保结果带有时区信息。
|
||||
"""
|
||||
dt = parser.parse(query_time) if isinstance(query_time, str) else query_time
|
||||
if dt.tzinfo is None:
|
||||
raise ValueError(f"{field_name} is missing timezone information. {TIMEZONE_REQUIRED_MESSAGE}")
|
||||
return dt
|
||||
|
||||
|
||||
def extract_date(value: date | datetime | str, field_name: str = "date") -> date:
|
||||
"""
|
||||
提取日期部分,但保留调用方原始时区语义,不强制转换到 UTC。
|
||||
"""
|
||||
if isinstance(value, date) and not isinstance(value, datetime):
|
||||
return value
|
||||
return parse_aware_time(value, field_name=field_name).date()
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
"""
|
||||
返回带 UTC 时区的当前时间。
|
||||
"""
|
||||
return datetime.now(UTC_TZ)
|
||||
|
||||
|
||||
def parse_utc_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
|
||||
'''
|
||||
接受带时区的时间字符串/对象,并统一转换成 UTC 时间。
|
||||
'''
|
||||
return parse_aware_time(query_time, field_name=field_name).astimezone(UTC_TZ)
|
||||
|
||||
|
||||
def parse_beijing_time(query_time: datetime | str, field_name: str = "datetime") -> datetime:
|
||||
'''
|
||||
接受带时区的时间字符串/对象,并统一转换成北京时间。
|
||||
'''
|
||||
return parse_aware_time(query_time, field_name=field_name).astimezone(tz=BG_TZ)
|
||||
|
||||
|
||||
def to_utc_time(dt: datetime | str, field_name: str = "datetime") -> datetime:
|
||||
'''
|
||||
将一个带时区的时间点,转换成 UTC。
|
||||
'''
|
||||
return parse_aware_time(dt, field_name=field_name).astimezone(UTC_TZ)
|
||||
|
||||
|
||||
def to_beijing_time(dt: datetime | str, field_name: str = "datetime") -> datetime:
|
||||
'''
|
||||
将一个带时区的时间点,转换成北京时间。
|
||||
'''
|
||||
return parse_aware_time(dt, field_name=field_name).astimezone(tz=BG_TZ)
|
||||
|
||||
|
||||
def to_time_range(dt: datetime, delta: float) -> tuple[datetime, datetime]:
|
||||
'''
|
||||
将一个时间点,转换成 start/end 时间段
|
||||
有些查询按照一个时间点查不到,用时间段保证能成功
|
||||
|
||||
delta 单位是秒
|
||||
'''
|
||||
start_time = dt - timedelta(seconds=delta)
|
||||
end_time = dt + timedelta(seconds=delta)
|
||||
|
||||
return (start_time, end_time)
|
||||
|
||||
|
||||
def parse_clock_duration_seconds(clock: str, field_name: str = "duration") -> int:
|
||||
"""
|
||||
Parse EPANET-style clock durations into seconds.
|
||||
|
||||
Accepted formats include H:MM, HH:MM, H:MM:SS, and HH:MM:SS.
|
||||
"""
|
||||
if not isinstance(clock, str):
|
||||
raise ValueError(f"{field_name} must be a string clock duration.")
|
||||
|
||||
parts = clock.strip().split(":")
|
||||
if len(parts) not in (2, 3):
|
||||
raise ValueError(
|
||||
f"{field_name} must use H:MM or H:MM:SS format, got {clock!r}."
|
||||
)
|
||||
|
||||
try:
|
||||
values = [int(part) for part in parts]
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"{field_name} must contain numeric clock parts, got {clock!r}."
|
||||
) from exc
|
||||
|
||||
if any(value < 0 for value in values):
|
||||
raise ValueError(f"{field_name} must not contain negative values.")
|
||||
|
||||
hours, minutes = values[0], values[1]
|
||||
seconds = values[2] if len(values) == 3 else 0
|
||||
if minutes >= 60 or seconds >= 60:
|
||||
raise ValueError(
|
||||
f"{field_name} minutes and seconds must be less than 60, got {clock!r}."
|
||||
)
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds
|
||||
|
||||
def parse_beijing_date_range(query_date: str) -> tuple[datetime, datetime]:
|
||||
'''
|
||||
将一个日期字符串,转换成 start/end 时间段,传进来的日期被认为是北京时间
|
||||
日期字符串格式:YYYY-MM-DD
|
||||
'''
|
||||
target_date = date.fromisoformat(query_date)
|
||||
start_time = datetime.combine(target_date, time.min, BG_TZ)
|
||||
end_time = start_time + timedelta(days=1)
|
||||
|
||||
return (start_time, end_time)
|
||||
|
||||
|
||||
def get_day_start(dt: datetime.date) -> datetime:
|
||||
'''
|
||||
获取 某一天的 00:00:00
|
||||
这一天可以是北京时间,也可以是 utc 时间
|
||||
'''
|
||||
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
|
||||
def get_day_end(dt: datetime.date) -> datetime:
|
||||
'''
|
||||
获取 某一天的 23:59:59
|
||||
这一天可以是北京时间,也可以是 utc 时间
|
||||
'''
|
||||
return dt.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
|
||||
def get_date_from_time(time: str) -> str:
|
||||
'''
|
||||
将一个时间点,转换成日期
|
||||
'''
|
||||
dt = parse_beijing_time(time, field_name="time")
|
||||
return str(dt.date())
|
||||
|
||||
|
||||
def is_today(query_date: str) -> bool:
|
||||
'''
|
||||
判断一个日期是否是今天
|
||||
'''
|
||||
dt = parse_beijing_time(query_date, field_name="query_date")
|
||||
return dt.date() == datetime.now(BG_TZ).date()
|
||||
|
||||
|
||||
def is_yesterday(query_date: str) -> bool:
|
||||
'''
|
||||
判断一个日期是否是昨天
|
||||
'''
|
||||
dt = parse_beijing_time(query_date, field_name="query_date")
|
||||
return dt.date() == (datetime.now(BG_TZ).date() - timedelta(days=1))
|
||||
|
||||
def is_tomorrow(query_date: str) -> bool:
|
||||
'''
|
||||
判断一个日期是否是明天
|
||||
'''
|
||||
dt = parse_beijing_time(query_date, field_name="query_date")
|
||||
return dt.date() == (datetime.now(BG_TZ).date() + timedelta(days=1))
|
||||
|
||||
def is_today_or_future(query_date: str) -> bool:
|
||||
'''
|
||||
判断一个日期是否是今天或未来
|
||||
'''
|
||||
dt = parse_beijing_time(query_date, field_name="query_date")
|
||||
return dt.date() >= datetime.now(BG_TZ).date()
|
||||
@@ -0,0 +1,614 @@
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from psycopg import AsyncConnection
|
||||
from uuid import UUID
|
||||
|
||||
from app.algorithms.pipe_health_prediction.survival_predictor import (
|
||||
PipeHealthSurvivalPredictor,
|
||||
)
|
||||
from app.algorithms.scada_cleaning.flow_series import clean_flow_data_df_kf
|
||||
from app.algorithms.scada_cleaning.pressure_series import clean_pressure_data_df_km
|
||||
from app.infra.db.postgresql.network_assets import NetworkAssetRepository
|
||||
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
||||
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
||||
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
|
||||
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
||||
|
||||
|
||||
class TimeseriesAnalysisService:
|
||||
"""
|
||||
复合查询类,提供跨表查询功能
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def _get_project_scada_index(
|
||||
postgres_conn: AsyncConnection,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
scadas = await ScadaInfoRepository.get_scadas(postgres_conn)
|
||||
return {scada["device_id"]: scada for scada in scadas}
|
||||
|
||||
@staticmethod
|
||||
async def get_scada_associated_realtime_simulation_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
postgres_conn: AsyncConnection,
|
||||
device_ids: List[str],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取 SCADA 关联的 link/node 模拟值
|
||||
|
||||
根据传入的 SCADA device_ids,找到关联的 link/node,
|
||||
并根据对应的 type,查询对应的模拟数据
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
postgres_conn: PostgreSQL 异步连接
|
||||
device_ids: SCADA 设备ID列表
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
模拟数据字典,以 device_id 为键,值为数据列表,每个数据包含 time, value 和 scada_id
|
||||
|
||||
Raises:
|
||||
ValueError: 当 SCADA 设备未找到或字段无效时
|
||||
"""
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
link_devices: dict[str, str] = {}
|
||||
node_devices: dict[str, str] = {}
|
||||
for device_id in device_ids:
|
||||
target_scada = scada_by_id.get(device_id)
|
||||
if not target_scada:
|
||||
raise ValueError(f"SCADA device {device_id} not found")
|
||||
scada_type = target_scada["device_type"]
|
||||
if scada_type in {"pipe_flow", "flow"}:
|
||||
link_devices[device_id] = target_scada["link_id"]
|
||||
elif scada_type == "pressure":
|
||||
node_devices[device_id] = target_scada["node_id"]
|
||||
else:
|
||||
raise ValueError(f"Unknown SCADA type: {scada_type}")
|
||||
|
||||
link_series = await RealtimeRepository.get_link_fields_by_ids_time_range(
|
||||
timescale_conn, start_time, end_time,
|
||||
list(dict.fromkeys(link_devices.values())), "flow",
|
||||
)
|
||||
node_series = await RealtimeRepository.get_node_fields_by_ids_time_range(
|
||||
timescale_conn, start_time, end_time,
|
||||
list(dict.fromkeys(node_devices.values())), "pressure",
|
||||
)
|
||||
return {
|
||||
device_id: [
|
||||
{**item, "scada_id": device_id}
|
||||
for item in (
|
||||
link_series.get(element_id, [])
|
||||
if device_id in link_devices
|
||||
else node_series.get(element_id, [])
|
||||
)
|
||||
]
|
||||
for device_id, element_id in (link_devices | node_devices).items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_scada_associated_analysis_simulation_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
postgres_conn: AsyncConnection,
|
||||
device_ids: List[str],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
run_id: UUID,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取 SCADA 关联的 link/node 分析模拟值
|
||||
|
||||
根据传入的 SCADA device_ids,找到关联的 link/node,
|
||||
并根据对应的 type,查询对应的模拟数据
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
postgres_conn: PostgreSQL 异步连接
|
||||
device_ids: SCADA 设备ID列表
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
模拟数据字典,以 device_id 为键,值为数据列表,每个数据包含 time, value 和 scada_id
|
||||
|
||||
Raises:
|
||||
ValueError: 当 SCADA 设备未找到或字段无效时
|
||||
"""
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
link_devices: dict[str, str] = {}
|
||||
node_devices: dict[str, str] = {}
|
||||
for device_id in device_ids:
|
||||
target_scada = scada_by_id.get(device_id)
|
||||
if not target_scada:
|
||||
raise ValueError(f"SCADA device {device_id} not found")
|
||||
|
||||
scada_type = target_scada["device_type"]
|
||||
if scada_type in {"pipe_flow", "flow"}:
|
||||
link_devices[device_id] = target_scada["link_id"]
|
||||
elif scada_type == "pressure":
|
||||
node_devices[device_id] = target_scada["node_id"]
|
||||
else:
|
||||
raise ValueError(f"Unknown SCADA type: {scada_type}")
|
||||
|
||||
link_series = await AnalysisResultsRepository.get_series_by_ids(
|
||||
timescale_conn, run_id, "link",
|
||||
list(dict.fromkeys(link_devices.values())), start_time, end_time, "flow",
|
||||
)
|
||||
node_series = await AnalysisResultsRepository.get_series_by_ids(
|
||||
timescale_conn, run_id, "node",
|
||||
list(dict.fromkeys(node_devices.values())), start_time, end_time, "pressure",
|
||||
)
|
||||
return {
|
||||
device_id: [
|
||||
{**item, "scada_id": device_id}
|
||||
for item in (
|
||||
link_series.get(element_id, [])
|
||||
if device_id in link_devices
|
||||
else node_series.get(element_id, [])
|
||||
)
|
||||
]
|
||||
for device_id, element_id in (link_devices | node_devices).items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_realtime_simulation_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
feature_infos: List[Tuple[str, str]],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取 link/node 模拟值
|
||||
|
||||
根据传入的 feature_infos,找到关联的 link/node,
|
||||
并根据对应的 type,查询对应的模拟数据
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
feature_infos: 传入的 feature 信息列表,包含 (element_id, type)
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
模拟数据字典,以 feature_id 为键,值为数据列表,每个数据包含 time, value 和 feature_id
|
||||
|
||||
Raises:
|
||||
ValueError: 当 SCADA 设备未找到或字段无效时
|
||||
"""
|
||||
pipe_ids: list[str] = []
|
||||
junction_ids: list[str] = []
|
||||
for feature_id, feature_type in feature_infos:
|
||||
if feature_type.lower() == "pipe":
|
||||
pipe_ids.append(feature_id)
|
||||
elif feature_type.lower() == "junction":
|
||||
junction_ids.append(feature_id)
|
||||
else:
|
||||
raise ValueError(f"Unknown type: {feature_type}")
|
||||
link_series = await RealtimeRepository.get_link_fields_by_ids_time_range(
|
||||
timescale_conn, start_time, end_time, list(dict.fromkeys(pipe_ids)), "flow"
|
||||
)
|
||||
node_series = await RealtimeRepository.get_node_fields_by_ids_time_range(
|
||||
timescale_conn, start_time, end_time,
|
||||
list(dict.fromkeys(junction_ids)), "pressure",
|
||||
)
|
||||
return {
|
||||
feature_id: [
|
||||
{**item, "feature_id": feature_id}
|
||||
for item in (
|
||||
link_series.get(feature_id, [])
|
||||
if feature_type.lower() == "pipe"
|
||||
else node_series.get(feature_id, [])
|
||||
)
|
||||
]
|
||||
for feature_id, feature_type in feature_infos
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_analysis_simulation_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
feature_infos: List[Tuple[str, str]],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
run_id: UUID,
|
||||
) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
获取 link/node 分析模拟值
|
||||
|
||||
根据传入的 feature_infos,找到关联的 link/node,
|
||||
并根据对应的 type,查询对应的模拟数据
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
feature_infos: 传入的 feature 信息列表,包含 (element_id, type)
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
run_id: 分析运行 ID
|
||||
|
||||
Returns:
|
||||
模拟数据字典,以 feature_id 为键,值为数据列表,每个数据包含 time, value 和 feature_id
|
||||
|
||||
Raises:
|
||||
ValueError: 当类型无效时
|
||||
"""
|
||||
pipe_ids: list[str] = []
|
||||
junction_ids: list[str] = []
|
||||
for feature_id, feature_type in feature_infos:
|
||||
if feature_type.lower() == "pipe":
|
||||
pipe_ids.append(feature_id)
|
||||
elif feature_type.lower() == "junction":
|
||||
junction_ids.append(feature_id)
|
||||
else:
|
||||
raise ValueError(f"Unknown type: {feature_type}")
|
||||
link_series = await AnalysisResultsRepository.get_series_by_ids(
|
||||
timescale_conn, run_id, "link", list(dict.fromkeys(pipe_ids)),
|
||||
start_time, end_time, "flow",
|
||||
)
|
||||
node_series = await AnalysisResultsRepository.get_series_by_ids(
|
||||
timescale_conn, run_id, "node", list(dict.fromkeys(junction_ids)),
|
||||
start_time, end_time, "pressure",
|
||||
)
|
||||
return {
|
||||
feature_id: [
|
||||
{**item, "feature_id": feature_id}
|
||||
for item in (
|
||||
link_series.get(feature_id, [])
|
||||
if feature_type.lower() == "pipe"
|
||||
else node_series.get(feature_id, [])
|
||||
)
|
||||
]
|
||||
for feature_id, feature_type in feature_infos
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_element_associated_scada_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
postgres_conn: AsyncConnection,
|
||||
element_id: str,
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
use_cleaned: bool = False,
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
获取 link/node 关联的 SCADA 监测值
|
||||
|
||||
根据传入的 link/node id,匹配 SCADA 信息,
|
||||
如果存在关联的 SCADA device_id,获取实际的监测数据
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
postgres_conn: PostgreSQL 异步连接
|
||||
element_id: link 或 node 的 ID
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
use_cleaned: 是否使用清洗后的数据 (True: "cleaned_value", False: "monitored_value")
|
||||
|
||||
Returns:
|
||||
SCADA 监测数据值,如果没有找到则返回 None
|
||||
|
||||
Raises:
|
||||
ValueError: 当元素类型无效时
|
||||
"""
|
||||
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
associated_scada = next(
|
||||
(
|
||||
scada
|
||||
for scada in scada_by_id.values()
|
||||
if (scada.get("node_id") or scada.get("link_id")) == element_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not associated_scada:
|
||||
return None
|
||||
|
||||
device_id = associated_scada["device_id"]
|
||||
|
||||
data_field = "cleaned_value" if use_cleaned else "monitored_value"
|
||||
|
||||
res = await ScadaRepository.get_scada_field_by_id_time_range(
|
||||
timescale_conn, [device_id], start_time, end_time, data_field
|
||||
)
|
||||
|
||||
return {element_id: res.get(device_id, [])}
|
||||
|
||||
@staticmethod
|
||||
async def clean_scada_data(
|
||||
timescale_conn: AsyncConnection,
|
||||
postgres_conn: AsyncConnection,
|
||||
device_ids: List[str],
|
||||
start_time: datetime,
|
||||
end_time: datetime,
|
||||
) -> str:
|
||||
"""
|
||||
清洗 SCADA 数据
|
||||
|
||||
根据 device_ids 查询 monitored_value,清洗后更新 cleaned_value
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 连接
|
||||
postgres_conn: PostgreSQL 连接
|
||||
device_ids: 设备 ID 列表
|
||||
start_time: 开始时间
|
||||
end_time: 结束时间
|
||||
|
||||
Returns:
|
||||
"success"
|
||||
|
||||
Raises:
|
||||
ValueError: 当前项目没有可清洗设备或指定时间范围内没有监测数据
|
||||
"""
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
supported_types = {"pressure", "pipe_flow", "flow"}
|
||||
|
||||
if device_ids:
|
||||
device_ids = [str(device_id).strip() for device_id in device_ids]
|
||||
missing_metadata_ids = [
|
||||
device_id
|
||||
for device_id in device_ids
|
||||
if device_id not in scada_by_id
|
||||
]
|
||||
if missing_metadata_ids:
|
||||
raise ValueError(
|
||||
f"当前项目中有 {len(missing_metadata_ids)} 个 SCADA 设备缺少元数据"
|
||||
)
|
||||
|
||||
unsupported_ids = [
|
||||
device_id
|
||||
for device_id in device_ids
|
||||
if scada_by_id[device_id]["device_type"] not in supported_types
|
||||
]
|
||||
if unsupported_ids:
|
||||
raise ValueError(
|
||||
f"当前项目中有 {len(unsupported_ids)} 个 SCADA 设备类型不支持清洗"
|
||||
)
|
||||
else:
|
||||
device_ids = [
|
||||
device_id
|
||||
for device_id, info in scada_by_id.items()
|
||||
if info["device_type"] in supported_types
|
||||
]
|
||||
|
||||
if not device_ids:
|
||||
raise ValueError("当前项目没有可清洗的 SCADA 设备")
|
||||
|
||||
data = await ScadaRepository.get_scada_field_by_id_time_range(
|
||||
timescale_conn, device_ids, start_time, end_time, "monitored_value"
|
||||
)
|
||||
if not data:
|
||||
raise ValueError("指定时间范围内没有 SCADA 监测数据")
|
||||
|
||||
normalized_data = {
|
||||
str(device_id): records for device_id, records in data.items()
|
||||
}
|
||||
missing_data_ids = [
|
||||
device_id for device_id in device_ids if not normalized_data.get(device_id)
|
||||
]
|
||||
if missing_data_ids:
|
||||
raise ValueError(
|
||||
f"指定时间范围内有 {len(missing_data_ids)} 个 SCADA 设备没有监测数据"
|
||||
)
|
||||
|
||||
all_records = [
|
||||
{
|
||||
"time": record["time"],
|
||||
"device_id": device_id,
|
||||
"value": record["value"],
|
||||
}
|
||||
for device_id, records in normalized_data.items()
|
||||
for record in records
|
||||
]
|
||||
if not all_records:
|
||||
raise ValueError("指定时间范围内没有 SCADA 监测数据")
|
||||
|
||||
df_long = pd.DataFrame(all_records)
|
||||
df = df_long.pivot(index="time", columns="device_id", values="value")
|
||||
|
||||
pressure_ids = [
|
||||
device_id
|
||||
for device_id in df.columns
|
||||
if scada_by_id[device_id]["device_type"] == "pressure"
|
||||
]
|
||||
flow_ids = [
|
||||
device_id
|
||||
for device_id in df.columns
|
||||
if scada_by_id[device_id]["device_type"] in {"pipe_flow", "flow"}
|
||||
]
|
||||
|
||||
cleaned_rows: list[tuple[datetime, str, float | None]] = []
|
||||
for grouped_ids, cleaning_function in (
|
||||
(pressure_ids, clean_pressure_data_df_km),
|
||||
(flow_ids, clean_flow_data_df_kf),
|
||||
):
|
||||
if not grouped_ids:
|
||||
continue
|
||||
|
||||
source_df = df[grouped_ids].reset_index()
|
||||
cleaned_df = cleaning_function(source_df)
|
||||
time_values = cleaned_df["time"].tolist()
|
||||
|
||||
for device_id in grouped_ids:
|
||||
if device_id not in cleaned_df.columns:
|
||||
raise ValueError(f"设备 {device_id} 的清洗结果缺少数据列")
|
||||
|
||||
cleaned_values = cleaned_df[device_id].tolist()
|
||||
for time_value, value in zip(time_values, cleaned_values):
|
||||
time_dt = (
|
||||
time_value
|
||||
if isinstance(time_value, datetime)
|
||||
else datetime.fromisoformat(str(time_value))
|
||||
)
|
||||
cleaned_rows.append(
|
||||
(
|
||||
time_dt,
|
||||
device_id,
|
||||
None if pd.isna(value) else float(value),
|
||||
)
|
||||
)
|
||||
|
||||
if not cleaned_rows:
|
||||
raise ValueError("SCADA 数据清洗未产生任何数据库更新")
|
||||
|
||||
expected_rows = len({(row[0], row[1]) for row in cleaned_rows})
|
||||
async with timescale_conn.transaction():
|
||||
updated_rows = await ScadaRepository.update_scada_field_batch(
|
||||
timescale_conn,
|
||||
cleaned_rows,
|
||||
"cleaned_value",
|
||||
)
|
||||
if updated_rows != expected_rows:
|
||||
raise ValueError(
|
||||
"SCADA 清洗目标在写入期间发生变化,"
|
||||
f"预期更新 {expected_rows} 行,实际更新 {updated_rows} 行"
|
||||
)
|
||||
|
||||
return "success"
|
||||
|
||||
@staticmethod
|
||||
async def predict_pipeline_health(
|
||||
timescale_conn: AsyncConnection,
|
||||
postgres_conn: AsyncConnection,
|
||||
query_time: datetime,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
预测管道健康状况
|
||||
|
||||
根据管网名称和当前时间,查询管道信息和实时数据,
|
||||
使用随机生存森林模型预测管道的生存概率
|
||||
|
||||
Args:
|
||||
timescale_conn: TimescaleDB 异步连接
|
||||
query_time: 查询时间
|
||||
property_conditions: 可选的管道筛选条件,如 {"diameter": 300}
|
||||
|
||||
Returns:
|
||||
预测结果列表,每个元素包含 link_id 和对应的生存函数
|
||||
|
||||
Raises:
|
||||
ValueError: 当参数无效或数据不足时
|
||||
FileNotFoundError: 当模型文件未找到时
|
||||
"""
|
||||
try:
|
||||
# 1. 准备时间范围(查询时间前后1秒)
|
||||
start_time = query_time - timedelta(seconds=1)
|
||||
end_time = query_time + timedelta(seconds=1)
|
||||
|
||||
# 2. 先查询流速数据(velocity),获取有数据的管道ID列表
|
||||
velocity_data = await RealtimeRepository.get_links_field_by_time_range(
|
||||
timescale_conn, start_time, end_time, "velocity"
|
||||
)
|
||||
|
||||
if not velocity_data:
|
||||
raise ValueError("未找到流速数据")
|
||||
|
||||
# 3. 只查询有流速数据的管道的基本信息
|
||||
valid_link_ids = list(velocity_data.keys())
|
||||
|
||||
# GIS 物化视图是低频更新管网的查询面;只读取本次有结果的管道。
|
||||
all_links = await NetworkAssetRepository.get_pipes_by_ids(
|
||||
postgres_conn, valid_link_ids
|
||||
)
|
||||
|
||||
# 转换为字典以快速查找
|
||||
links_dict = {str(link["id"]): link for link in all_links}
|
||||
|
||||
# 获取所有需要查询的节点ID
|
||||
node_ids = set()
|
||||
for link_id in valid_link_ids:
|
||||
if link_id in links_dict:
|
||||
link = links_dict[link_id]
|
||||
node_ids.add(link["node1"])
|
||||
node_ids.add(link["node2"])
|
||||
|
||||
# 4. 批量查询压力数据(pressure)
|
||||
pressure_data = await RealtimeRepository.get_nodes_field_by_time_range(
|
||||
timescale_conn, start_time, end_time, "pressure"
|
||||
)
|
||||
|
||||
# 5. 组合数据结构
|
||||
materials = []
|
||||
diameters = []
|
||||
velocities = []
|
||||
pressures = []
|
||||
link_ids = []
|
||||
|
||||
for link_id in valid_link_ids:
|
||||
# 跳过不在管道字典中的ID(如泵等其他元素)
|
||||
if link_id not in links_dict:
|
||||
continue
|
||||
|
||||
link = links_dict[link_id]
|
||||
diameter = link["diameter"]
|
||||
node1 = link["node1"]
|
||||
node2 = link["node2"]
|
||||
|
||||
# 获取流速数据
|
||||
velocity_values = velocity_data[link_id]
|
||||
velocity = velocity_values[-1]["value"] if velocity_values else 0
|
||||
|
||||
# 获取node1和node2的压力数据,计算平均值
|
||||
node1_pressure = 0
|
||||
node2_pressure = 0
|
||||
|
||||
if node1 in pressure_data and pressure_data[node1]:
|
||||
pressure_values = pressure_data[node1]
|
||||
node1_pressure = (
|
||||
pressure_values[-1]["value"] if pressure_values else 0
|
||||
)
|
||||
|
||||
if node2 in pressure_data and pressure_data[node2]:
|
||||
pressure_values = pressure_data[node2]
|
||||
node2_pressure = (
|
||||
pressure_values[-1]["value"] if pressure_values else 0
|
||||
)
|
||||
|
||||
# 计算平均压力
|
||||
avg_pressure = (node1_pressure + node2_pressure) / 2
|
||||
|
||||
# 添加到列表
|
||||
link_ids.append(link_id)
|
||||
materials.append(7) # 默认材料类型为7,可根据实际情况调整
|
||||
diameters.append(diameter)
|
||||
velocities.append(velocity)
|
||||
pressures.append(avg_pressure)
|
||||
|
||||
if not link_ids:
|
||||
raise ValueError("没有找到有效的管道数据用于预测")
|
||||
|
||||
# 6. 创建DataFrame
|
||||
data = pd.DataFrame(
|
||||
{
|
||||
"Material": materials,
|
||||
"Diameter": diameters,
|
||||
"Flow Velocity": velocities,
|
||||
"Pressure": pressures,
|
||||
}
|
||||
)
|
||||
|
||||
# 7. 使用生存模型进行预测
|
||||
analyzer = PipeHealthSurvivalPredictor()
|
||||
survival_functions = analyzer.predict_survival(data)
|
||||
# 8. 组合结果
|
||||
results = []
|
||||
for i, link_id in enumerate(link_ids):
|
||||
sf = survival_functions[i]
|
||||
results.append(
|
||||
{
|
||||
"link_id": link_id,
|
||||
"survival_function": {
|
||||
"x": sf.x.tolist(), # 时间点(年)
|
||||
"y": sf.y.tolist(), # 生存概率
|
||||
},
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"管道健康预测失败: {str(e)}")
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Application-facing water-network service boundary.
|
||||
|
||||
The native WNDB package is organized by responsibility. This module only
|
||||
combines operations that need more than one native module and re-exports the
|
||||
small set of operations used by HTTP and algorithm services.
|
||||
The native WNDB package is organized by responsibility. This transitional
|
||||
facade re-exports the operations still consumed by HTTP endpoints. Application
|
||||
services and algorithms depend on their explicit native/repository modules.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import app.infra.epanet as epanet
|
||||
from app.algorithms.water_demand import (
|
||||
from app.services.demand_allocation import (
|
||||
calculate_demand_to_network,
|
||||
calculate_demand_to_nodes,
|
||||
calculate_demand_to_region,
|
||||
@@ -234,10 +234,6 @@ def run_inp(name: str) -> str:
|
||||
return epanet.run_inp(name)
|
||||
|
||||
|
||||
def dump_output(path: str) -> str:
|
||||
return epanet.dump_output(path)
|
||||
|
||||
|
||||
def get_node_properties(name: str, node_id: str) -> dict[str, Any]:
|
||||
if is_junction(name, node_id):
|
||||
return get_junction(name, node_id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from app.algorithms.isolation.valve import valve_isolation_analysis
|
||||
from app.algorithms.valve_isolation.topology_search import valve_isolation_analysis
|
||||
from app.native.wndb.gis.network_views import get_network_link_nodes
|
||||
|
||||
|
||||
def analyze_valve_isolation(
|
||||
@@ -8,4 +9,5 @@ def analyze_valve_isolation(
|
||||
accident_element: str | list[str],
|
||||
disabled_valves: list[str] = None,
|
||||
) -> dict[str, Any]:
|
||||
return valve_isolation_analysis(network, accident_element, disabled_valves)
|
||||
link_entries = get_network_link_nodes(network)
|
||||
return valve_isolation_analysis(link_entries, accident_element, disabled_valves)
|
||||
|
||||
Reference in New Issue
Block a user