refactor(backend)!: separate algorithm and data layers
Reorganize algorithm packages by business responsibility, move orchestration into services, and keep database access behind pooled repositories. Harden analysis API validation, remove unsafe legacy simulation endpoints, and add regression and architecture boundary coverage. BREAKING CHANGE: legacy algorithm module paths and obsolete simulation endpoints are removed.
This commit is contained in:
@@ -1,45 +1,5 @@
|
||||
"""Algorithm package with side-effect-free, lazy compatibility exports."""
|
||||
"""Pure water-network calculation packages.
|
||||
|
||||
from importlib import import_module
|
||||
from typing import Any
|
||||
|
||||
|
||||
_EXPORT_MODULES = {
|
||||
"flow_data_clean": "app.algorithms.cleaning",
|
||||
"pressure_data_clean": "app.algorithms.cleaning",
|
||||
"pressure_sensor_placement_sensitivity": "app.algorithms.sensor",
|
||||
"pressure_sensor_placement_kmeans": "app.algorithms.sensor",
|
||||
"valve_isolation_analysis": "app.algorithms.isolation.valve",
|
||||
"LeakageIdentifier": "app.algorithms.leakage",
|
||||
"PipelineHealthAnalyzer": "app.algorithms.health",
|
||||
"run_burst_location": "app.algorithms.burst_location",
|
||||
**{
|
||||
name: "app.algorithms.simulation.scenarios"
|
||||
for name in (
|
||||
"convert_to_local_unit",
|
||||
"burst_analysis",
|
||||
"valve_close_analysis",
|
||||
"flushing_analysis",
|
||||
"contaminant_simulation",
|
||||
"age_analysis",
|
||||
"pressure_regulation",
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
__all__ = list(_EXPORT_MODULES)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
try:
|
||||
module_name = _EXPORT_MODULES[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
|
||||
|
||||
value = getattr(import_module(module_name), name)
|
||||
globals()[name] = value
|
||||
return value
|
||||
|
||||
|
||||
def __dir__() -> list[str]:
|
||||
return sorted({*globals(), *__all__})
|
||||
Application workflows belong in :mod:`app.services`; database and external
|
||||
system access belongs in :mod:`app.infra` or :mod:`app.native`.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from app.algorithms.burst_detection.burst_detector import BurstDetector
|
||||
from app.algorithms.burst_detection.pressure_anomaly import PressureAnomalyDetector
|
||||
|
||||
__all__ = ["BurstDetector"]
|
||||
__all__ = ["PressureAnomalyDetector"]
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ PressureDataInput = (
|
||||
IGNORED_OBSERVATION_COLUMNS = {"time", "timestamp", "datetime", "date"}
|
||||
|
||||
|
||||
class BurstDetector:
|
||||
class PressureAnomalyDetector:
|
||||
"""FFT + IsolationForest based burst detection for daily aligned pressure data."""
|
||||
|
||||
def __init__(
|
||||
@@ -0,0 +1,3 @@
|
||||
from .pipeline import run_burst_location
|
||||
|
||||
__all__ = ["run_burst_location"]
|
||||
+3
-4
@@ -11,13 +11,13 @@ import networkx as nx
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from .leak_simulator import cal_signature_pipe_multi_pf
|
||||
from .network_partitioner import (
|
||||
from .leak_signature import cal_signature_pipe_multi_pf
|
||||
from .topology_partitioning import (
|
||||
cal_group_num,
|
||||
metis_grouping_pipe_weight,
|
||||
visualize_metis_partition,
|
||||
)
|
||||
from .similarity_calculator import (
|
||||
from .similarity_metrics import (
|
||||
adjust_ratio,
|
||||
cal_similarity_all_multi_new_sq_improve_double_lzr,
|
||||
decode_mode,
|
||||
@@ -769,4 +769,3 @@ def DN_search_multi_simple_add_flow_count_new(
|
||||
final_candidates_csv,
|
||||
)
|
||||
|
||||
|
||||
+4
-108
@@ -1,5 +1,3 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
@@ -7,12 +5,12 @@ from typing import Any, Iterable
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.algorithms.burst_location import leak_simulator
|
||||
from app.algorithms.burst_localization import leak_signature
|
||||
|
||||
from .burst_locator import (
|
||||
from .candidate_ranking import (
|
||||
DN_search_multi_simple_add_flow_count_new,
|
||||
)
|
||||
from .network_model import (
|
||||
from .topology_model import (
|
||||
_build_node_pipe_maps,
|
||||
cal_node_coordinate,
|
||||
construct_graph,
|
||||
@@ -26,35 +24,6 @@ DEFAULT_N_WORKERS = max(1, min(cpu_count() - 1, 4))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _read_id_list_json(path):
|
||||
if path is None:
|
||||
return None
|
||||
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
if isinstance(data, list):
|
||||
return [str(item) for item in data]
|
||||
if isinstance(data, dict):
|
||||
if "ids" in data and isinstance(data["ids"], list):
|
||||
return [str(item) for item in data["ids"]]
|
||||
raise ValueError(f"ID JSON must be list or dict with key 'ids': {path}")
|
||||
raise ValueError(f"Unsupported ID JSON format: {path}")
|
||||
|
||||
|
||||
def _read_series_csv(path):
|
||||
if path is None:
|
||||
return None
|
||||
df = pd.read_csv(path)
|
||||
if df.shape[1] < 2:
|
||||
raise ValueError(f"CSV must contain at least two columns (id,value): {path}")
|
||||
if {"id", "value"}.issubset(df.columns):
|
||||
id_col, value_col = "id", "value"
|
||||
else:
|
||||
id_col, value_col = df.columns[0], df.columns[1]
|
||||
series = pd.Series(
|
||||
df[value_col].values, index=df[id_col].astype(str).values, dtype=float
|
||||
)
|
||||
return series
|
||||
|
||||
|
||||
def _align_scada_series(
|
||||
series: pd.Series, ids: Iterable[str], series_name: str
|
||||
) -> pd.Series:
|
||||
@@ -159,7 +128,7 @@ def run_burst_location(
|
||||
pipe_diameter,
|
||||
) = read_inf_inp(wn)
|
||||
|
||||
candidate_pipe, _ = leak_simulator.cal_possible_pipe(
|
||||
candidate_pipe, _ = leak_signature.cal_possible_pipe(
|
||||
burst_leakage, all_pipe, pipe_diameter
|
||||
)
|
||||
|
||||
@@ -267,76 +236,3 @@ def run_burst_location(
|
||||
"final_candidates_csv": final_candidates_csv,
|
||||
"stage_timing_seconds": stage_timing,
|
||||
}
|
||||
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser(description="爆管定位主函数入口")
|
||||
parser.add_argument("--wn-inp", required=True, help="EPANET inp 文件路径")
|
||||
parser.add_argument(
|
||||
"--pressure-ids-json", required=True, help="压力SCADA ID列表 JSON 文件"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--flow-ids-json", default=None, help="(可选)流量SCADA ID列表 JSON 文件"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--burst-pressure-csv", required=True, help="爆管时压力 CSV(id,value)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normal-pressure-csv", required=True, help="正常时压力 CSV(id,value)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--burst-flow-csv", default=None, help="(可选)爆管时流量 CSV(id,value)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--normal-flow-csv", default=None, help="(可选)正常时流量 CSV(id,value)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--burst-leakage", type=float, required=True, help="爆管漏损流量"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-dpressure",
|
||||
type=float,
|
||||
default=2.0,
|
||||
help="(可选)最小压降阈值,默认 2.0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--basic-pressure",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="(可选)基础服务压力,默认 10.0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--n-workers",
|
||||
type=int,
|
||||
default=DEFAULT_N_WORKERS,
|
||||
help="(可选)特征中心模拟进程数,默认 max(1, min(cpu_count()-1, 4))",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--final-candidates-csv-path",
|
||||
default="temp/burst_location/final_round_candidates.csv",
|
||||
help="(可选)最后一轮候选管道明细 CSV 输出路径",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = _parse_args()
|
||||
result = run_burst_location(
|
||||
wn_inp_path=args.wn_inp,
|
||||
pressure_scada_ids=_read_id_list_json(args.pressure_ids_json),
|
||||
burst_pressure=_read_series_csv(args.burst_pressure_csv),
|
||||
normal_pressure=_read_series_csv(args.normal_pressure_csv),
|
||||
burst_leakage=args.burst_leakage,
|
||||
flow_scada_ids=_read_id_list_json(args.flow_ids_json),
|
||||
burst_flow=_read_series_csv(args.burst_flow_csv),
|
||||
normal_flow=_read_series_csv(args.normal_flow_csv),
|
||||
min_dpressure=args.min_dpressure,
|
||||
basic_pressure=args.basic_pressure,
|
||||
n_workers=args.n_workers,
|
||||
final_candidates_csv_path=args.final_candidates_csv_path,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+1
-2
@@ -6,7 +6,7 @@ import random
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from .leak_simulator import simple_add_leak, simple_recover_wn, simple_simulation_pf
|
||||
from .leak_signature import simple_add_leak, simple_recover_wn, simple_simulation_pf
|
||||
|
||||
|
||||
def add_noise_pd(data, noise_type, noise_para):
|
||||
@@ -195,4 +195,3 @@ def change_para_of_wn(wn, pipe_roughness_change):
|
||||
pipe.roughness = pipe_roughness_change[pipe_name]
|
||||
return wn
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from .burst_location import run_burst_location
|
||||
|
||||
__all__ = ["run_burst_location"]
|
||||
@@ -1,59 +0,0 @@
|
||||
import os
|
||||
|
||||
from app.algorithms.cleaning import flow as _flow_module
|
||||
from app.algorithms.cleaning import pressure as _pressure_module
|
||||
|
||||
|
||||
############################################################
|
||||
# 流量监测数据清洗 ***卡尔曼滤波法***
|
||||
############################################################
|
||||
# 2025/08/21 hxyan
|
||||
|
||||
|
||||
def flow_data_clean(input_csv_file: str) -> str:
|
||||
"""
|
||||
读取 input_csv_path 中的每列时间序列,使用一维 Kalman 滤波平滑并用预测值替换基于 3σ 检测出的异常点。
|
||||
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
|
||||
:param: input_csv_file: 输入的 CSV 文件明或路径
|
||||
:return: 输出文件的绝对路径
|
||||
"""
|
||||
|
||||
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
|
||||
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
input_csv_path = os.path.join(script_dir, input_csv_file)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(input_csv_path):
|
||||
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
|
||||
# 调用 clean_flow_data_kf 函数进行数据清洗
|
||||
out_xlsx_path = _flow_module.clean_flow_data_kf(input_csv_path)
|
||||
print("清洗后的数据已保存到:", out_xlsx_path)
|
||||
|
||||
|
||||
############################################################
|
||||
# 压力监测数据清洗 ***kmean++法***
|
||||
############################################################
|
||||
# 2025/08/21 hxyan
|
||||
|
||||
|
||||
def pressure_data_clean(input_csv_file: str) -> str:
|
||||
"""
|
||||
读取 input_csv_path 中的每列时间序列,使用Kmean++清洗数据。
|
||||
保存输出为:<input_filename>_cleaned.xlsx(与输入同目录),并返回输出文件的绝对路径。如有同名文件存在,则覆盖。
|
||||
原始数据在 sheet 'raw_pressure_data',处理后数据在 sheet 'cleaned_pressusre_data'。
|
||||
:param input_csv_path: 输入的 CSV 文件路径
|
||||
:return: 输出文件的绝对路径
|
||||
"""
|
||||
|
||||
# 提供的 input_csv_path 绝对路径,以下为 默认脚本目录下同名 CSV 文件,构建绝对路径,可根据情况修改
|
||||
# 使用 algorithms 根目录保持与原 data_cleaning.py 一致的行为
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
input_csv_path = os.path.join(script_dir, input_csv_file)
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(input_csv_path):
|
||||
raise FileNotFoundError(f"指定的文件不存在: {input_csv_path}")
|
||||
# 调用 clean_pressure_data_km 函数进行数据清洗
|
||||
out_xlsx_path = _pressure_module.clean_pressure_data_km(input_csv_path)
|
||||
print("清洗后的数据已保存到:", out_xlsx_path)
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Demand allocation calculations."""
|
||||
|
||||
from .pipe_length_weighted import allocate_demand_by_pipe_length
|
||||
|
||||
__all__ = ["allocate_demand_by_pipe_length"]
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Pipe-length-weighted demand allocation.
|
||||
|
||||
This module deliberately accepts plain topology data and performs no database
|
||||
or file access. Application services are responsible for loading topology.
|
||||
"""
|
||||
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
def allocate_demand_by_pipe_length(
|
||||
demand: float,
|
||||
topology_nodes: Mapping[str, Mapping[str, Any]],
|
||||
topology_links: Mapping[str, Mapping[str, Any]],
|
||||
) -> dict[str, float]:
|
||||
"""Allocate total demand to junctions by half of each incident link length."""
|
||||
|
||||
if not topology_nodes or not topology_links or demand == 0.0:
|
||||
return {}
|
||||
|
||||
total_link_length = sum(
|
||||
abs(float(link["length"])) for link in topology_links.values()
|
||||
)
|
||||
if total_link_length <= 0.0:
|
||||
return {}
|
||||
|
||||
demand_per_length = demand / total_link_length
|
||||
result: dict[str, float] = {}
|
||||
for node_id, node in topology_nodes.items():
|
||||
if node["type"] != "junction":
|
||||
continue
|
||||
incident_length = sum(
|
||||
abs(float(topology_links[link_id]["length"]))
|
||||
for link_id in node["links"]
|
||||
)
|
||||
result[node_id] = incident_length * demand_per_length * 0.5
|
||||
return result
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.algorithms.dma_leakage_estimation.genetic_optimizer import DmaLeakageOptimizer
|
||||
|
||||
__all__ = ["DmaLeakageOptimizer"]
|
||||
+6
-52
@@ -3,7 +3,6 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from typing import Any, List, Dict, Union
|
||||
|
||||
@@ -70,7 +69,7 @@ def _worker_init(
|
||||
|
||||
def _worker_evaluate(raw_ratios: np.ndarray) -> float:
|
||||
d = _worker_data
|
||||
effective_ratio_map = LeakageIdentifier._effective_area_ratios(
|
||||
effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
|
||||
raw_ratios,
|
||||
d["area_ids"],
|
||||
d["nodes_by_area"],
|
||||
@@ -121,7 +120,7 @@ def _worker_evaluate(raw_ratios: np.ndarray) -> float:
|
||||
_cleanup_temp_files(prefix)
|
||||
|
||||
|
||||
class LeakageIdentifier:
|
||||
class DmaLeakageOptimizer:
|
||||
FLOW_UNIT_TO_M3S = {
|
||||
"m3/s": 1.0,
|
||||
"m³/s": 1.0,
|
||||
@@ -543,7 +542,7 @@ class LeakageProblem(Problem):
|
||||
leak_ratios = x
|
||||
|
||||
# 将漏损分布归一化
|
||||
effective_ratio_map = LeakageIdentifier._effective_area_ratios(
|
||||
effective_ratio_map = DmaLeakageOptimizer._effective_area_ratios(
|
||||
leak_ratios,
|
||||
self.area_ids,
|
||||
self.nodes_by_area,
|
||||
@@ -605,51 +604,6 @@ class LeakageProblem(Problem):
|
||||
|
||||
def close(self) -> None:
|
||||
if self._pool is not None:
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="漏损区域识别")
|
||||
parser.add_argument("--inp", required=True, help=".inp 文件路径")
|
||||
parser.add_argument("--map", help="节点-区域映射 CSV 路径")
|
||||
parser.add_argument("--scada", help="SCADA 压力 CSV 路径 (观测数据)")
|
||||
parser.add_argument("--sensors", help="传感器节点 ID 列表 (逗号分隔)")
|
||||
parser.add_argument("--output", default="Results", help="输出目录")
|
||||
|
||||
parser.add_argument("--pop_size", type=int, default=50, help="种群大小")
|
||||
parser.add_argument("--max_gen", type=int, default=100, help="最大代数")
|
||||
parser.add_argument("--duration", type=float, default=24, help="模拟时长(小时)")
|
||||
parser.add_argument("--q_sum", type=float, default=0.241, help="总漏损流量")
|
||||
parser.add_argument(
|
||||
"--q_sum_unit",
|
||||
default="m3/s",
|
||||
choices=list(LeakageIdentifier.FLOW_UNIT_TO_M3S.keys()),
|
||||
help="q_sum 输入单位(建议与现场习惯一致,内部统一换算为 m3/s)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.map or not args.scada or not args.sensors:
|
||||
parser.error("--map、--scada、--sensors 为必填")
|
||||
|
||||
q_sum_m3s = LeakageIdentifier._flow_to_m3s(args.q_sum, args.q_sum_unit)
|
||||
|
||||
sensors = [sensor.strip() for sensor in args.sensors.split(",") if sensor.strip()]
|
||||
identifier = LeakageIdentifier(
|
||||
args.inp, sensors, args.map, duration=args.duration, q_sum=q_sum_m3s
|
||||
)
|
||||
|
||||
identifier.run_identification(
|
||||
args.scada,
|
||||
args.output,
|
||||
pop_size=args.pop_size,
|
||||
max_gen=args.max_gen,
|
||||
output_flow_unit=args.q_sum_unit,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Pure topology partitioning used by DMA leakage estimation."""
|
||||
|
||||
import math
|
||||
from collections import deque
|
||||
from typing import Any, Iterable, Mapping
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def build_dma_partitions(
|
||||
sensor_nodes: list[str],
|
||||
node_coords: Mapping[str, Mapping[str, Any]],
|
||||
link_entries: Iterable[str],
|
||||
dma_count: int | None,
|
||||
) -> tuple[dict[str, str], list[dict[str, Any]]]:
|
||||
"""Assign every topology node to a sensor-seeded virtual DMA."""
|
||||
|
||||
all_nodes = list(node_coords)
|
||||
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
|
||||
)
|
||||
adjacency = _build_adjacency(link_entries, 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,
|
||||
available_sensors,
|
||||
node_coords,
|
||||
distance_by_sensor,
|
||||
assignment_count,
|
||||
)
|
||||
assignment_count[sensor] += 1
|
||||
area_map[node_id] = sensor_area_map[sensor]
|
||||
|
||||
return area_map, _build_area_meta(area_map, sensor_area_map)
|
||||
|
||||
|
||||
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: Mapping[str, Mapping[str, Any]],
|
||||
area_count: int,
|
||||
) -> dict[str, str]:
|
||||
if area_count >= len(sensor_nodes):
|
||||
return {sensor: str(index + 1) for index, sensor in enumerate(sensor_nodes)}
|
||||
|
||||
points = np.array(
|
||||
[
|
||||
[float(node_coords[sensor]["x"]), float(node_coords[sensor]["y"])]
|
||||
for sensor in sensor_nodes
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
centers = points[:area_count].copy()
|
||||
labels = np.full(points.shape[0], -1, dtype=int)
|
||||
for _ in range(20):
|
||||
distances_squared = (
|
||||
(points[:, None, :] - centers[None, :, :]) ** 2
|
||||
).sum(axis=2)
|
||||
next_labels = distances_squared.argmin(axis=1)
|
||||
if np.array_equal(labels, next_labels):
|
||||
break
|
||||
labels = next_labels
|
||||
for index in range(area_count):
|
||||
cluster_points = points[labels == index]
|
||||
if cluster_points.size > 0:
|
||||
centers[index] = cluster_points.mean(axis=0)
|
||||
labels = _restore_empty_area_labels(labels, points, centers, area_count)
|
||||
return {
|
||||
sensor: str(int(labels[index]) + 1)
|
||||
for index, sensor in enumerate(sensor_nodes)
|
||||
}
|
||||
|
||||
|
||||
def _restore_empty_area_labels(
|
||||
labels: np.ndarray,
|
||||
points: np.ndarray,
|
||||
centers: np.ndarray,
|
||||
area_count: int,
|
||||
) -> np.ndarray:
|
||||
"""Keep every requested area represented when coordinates are degenerate."""
|
||||
|
||||
labels = labels.copy()
|
||||
for missing_area in sorted(set(range(area_count)) - set(labels.tolist())):
|
||||
area_sizes = {
|
||||
area: int(np.count_nonzero(labels == area)) for area in range(area_count)
|
||||
}
|
||||
donor_area = max(
|
||||
(area for area, size in area_sizes.items() if size > 1),
|
||||
key=lambda area: (area_sizes[area], -area),
|
||||
)
|
||||
donor_indices = np.flatnonzero(labels == donor_area)
|
||||
replacement_index = max(
|
||||
(int(index) for index in donor_indices),
|
||||
key=lambda index: (
|
||||
float(np.sum((points[index] - centers[donor_area]) ** 2)),
|
||||
index,
|
||||
),
|
||||
)
|
||||
labels[replacement_index] = missing_area
|
||||
centers[missing_area] = points[replacement_index]
|
||||
return labels
|
||||
|
||||
|
||||
def _build_adjacency(
|
||||
link_entries: Iterable[str], all_nodes: list[str]
|
||||
) -> dict[str, set[str]]:
|
||||
adjacency: dict[str, set[str]] = {node: set() for node in all_nodes}
|
||||
for link in link_entries:
|
||||
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: Mapping[str, set[str]], start: str) -> dict[str, int]:
|
||||
distances = {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: Mapping[str, Mapping[str, Any]],
|
||||
distance_by_sensor: Mapping[str, Mapping[str, int]],
|
||||
assignment_count: Mapping[str, int],
|
||||
) -> str:
|
||||
min_distance: int | None = None
|
||||
candidates: list[str] = []
|
||||
for sensor in sensors:
|
||||
distance = distance_by_sensor.get(sensor, {}).get(node_id)
|
||||
if distance is None:
|
||||
continue
|
||||
if min_distance is None or distance < min_distance:
|
||||
min_distance = distance
|
||||
candidates = [sensor]
|
||||
elif distance == min_distance:
|
||||
candidates.append(sensor)
|
||||
if not candidates:
|
||||
node_coord = node_coords[node_id]
|
||||
return min(
|
||||
sensors,
|
||||
key=lambda sensor: math.hypot(
|
||||
float(node_coord["x"]) - float(node_coords[sensor]["x"]),
|
||||
float(node_coord["y"]) - float(node_coords[sensor]["y"]),
|
||||
),
|
||||
)
|
||||
return min(candidates, key=lambda sensor: (assignment_count[sensor], sensor))
|
||||
|
||||
|
||||
def _build_area_meta(
|
||||
area_map: Mapping[str, str], sensor_area_map: Mapping[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)
|
||||
|
||||
return [
|
||||
{
|
||||
"area_id": area_id,
|
||||
"sensor_nodes": sorted(sensors_by_area.get(area_id, [])),
|
||||
"node_ids": sorted(nodes_by_area[area_id]),
|
||||
"node_count": len(nodes_by_area[area_id]),
|
||||
}
|
||||
for area_id in sorted(nodes_by_area, key=int)
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
|
||||
|
||||
__all__ = ["PipelineHealthAnalyzer"]
|
||||
@@ -1,3 +0,0 @@
|
||||
from app.algorithms.isolation.valve import valve_isolation_analysis
|
||||
|
||||
__all__ = ["valve_isolation_analysis"]
|
||||
@@ -1,167 +0,0 @@
|
||||
from collections import defaultdict, deque
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from app.services.tjnetwork import (
|
||||
get_network_link_nodes,
|
||||
is_node,
|
||||
get_link_properties,
|
||||
)
|
||||
|
||||
|
||||
VALVE_LINK_TYPE = "valve"
|
||||
|
||||
|
||||
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
|
||||
parts = link_entry.split(":", 3)
|
||||
if len(parts) != 4:
|
||||
raise ValueError(f"Invalid link entry format: {link_entry}")
|
||||
return parts[0], parts[1], parts[2], parts[3]
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _get_network_topology(network: str):
|
||||
"""
|
||||
解析并缓存网络拓扑,大幅减少重复的 API 调用和字符串解析开销。
|
||||
返回:
|
||||
- pipe_adj: 永久连通的管道/泵邻接表 (dict[str, set])
|
||||
- all_valves: 所有阀门字典 {id: (n1, n2)}
|
||||
- link_lookup: 链路快速查表 {id: (n1, n2, type)} 用于快速定位事故点
|
||||
- node_set: 所有已知节点集合
|
||||
"""
|
||||
pipe_adj = defaultdict(set)
|
||||
all_valves = {}
|
||||
link_lookup = {}
|
||||
node_set = set()
|
||||
|
||||
# 此处假设 get_network_link_nodes 获取全网数据
|
||||
for link_entry in get_network_link_nodes(network):
|
||||
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
|
||||
link_type_name = str(link_type).lower()
|
||||
|
||||
link_lookup[link_id] = (node1, node2, link_type_name)
|
||||
node_set.add(node1)
|
||||
node_set.add(node2)
|
||||
|
||||
if link_type_name == VALVE_LINK_TYPE:
|
||||
all_valves[link_id] = (node1, node2)
|
||||
else:
|
||||
# 只有非阀门(管道/泵)才进入永久连通图
|
||||
pipe_adj[node1].add(node2)
|
||||
pipe_adj[node2].add(node1)
|
||||
|
||||
return pipe_adj, all_valves, link_lookup, node_set
|
||||
|
||||
|
||||
def valve_isolation_analysis(
|
||||
network: str, accident_elements: str | list[str], disabled_valves: list[str] = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
关阀搜索/分析:基于拓扑结构确定事故隔离所需关阀。
|
||||
:param network: 模型名称
|
||||
:param accident_elements: 事故点(节点或管道/泵/阀门ID),可以是单个ID字符串或ID列表
|
||||
:param disabled_valves: 故障/无法关闭的阀门ID列表
|
||||
:return: dict,包含受影响节点、必须关闭阀门、可选阀门等信息
|
||||
"""
|
||||
if disabled_valves is None:
|
||||
disabled_valves_set = set()
|
||||
else:
|
||||
disabled_valves_set = set(disabled_valves)
|
||||
|
||||
if isinstance(accident_elements, str):
|
||||
target_elements = [accident_elements]
|
||||
else:
|
||||
target_elements = accident_elements
|
||||
|
||||
# 1. 获取缓存拓扑 (极快,无 IO)
|
||||
pipe_adj, all_valves, link_lookup, node_set = _get_network_topology(network)
|
||||
|
||||
# 2. 确定起点,优先查表避免 API 调用
|
||||
start_nodes = set()
|
||||
for element in target_elements:
|
||||
if element in node_set:
|
||||
start_nodes.add(element)
|
||||
elif element in link_lookup:
|
||||
n1, n2, _ = link_lookup[element]
|
||||
start_nodes.add(n1)
|
||||
start_nodes.add(n2)
|
||||
else:
|
||||
# 仅当缓存中没找到时(极少见),才回退到慢速 API
|
||||
if is_node(network, element):
|
||||
start_nodes.add(element)
|
||||
else:
|
||||
props = get_link_properties(network, element)
|
||||
n1, n2 = props.get("node1"), props.get("node2")
|
||||
if n1 and n2:
|
||||
start_nodes.add(n1)
|
||||
start_nodes.add(n2)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Accident element {element} invalid or missing endpoints"
|
||||
)
|
||||
|
||||
# 3. 处理故障阀门 (构建临时增量图)
|
||||
# 我们不修改 cached pipe_adj,而是建立一个 extra_adj
|
||||
extra_adj = defaultdict(list)
|
||||
boundary_valves = {} # 当前有效的边界阀门
|
||||
|
||||
for vid, (n1, n2) in all_valves.items():
|
||||
if vid in disabled_valves_set:
|
||||
# 故障阀门:视为连通管道
|
||||
extra_adj[n1].append(n2)
|
||||
extra_adj[n2].append(n1)
|
||||
else:
|
||||
# 正常阀门:视为潜在边界
|
||||
boundary_valves[vid] = (n1, n2)
|
||||
|
||||
# 4. BFS 搜索 (叠加 pipe_adj 和 extra_adj)
|
||||
affected_nodes: set[str] = set()
|
||||
queue = deque(start_nodes)
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node in affected_nodes:
|
||||
continue
|
||||
affected_nodes.add(node)
|
||||
|
||||
# 遍历永久管道邻居
|
||||
if node in pipe_adj:
|
||||
for neighbor in pipe_adj[node]:
|
||||
if neighbor not in affected_nodes:
|
||||
queue.append(neighbor)
|
||||
|
||||
# 遍历故障阀门带来的额外邻居
|
||||
if node in extra_adj:
|
||||
for neighbor in extra_adj[node]:
|
||||
if neighbor not in affected_nodes:
|
||||
queue.append(neighbor)
|
||||
|
||||
# 5. 结果聚合
|
||||
must_close_valves: list[str] = []
|
||||
optional_valves: list[str] = []
|
||||
|
||||
for valve_id, (n1, n2) in boundary_valves.items():
|
||||
in_n1 = n1 in affected_nodes
|
||||
in_n2 = n2 in affected_nodes
|
||||
if in_n1 and in_n2:
|
||||
optional_valves.append(valve_id)
|
||||
elif in_n1 or in_n2:
|
||||
must_close_valves.append(valve_id)
|
||||
|
||||
must_close_valves.sort()
|
||||
optional_valves.sort()
|
||||
isolatable = bool(must_close_valves)
|
||||
|
||||
result = {
|
||||
"accident_elements": target_elements,
|
||||
"disabled_valves": disabled_valves,
|
||||
"affected_nodes": sorted(affected_nodes) if isolatable else [],
|
||||
"affected_node_count": len(affected_nodes),
|
||||
"must_close_valves": must_close_valves,
|
||||
"optional_valves": optional_valves,
|
||||
"isolatable": isolatable,
|
||||
}
|
||||
|
||||
if len(target_elements) == 1:
|
||||
result["accident_element"] = target_elements[0]
|
||||
|
||||
return result
|
||||
@@ -1,3 +0,0 @@
|
||||
from app.algorithms.leakage.identifier import LeakageIdentifier
|
||||
|
||||
__all__ = ["LeakageIdentifier"]
|
||||
@@ -0,0 +1,5 @@
|
||||
from app.algorithms.pipe_health_prediction.survival_predictor import (
|
||||
PipeHealthSurvivalPredictor,
|
||||
)
|
||||
|
||||
__all__ = ["PipeHealthSurvivalPredictor"]
|
||||
+8
-18
@@ -4,7 +4,7 @@ import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class PipelineHealthAnalyzer:
|
||||
class PipeHealthSurvivalPredictor:
|
||||
"""
|
||||
管道健康分析器类,使用随机生存森林模型预测管道的生存概率。
|
||||
|
||||
@@ -28,11 +28,6 @@ class PipelineHealthAnalyzer:
|
||||
"model",
|
||||
"my_survival_forest_model_quxi.joblib",
|
||||
)
|
||||
# 确保 model 目录存在
|
||||
model_dir = os.path.dirname(model_path)
|
||||
if model_dir and not os.path.exists(model_dir):
|
||||
os.makedirs(model_dir, exist_ok=True)
|
||||
|
||||
if not os.path.exists(model_path):
|
||||
raise FileNotFoundError(f"模型文件未找到: {model_path}")
|
||||
|
||||
@@ -102,7 +97,7 @@ class PipelineHealthAnalyzer:
|
||||
|
||||
# 调用说明示例
|
||||
"""
|
||||
在其他项目中使用PipelineHealthAnalyzer类的步骤:
|
||||
在其他项目中使用 PipeHealthSurvivalPredictor 类的步骤:
|
||||
|
||||
1. 安装依赖(在requirements.txt中添加):
|
||||
joblib==1.5.0
|
||||
@@ -112,34 +107,29 @@ class PipelineHealthAnalyzer:
|
||||
matplotlib==3.9.4
|
||||
|
||||
2. 导入类:
|
||||
from pipeline_health_analyzer import PipelineHealthAnalyzer
|
||||
from survival_predictor import PipeHealthSurvivalPredictor
|
||||
|
||||
3. 初始化分析器(替换为实际模型路径):
|
||||
analyzer = PipelineHealthAnalyzer(model_path='path/to/my_survival_forest_model3-10.joblib')
|
||||
predictor = PipeHealthSurvivalPredictor(model_path='path/to/model.joblib')
|
||||
|
||||
4. 准备数据(pandas DataFrame,包含9个特征列):
|
||||
4. 准备数据(pandas DataFrame,包含4个特征列):
|
||||
import pandas as pd
|
||||
data = pd.DataFrame({
|
||||
'Material': [1, 2], # 示例数据
|
||||
'Diameter': [100, 150],
|
||||
'Flow Velocity': [1.5, 2.0],
|
||||
'Pressure': [50, 60],
|
||||
'Temperature': [20, 25],
|
||||
'Precipitation': [0.1, 0.2],
|
||||
'Location': [1, 2],
|
||||
'Structural Defects': [0, 1],
|
||||
'Functional Defects': [0, 0]
|
||||
'Pressure': [50, 60]
|
||||
})
|
||||
|
||||
5. 进行预测:
|
||||
survival_funcs = analyzer.predict_survival(data)
|
||||
survival_funcs = predictor.predict_survival(data)
|
||||
|
||||
6. 查看结果(每个样本的生存概率随时间变化):
|
||||
for i, sf in enumerate(survival_funcs):
|
||||
print(f"样本 {i+1}: 时间点: {sf.x[:5]}..., 生存概率: {sf.y[:5]}...")
|
||||
|
||||
7. 可视化(可选):
|
||||
analyzer.plot_survival(survival_funcs, save_path='survival_plot.png')
|
||||
predictor.plot_survival(survival_funcs, save_path='survival_plot.png')
|
||||
|
||||
注意:
|
||||
- 数据格式必须匹配特征列表,特征值为数值型。
|
||||
@@ -0,0 +1 @@
|
||||
"""Pressure sensor placement calculation implementations."""
|
||||
@@ -0,0 +1,96 @@
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import sklearn.cluster
|
||||
import wntr
|
||||
|
||||
|
||||
class KMeansPlacement:
|
||||
def __init__(self, wn, num_monitors: int, min_diameter_mm: float):
|
||||
self.cluster_num = num_monitors
|
||||
self.wn = wn
|
||||
self.monitor_nodes: list[str] = []
|
||||
self.coords: list[tuple[float, float]] = []
|
||||
self.candidate_nodes: list[str] = []
|
||||
self.min_diameter_mm = min_diameter_mm
|
||||
|
||||
def get_junctions_coordinates(self) -> None:
|
||||
eligible_nodes: set[str] = set()
|
||||
junction_names = set(self.wn.junction_name_list)
|
||||
for pipe_name in self.wn.pipe_name_list:
|
||||
pipe = self.wn.get_link(pipe_name)
|
||||
if float(pipe.diameter) * 1000 < self.min_diameter_mm:
|
||||
continue
|
||||
eligible_nodes.update(
|
||||
node_id
|
||||
for node_id in (pipe.start_node_name, pipe.end_node_name)
|
||||
if node_id in junction_names
|
||||
)
|
||||
|
||||
for junction_name in self.wn.junction_name_list:
|
||||
if junction_name not in eligible_nodes:
|
||||
continue
|
||||
junction = self.wn.get_node(junction_name)
|
||||
self.candidate_nodes.append(junction_name)
|
||||
self.coords.append(junction.coordinates)
|
||||
|
||||
def select_monitoring_points(self) -> list[str]:
|
||||
if not self.coords:
|
||||
self.get_junctions_coordinates()
|
||||
if self.cluster_num <= 0:
|
||||
raise ValueError("sensor_count must be greater than zero")
|
||||
if self.cluster_num > len(self.candidate_nodes):
|
||||
raise ValueError("符合最小管径条件的候选节点数量少于请求的监测点数量")
|
||||
coords = np.array(self.coords)
|
||||
coordinate_span = coords.max(axis=0) - coords.min(axis=0)
|
||||
coordinate_span[coordinate_span == 0] = 1.0
|
||||
coords_normalized = (coords - coords.min(axis=0)) / coordinate_span
|
||||
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
|
||||
kmeans.fit(coords_normalized)
|
||||
|
||||
selected_indices: set[int] = set()
|
||||
for cluster_index, center in enumerate(kmeans.cluster_centers_):
|
||||
cluster_indices = np.flatnonzero(kmeans.labels_ == cluster_index)
|
||||
available_indices = [
|
||||
int(index)
|
||||
for index in cluster_indices
|
||||
if int(index) not in selected_indices
|
||||
]
|
||||
if not available_indices:
|
||||
available_indices = [
|
||||
index
|
||||
for index in range(len(self.candidate_nodes))
|
||||
if index not in selected_indices
|
||||
]
|
||||
nearest_index = min(
|
||||
available_indices,
|
||||
key=lambda index: (
|
||||
float(np.sum((coords_normalized[index] - center) ** 2)),
|
||||
index,
|
||||
),
|
||||
)
|
||||
selected_indices.add(nearest_index)
|
||||
nearest_node = self.candidate_nodes[nearest_index]
|
||||
self.monitor_nodes.append(nearest_node)
|
||||
|
||||
return self.monitor_nodes
|
||||
|
||||
def visualize_network(self) -> None:
|
||||
"""Visualize network with monitoring points."""
|
||||
wntr.graphics.plot_network(
|
||||
self.wn,
|
||||
node_attribute=self.monitor_nodes,
|
||||
node_size=30,
|
||||
title="Optimal sensor",
|
||||
)
|
||||
plt.show()
|
||||
|
||||
|
||||
def optimize_sensor_placement(
|
||||
network_model: wntr.network.WaterNetworkModel,
|
||||
sensor_count: int,
|
||||
min_diameter_mm: float,
|
||||
) -> list[str]:
|
||||
"""Select sensor nodes from an already loaded network model."""
|
||||
|
||||
placement = KMeansPlacement(network_model, sensor_count, min_diameter_mm)
|
||||
return placement.select_monitoring_points()
|
||||
-11
@@ -903,14 +903,3 @@ def optimize_sensor_placement_from_inp(
|
||||
sensor_num=sensor_num,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
|
||||
|
||||
def get_ID(name: str, sensor_num: int, min_diameter: int) -> list[str]:
|
||||
"""Compatibility entry point used by the sensor placement service."""
|
||||
|
||||
inp_path = Path("db_inp") / f"{name}.db.inp"
|
||||
return optimize_sensor_placement_from_inp(
|
||||
inp_path,
|
||||
sensor_num=sensor_num,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
"""SCADA time-series cleaning algorithms."""
|
||||
|
||||
from .flow_series import clean_flow_data_df_kf
|
||||
from .pressure_series import clean_pressure_data_df_km
|
||||
|
||||
__all__ = ["clean_flow_data_df_kf", "clean_pressure_data_df_km"]
|
||||
@@ -142,11 +142,13 @@ def clean_flow_data_kf(
|
||||
return os.path.abspath(output_path)
|
||||
|
||||
|
||||
def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict:
|
||||
def clean_flow_data_df_kf(
|
||||
data: pd.DataFrame, show_plot: bool = False
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
接收一个 DataFrame 数据结构,使用一维 Kalman 滤波平滑并用预测值替换基于 IQR 检测出的异常点。
|
||||
区分合理的0值(流量转换)和异常的0值(连续多个0或孤立0)。
|
||||
返回完整的清洗后的字典数据结构。
|
||||
返回完整的清洗后 DataFrame。
|
||||
|
||||
Args:
|
||||
data: 输入 DataFrame(可包含 time 列)
|
||||
@@ -305,42 +307,3 @@ def clean_flow_data_df_kf(data: pd.DataFrame, show_plot: bool = False) -> dict:
|
||||
|
||||
# 返回完整的修复后字典
|
||||
return cleaned_data
|
||||
|
||||
|
||||
# # 测试
|
||||
# if __name__ == "__main__":
|
||||
# # 默认:脚本目录下同名 CSV 文件
|
||||
# script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# default_csv = os.path.join(script_dir, "pipe_flow_data_to_clean2.0.csv")
|
||||
# out = clean_flow_data_kf(default_csv)
|
||||
# print("清洗后的数据已保存到:", out)
|
||||
|
||||
# 测试 clean_flow_data_dict 函数
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
|
||||
# 读取 szh_flow_scada.csv 文件
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
csv_path = os.path.join(script_dir, "szh_flow_scada.csv")
|
||||
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
|
||||
|
||||
# 排除 Time 列,随机选择 5 列
|
||||
columns_to_exclude = ["Time"]
|
||||
available_columns = [col for col in data.columns if col not in columns_to_exclude]
|
||||
selected_columns = random.sample(available_columns, 1)
|
||||
|
||||
# 将选中的列转换为字典
|
||||
data_dict = {col: data[col].tolist() for col in selected_columns}
|
||||
|
||||
print("选中的列:", selected_columns)
|
||||
print("原始数据长度:", len(data_dict[selected_columns[0]]))
|
||||
|
||||
# 调用函数进行清洗
|
||||
cleaned_dict = clean_flow_data_df_kf(data_dict, show_plot=True)
|
||||
# 将清洗后的字典写回 CSV
|
||||
out_csv = os.path.join(script_dir, f"{selected_columns[0]}_clean.csv")
|
||||
pd.DataFrame(cleaned_dict).to_csv(out_csv, index=False, encoding="utf-8-sig")
|
||||
print("已保存清洗结果到:", out_csv)
|
||||
print("清洗后的字典键:", list(cleaned_dict.keys()))
|
||||
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
|
||||
print("测试完成:函数运行正常")
|
||||
-36
@@ -541,39 +541,3 @@ def clean_pressure_data_df_km(data: pd.DataFrame, show_plot: bool = False) -> pd
|
||||
plt.show()
|
||||
|
||||
return data_repaired
|
||||
|
||||
|
||||
# 测试
|
||||
# if __name__ == "__main__":
|
||||
# # 默认使用脚本目录下的 pressure_raw_data.csv
|
||||
# script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# default_csv = os.path.join(script_dir, "pressure_raw_data.csv")
|
||||
# out_path = clean_pressure_data_km(default_csv, show_plot=False)
|
||||
# print("保存路径:", out_path)
|
||||
|
||||
# 测试 clean_pressure_data_dict_km 函数
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
|
||||
# 读取 szh_pressure_scada.csv 文件
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
csv_path = os.path.join(script_dir, "szh_pressure_scada.csv")
|
||||
data = pd.read_csv(csv_path, header=0, index_col=None, encoding="utf-8")
|
||||
|
||||
# 排除 Time 列,随机选择 5 列
|
||||
columns_to_exclude = ["Time"]
|
||||
available_columns = [col for col in data.columns if col not in columns_to_exclude]
|
||||
selected_columns = random.sample(available_columns, 5)
|
||||
|
||||
# 将选中的列转换为字典
|
||||
data_dict = {col: data[col].tolist() for col in selected_columns}
|
||||
|
||||
print("选中的列:", selected_columns)
|
||||
print("原始数据长度:", len(data_dict[selected_columns[0]]))
|
||||
|
||||
# 调用函数进行清洗
|
||||
cleaned_dict = clean_pressure_data_df_km(data_dict, show_plot=True)
|
||||
|
||||
print("清洗后的字典键:", list(cleaned_dict.keys()))
|
||||
print("清洗后的数据长度:", len(cleaned_dict[selected_columns[0]]))
|
||||
print("测试完成:函数运行正常")
|
||||
@@ -1,131 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.algorithms.sensor import kmeans as kmeans_sensor
|
||||
from app.algorithms.sensor import sensitivity
|
||||
from app.infra.db.postgresql.sensor_placement import create_sensor_placement
|
||||
from app.services.sensor_placement import (
|
||||
SensorPlacementConflictError,
|
||||
SensorPlacementValidationError,
|
||||
validate_sensor_placement_nodes,
|
||||
)
|
||||
from app.services.tjnetwork import dump_inp
|
||||
|
||||
|
||||
def _sensor_inp_path(name: str) -> Path:
|
||||
if (
|
||||
not name
|
||||
or name in {".", ".."}
|
||||
or "/" in name
|
||||
or "\\" in name
|
||||
or "\x00" in name
|
||||
):
|
||||
raise SensorPlacementValidationError("管网名称不是有效的项目标识")
|
||||
return Path("db_inp") / f"{name}.db.inp"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _sensor_inp_lock(name: str):
|
||||
inp_path = _sensor_inp_path(name)
|
||||
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(
|
||||
name: str,
|
||||
*,
|
||||
run_name: str,
|
||||
min_diameter: int,
|
||||
created_by: str,
|
||||
sensor_locations: list[str],
|
||||
) -> dict[str, Any]:
|
||||
validate_sensor_placement_nodes(name, sensor_locations)
|
||||
return create_sensor_placement(
|
||||
name,
|
||||
run_name=run_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=created_by,
|
||||
sensor_locations=sensor_locations,
|
||||
)
|
||||
|
||||
|
||||
def pressure_sensor_placement_sensitivity(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
sensor_number: int,
|
||||
min_diameter: int,
|
||||
username: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
基于改进灵敏度法进行压力监测点优化布置
|
||||
:param name: 数据库名称
|
||||
:param scheme_name: 监测优化布置方案名称
|
||||
:param sensor_number: 传感器数目
|
||||
:param min_diameter: 最小管径
|
||||
:param username: 用户名
|
||||
:return: 新建的监测点方案
|
||||
"""
|
||||
with _sensor_inp_lock(name):
|
||||
sensor_location = sensitivity.get_ID(
|
||||
name=name,
|
||||
sensor_num=sensor_number,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
return _create_validated_placement(
|
||||
name,
|
||||
run_name=scheme_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=username,
|
||||
sensor_locations=sensor_location,
|
||||
)
|
||||
|
||||
|
||||
# 2025/08/21
|
||||
# 基于kmeans聚类法进行压力监测点优化布置
|
||||
def pressure_sensor_placement_kmeans(
|
||||
name: str,
|
||||
scheme_name: str,
|
||||
sensor_number: int,
|
||||
min_diameter: int,
|
||||
username: str,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
基于聚类法进行压力监测点优化布置
|
||||
:param name: 数据库名称(注意,此处数据库名称也是inp文件名称,inp文件与pg库名要一样)
|
||||
:param scheme_name: 监测优化布置方案名称
|
||||
:param sensor_number: 传感器数目
|
||||
:param min_diameter: 最小管径
|
||||
:param username: 用户名
|
||||
:return: 新建的监测点方案
|
||||
"""
|
||||
# dump_inp
|
||||
with _sensor_inp_lock(name) as inp_path:
|
||||
dump_inp(name, str(inp_path), "2")
|
||||
sensor_location = kmeans_sensor.kmeans_sensor_placement(
|
||||
name=name,
|
||||
sensor_num=sensor_number,
|
||||
min_diameter=min_diameter,
|
||||
)
|
||||
return _create_validated_placement(
|
||||
name,
|
||||
run_name=scheme_name,
|
||||
min_diameter=min_diameter,
|
||||
created_by=username,
|
||||
sensor_locations=sensor_location,
|
||||
)
|
||||
@@ -1,71 +0,0 @@
|
||||
import wntr
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import sklearn.cluster
|
||||
import os
|
||||
|
||||
|
||||
class QD_KMeans(object):
|
||||
def __init__(self, wn, num_monitors):
|
||||
# self.inp = inp
|
||||
self.cluster_num = num_monitors # 聚类中心个数,也即测压点个数
|
||||
self.wn = wn
|
||||
self.monitor_nodes = []
|
||||
self.coords = []
|
||||
self.junction_nodes = {} # Added missing initialization
|
||||
|
||||
def get_junctions_coordinates(self):
|
||||
|
||||
for junction_name in self.wn.junction_name_list:
|
||||
junction = self.wn.get_node(junction_name)
|
||||
self.junction_nodes[junction_name] = junction.coordinates
|
||||
self.coords.append(junction.coordinates)
|
||||
|
||||
# print(f"Total junctions: {self.junction_coordinates}")
|
||||
|
||||
def select_monitoring_points(self):
|
||||
if not self.coords: # Add check if coordinates are collected
|
||||
self.get_junctions_coordinates()
|
||||
coords = np.array(self.coords)
|
||||
coords_normalized = (coords - coords.min(axis=0)) / (
|
||||
coords.max(axis=0) - coords.min(axis=0)
|
||||
)
|
||||
kmeans = sklearn.cluster.KMeans(n_clusters=self.cluster_num, random_state=42)
|
||||
kmeans.fit(coords_normalized)
|
||||
|
||||
for center in kmeans.cluster_centers_:
|
||||
distances = np.sum((coords_normalized - center) ** 2, axis=1)
|
||||
nearest_node = self.wn.junction_name_list[np.argmin(distances)]
|
||||
self.monitor_nodes.append(nearest_node)
|
||||
|
||||
return self.monitor_nodes
|
||||
|
||||
def visualize_network(self):
|
||||
"""Visualize network with monitoring points"""
|
||||
ax = wntr.graphics.plot_network(
|
||||
self.wn,
|
||||
node_attribute=self.monitor_nodes,
|
||||
node_size=30,
|
||||
title="Optimal sensor",
|
||||
)
|
||||
plt.show()
|
||||
|
||||
|
||||
def kmeans_sensor_placement(name: str, sensor_num: int, min_diameter: int) -> list:
|
||||
inp_name = f"./db_inp/{name}.db.inp"
|
||||
wn = wntr.network.WaterNetworkModel(inp_name)
|
||||
wn_cluster = QD_KMeans(wn, sensor_num)
|
||||
|
||||
# Select monitoring pointse
|
||||
sensor_ids = wn_cluster.select_monitoring_points()
|
||||
|
||||
# wn_cluster.visualize_network()
|
||||
|
||||
return sensor_ids
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# sensorindex = get_ID(name='suzhouhe_2024_cloud_0817', sensor_num=30, min_diameter=500)
|
||||
sensorindex = kmeans_sensor_placement(name="szh", sensor_num=50, min_diameter=300)
|
||||
print(sensorindex)
|
||||
@@ -1,19 +0,0 @@
|
||||
from app.algorithms.simulation.scenarios import (
|
||||
convert_to_local_unit,
|
||||
burst_analysis,
|
||||
valve_close_analysis,
|
||||
flushing_analysis,
|
||||
contaminant_simulation,
|
||||
age_analysis,
|
||||
pressure_regulation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"convert_to_local_unit",
|
||||
"burst_analysis",
|
||||
"valve_close_analysis",
|
||||
"flushing_analysis",
|
||||
"contaminant_simulation",
|
||||
"age_analysis",
|
||||
"pressure_regulation",
|
||||
]
|
||||
@@ -1,867 +0,0 @@
|
||||
import numpy as np
|
||||
from functools import wraps
|
||||
from app.services.tjnetwork import (
|
||||
ChangeSet,
|
||||
get_pattern,
|
||||
get_patterns,
|
||||
get_pump,
|
||||
get_reservoir,
|
||||
get_status,
|
||||
get_tank,
|
||||
get_time,
|
||||
read_all,
|
||||
run_project,
|
||||
set_pattern,
|
||||
set_status,
|
||||
set_tank,
|
||||
set_time,
|
||||
)
|
||||
# from get_real_status import *
|
||||
from datetime import datetime,timedelta
|
||||
from math import modf
|
||||
import json
|
||||
import pytz
|
||||
import requests
|
||||
import time
|
||||
import app.services.project_info as project_info
|
||||
from app.native.wndb.core.projects import temporary_project_database
|
||||
from app.services.time_api import parse_clock_duration_seconds
|
||||
|
||||
url_path = 'http://10.101.15.16:9000/loong' # 内网
|
||||
# url_path = 'http://183.64.62.100:9057/loong' # 外网
|
||||
url_real = url_path + '/api/mpoints/realValue'
|
||||
url_hist = url_path + '/api/curves/data'
|
||||
|
||||
PATTERN_TIME_STEP=15.0
|
||||
DN_900_ID='2498'
|
||||
DN_500_ID='3854'
|
||||
DN_1000_ID='3853'
|
||||
H_RESSURE='2510'
|
||||
L_PRESURE='2514'
|
||||
H_TANK='4780'
|
||||
L_TANK='4854'
|
||||
|
||||
H_REGION_1='SA_ZBBDJSCP000002'
|
||||
H_REGION_2='' #to do
|
||||
L_REGION_1='SA_ZBBDTJSC000001'
|
||||
L_REGION_2='SA_R00003'
|
||||
|
||||
# reservoir basic height
|
||||
RESERVOIR_BASIC_HEIGHT = float(250.35)
|
||||
|
||||
# regions
|
||||
regions = ['hp', 'lp']
|
||||
regions_demand_patterns = {'hp': ['DN900', 'DN500'], 'lp': ['DN1000']} # 出厂水量近似表示用水量
|
||||
regions_patterns = {'hp': ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
|
||||
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
|
||||
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
|
||||
'YongJinFu', 'BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
|
||||
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng', 'CaoJiaBa',
|
||||
'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
|
||||
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
|
||||
'DN500', 'DN900'],
|
||||
'lp': ['PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
|
||||
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
|
||||
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
|
||||
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
|
||||
'WanKeJinYuHuaFuYangFang', 'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
|
||||
'YueLiangTian', 'YueLiangTian200', 'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
|
||||
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
|
||||
'ZhangDouHua', 'JinYunXiaoQuDN400',
|
||||
'DN1000']}
|
||||
|
||||
# nodes
|
||||
monitor_single_patterns = ['ChuanYiJiXiao', 'BeiQuanHuaYuan', 'ZhuangYuanFuDi', 'JingNingJiaYuan',
|
||||
'308', 'JiaYinYuan', 'XinChengGuoJi', 'YiJingBeiChen', 'ZhongYangXinDu',
|
||||
'XinHaiJiaYuan', 'DongFengJie', 'DingYaXinYu', 'ZiYunTai', 'XieMaGuangChang',
|
||||
'YongJinFu', 'PanXiMingDu', 'WanKeJinYuHuaFuGaoCeng', 'KeJiXiao',
|
||||
'LuGouQiao', 'LongJiangHuaYuan', 'LaoQiZhongDui', 'ShiYanCun', 'TianQiDaSha',
|
||||
'TianShengPaiChuSuo', 'TianShengShangPin', 'JiaoTang', 'RenMinHuaYuan',
|
||||
'TaiJiBinJiangYiQi', 'TianQiHuaYuan', 'TaiJiBinJiangErQi', '122Zhong',
|
||||
'WanKeJinYuHuaFuYangFang']
|
||||
monitor_single_patterns_id = {'ChuanYiJiXiao': '7338', 'BeiQuanHuaYuan': '7315', 'ZhuangYuanFuDi': '7316',
|
||||
'JingNingJiaYuan': '7528', '308': '8272', 'JiaYinYuan': '7304',
|
||||
'XinChengGuoJi': '7325', 'YiJingBeiChen': '7328', 'ZhongYangXinDu': '7329',
|
||||
'XinHaiJiaYuan': '9138', 'DongFengJie': '7302', 'DingYaXinYu': '7331',
|
||||
'ZiYunTai': '7420,9059', 'XieMaGuangChang': '7326', 'YongJinFu': '9059',
|
||||
'PanXiMingDu': '7320', 'WanKeJinYuHuaFuGaoCeng': '7419',
|
||||
'KeJiXiao': '7305', 'LuGouQiao': '7306', 'LongJiangHuaYuan': '7318',
|
||||
'LaoQiZhongDui': '9075', 'ShiYanCun': '7309', 'TianQiDaSha': '7323',
|
||||
'TianShengPaiChuSuo': '7335', 'TianShengShangPin': '7324', 'JiaoTang': '7332',
|
||||
'RenMinHuaYuan': '7322', 'TaiJiBinJiangYiQi': '7333', 'TianQiHuaYuan': '8235',
|
||||
'TaiJiBinJiangErQi': '7334', '122Zhong': '7314', 'WanKeJinYuHuaFuYangFang': '7418'}
|
||||
|
||||
monitor_unity_patterns = ['BianDianZhan', 'BeiNanDaDao', 'TianShengLiJie', 'XueYuanXiaoQu',
|
||||
'YunHuaLu', 'GaoJiaQiao', 'LuZuoFuLuXiaDuan', 'TianRunCheng',
|
||||
'CaoJiaBa', 'PuLingChang', 'QiLongXiaoQu', 'TuanXiao',
|
||||
'ChengBeiCaiShiKou', 'WenXingShe', 'YueLiangTianBBGJCZ',
|
||||
'YueLiangTian', 'YueLiangTian200',
|
||||
'ChengTaoChang', 'HuoCheZhan', 'LiangKu', 'QunXingLu',
|
||||
'TuanShanBaoZhongShiHua', 'XieMa', 'BeiWenQuanJiuHaoErQi', 'LaiYinHuSiQi',
|
||||
'JiuYuanErTongYiYuan', 'TangDouHua', 'TaiJiBinJiangErQi(SanJi)',
|
||||
'ZhangDouHua', 'JinYunXiaoQuDN400',
|
||||
'DN500', 'DN900', 'DN1000']
|
||||
monitor_unity_patterns_id = {'BianDianZhan': '7339', 'BeiNanDaDao': '7319', 'TianShengLiJie': '8242',
|
||||
'XueYuanXiaoQu': '7327', 'YunHuaLu': '7312', 'GaoJiaQiao': '7340',
|
||||
'LuZuoFuLuXiaDuan': '7343', 'TianRunCheng': '7310', 'CaoJiaBa': '7300',
|
||||
'PuLingChang': '7307', 'QiLongXiaoQu': '7321', 'TuanXiao': '8963',
|
||||
'ChengBeiCaiShiKou': '7330', 'WenXingShe': '7311',
|
||||
'YueLiangTianBBGJCZ': '7313', 'YueLiangTian': '7313', 'YueLiangTian200': '7313',
|
||||
'ChengTaoChang': '7301', 'HuoCheZhan': '7303',
|
||||
'LiangKu': '7296', 'QunXingLu': '7308',
|
||||
'DN500': '3854', 'DN900': '2498', 'DN1000': '3853'}
|
||||
monitor_patterns = monitor_single_patterns + monitor_unity_patterns
|
||||
monitor_patterns_id = {**monitor_single_patterns_id, **monitor_unity_patterns_id}
|
||||
# pumps
|
||||
pumps_name = ['1#', '2#', '3#', '4#', '5#', '6#', '7#']
|
||||
pumps = ['PU00000', 'PU00001', 'PU00002', 'PU00003', 'PU00004', 'PU00005', 'PU00006']
|
||||
variable_frequency_pumps = ['PU00004', 'PU00005', 'PU00006']
|
||||
pumps_id = {'PU00000': '2747', 'PU00001': '2776', 'PU00002': '2730', 'PU00003': '2787',
|
||||
'PU00004': '2500', 'PU00005': '2502', 'PU00006': '2504'}
|
||||
# reservoirs
|
||||
reservoirs = ['ZBBDJSCP000002', 'R00003']
|
||||
reservoirs_id = {'ZBBDJSCP000002': '2497', 'R00003': '2571'}
|
||||
# tanks
|
||||
tanks = ['ZBBDTJSC000002', 'ZBBDTJSC000001']
|
||||
tanks_id = {'ZBBDTJSC000002': '4780', 'ZBBDTJSC000001': '9774'}
|
||||
|
||||
|
||||
class DataLoader:
|
||||
"""数据加载器"""
|
||||
def __init__(self, project_name, start_time: datetime, end_time: datetime,
|
||||
pumps_control: dict = None, tank_initial_level_control: dict = None,
|
||||
region_demand_control: dict = None, downloading_prohibition: bool = False):
|
||||
self.project_name = project_name # 数据库名
|
||||
self.current_time = self.round_time(datetime.now(pytz.timezone('Asia/Shanghai')), 1) # 圆整至整分钟
|
||||
self.current_round_time = self.round_time(self.current_time, int(PATTERN_TIME_STEP))
|
||||
self.updating_data_flag = True \
|
||||
if self.current_round_time == self.round_time(start_time, int(PATTERN_TIME_STEP)) \
|
||||
else False # 判断是否从当前时刻开始模拟(是否更新最新监测数据)
|
||||
self.downloading_prohibition = downloading_prohibition # 是否禁止下载数据(默认False: 允许下载)
|
||||
self.updating_data_flag = False if self.downloading_prohibition else self.updating_data_flag
|
||||
self.pattern_start_index = get_pattern_index(
|
||||
self.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern起始索引
|
||||
self.pattern_end_index = get_pattern_index(
|
||||
self.round_time(end_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S")) # pattern结束索引
|
||||
self.pattern_index_list = list(range(self.pattern_start_index, self.pattern_end_index + 1)) # pattern索引列表
|
||||
self.download_id = self.get_download_id() # 数据下载接口id '7338,7315,7316,...'
|
||||
self.current_time_download_data = dict(
|
||||
zip(self.download_id.split(','),
|
||||
[np.nan]*len(list(self.download_id.split(','))))
|
||||
) # {id(str): value(float)}
|
||||
self.current_time_download_data_flag = dict(
|
||||
zip(self.download_id.split(','),
|
||||
[False]*len(list(self.download_id.split(','))))
|
||||
) # 下载数据是否具备实时性, {id(str): flag(bool)}
|
||||
self.old_flow_data = self.init_dict_of_list(dict(
|
||||
zip(monitor_patterns,
|
||||
[[np.nan]] * (len(monitor_patterns)))
|
||||
)) # {pattern_name(str): flow(float)}
|
||||
self.old_pattern_factor = self.init_dict_of_list(dict(
|
||||
zip(monitor_patterns,
|
||||
[[np.nan]] * (len(monitor_patterns)))
|
||||
)) # {pattern_name(str): [pattern_factor(float)]}
|
||||
self.new_flow_data = self.init_dict_of_list(dict(
|
||||
zip(monitor_patterns,
|
||||
[[np.nan]] * (len(monitor_patterns)))
|
||||
)) # {pattern_name(str): flow(float)}
|
||||
self.new_pattern_factor = self.init_dict_of_list(dict(
|
||||
zip(monitor_patterns,
|
||||
[[np.nan]] * (len(monitor_patterns)))
|
||||
)) # {pattern_name(str): [pattern_factor(float)]}
|
||||
self.reservoir_data = dict(zip(reservoirs, [np.nan]*len(reservoirs))) # {reservoir_name(str): level(float)}
|
||||
self.tank_data = dict(zip(tanks, [np.nan] * len(tanks))) # {tank_name(str): level(float)}
|
||||
self.pump_data = self.init_dict_of_list(
|
||||
dict(zip(pumps, [[np.nan]]*len(pumps)))) # {pump_name(str): [frequency(float)]}
|
||||
self.pump_control = pumps_control # {pump_name(str): [frequency(float)]}
|
||||
self.tank_initial_level_control = tank_initial_level_control # {tank_name(str): level(float)}
|
||||
self.region_demand_current = dict(zip(regions, [0]*len(regions))) # {region_name(str): total_demand(float)}
|
||||
self.region_demand_control = region_demand_control # {region_name(str): total_demand(float)}
|
||||
self.region_demand_control_factor = dict(
|
||||
zip(regions, [1]*len(regions))) # 区域流量控制系数(用于调整用水量), {region_name(str): factor(float)}
|
||||
|
||||
def load_data(self):
|
||||
"""生成数据集"""
|
||||
self.download_data() # 下载实时数据
|
||||
self.get_old_pattern_and_flow() # 读取历史记录pattern信息
|
||||
self.cal_demand_convert_factor() # 计算用水量转换系数(设定用水量时)
|
||||
self.set_new_flow() # 设置'更新'流量
|
||||
self.set_new_pattern_factor() # 设置'更新'pattern factors
|
||||
self.set_reservoirs() # 设置清水池
|
||||
self.set_tanks() # 设置调节池
|
||||
self.set_pumps() # 设置水泵
|
||||
return self.pattern_start_index
|
||||
|
||||
def download_data(self):
|
||||
"""下载数据"""
|
||||
if self.updating_data_flag is True:
|
||||
print('{} -- Start downloading data.'.format(
|
||||
datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||
data_wait_flag = True
|
||||
while data_wait_flag:
|
||||
try:
|
||||
newest_data_time = self.download_real_data(self.download_id) # 获取实时数据
|
||||
except Exception as e:
|
||||
print('{}\nWaiting for real data.'.format(e))
|
||||
time.sleep(1)
|
||||
else:
|
||||
print('{} -- Downloading data ok. Newest timestamp: {}.'.format(
|
||||
datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
newest_data_time.strftime('%Y-%m-%d %H:%M:%S')))
|
||||
data_wait_flag = False
|
||||
|
||||
def cal_current_region_demand(self):
|
||||
"""计算区域当前用水量"""
|
||||
if self.updating_data_flag is True:
|
||||
for region in self.region_demand_current.keys():
|
||||
total_demand = 0
|
||||
for pipe in regions_demand_patterns[region]:
|
||||
total_demand += self.current_time_download_data[monitor_patterns_id[pipe]] # 出厂流量
|
||||
self.region_demand_current[region] = total_demand
|
||||
|
||||
def cal_history_region_demand(self, pattern_index_list):
|
||||
"""计算区域历史用水量(对应记录的pattern)"""
|
||||
old_demand = {}
|
||||
for region in regions:
|
||||
total_demand = 0
|
||||
for pipe_pattern_name in regions_demand_patterns[region]:
|
||||
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, pipe_pattern_name)
|
||||
for idx in pattern_index_list:
|
||||
total_demand += old_flows[idx] / 4 # 15分钟水量
|
||||
old_demand[region] = total_demand
|
||||
return old_demand
|
||||
|
||||
def cal_demand_convert_factor(self):
|
||||
"""计算用水量转换系数(设定用水量时)"""
|
||||
self.cal_current_region_demand() # 计算区域当前时刻用水量
|
||||
old_demand_moment = self.cal_history_region_demand([self.pattern_start_index]) # 计算区域目标时刻总用水量
|
||||
old_demand_period = self.cal_history_region_demand(self.pattern_index_list) # 计算区域目标时段总用水量
|
||||
for region in regions:
|
||||
self.region_demand_control_factor[region] \
|
||||
= (self.region_demand_current[region] / 4) / old_demand_moment[region] \
|
||||
if self.updating_data_flag is True else 1
|
||||
self.region_demand_control_factor[region] = self.region_demand_control[region] / old_demand_period[region] \
|
||||
if (self.region_demand_control is not None) and (region in self.region_demand_control.keys()) \
|
||||
else self.region_demand_control_factor[region]
|
||||
|
||||
def get_old_pattern_and_flow(self):
|
||||
"""获取所有pattern的选定时段的历史记录的pattern和flow"""
|
||||
for idx in monitor_patterns: # 遍历patterns
|
||||
old_flows, old_patterns = self.get_history_pattern_info(self.project_name, idx)
|
||||
for pattern_idx in self.pattern_index_list:
|
||||
old_flow_data = old_flows[pattern_idx]
|
||||
old_pattern_factor = old_patterns[pattern_idx]
|
||||
if pattern_idx == self.pattern_start_index: # 起始时刻
|
||||
self.old_flow_data[idx][0] = old_flow_data
|
||||
self.old_pattern_factor[idx][0] = old_pattern_factor
|
||||
else:
|
||||
self.old_flow_data[idx].append(old_flow_data)
|
||||
self.old_pattern_factor[idx].append(old_pattern_factor)
|
||||
|
||||
def set_new_flow(self):
|
||||
"""计算模拟时段新流量(相较于历史记录)"""
|
||||
for idx in self.new_flow_data.keys(): # 遍历patterns
|
||||
region_name = None
|
||||
for region in regions_patterns.keys():
|
||||
if idx in regions_patterns[region]:
|
||||
region_name = region # pattern所属分区
|
||||
break
|
||||
# 实时流量
|
||||
if self.updating_data_flag is True:
|
||||
if idx in monitor_unity_patterns[-3:]: # 出水管流量
|
||||
self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
|
||||
else: # 其余流量
|
||||
self.new_flow_data[idx][0] \
|
||||
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
||||
# if idx == 'ZiYunTai':
|
||||
# idx_a, idx_b = monitor_patterns_id[idx].split(',')
|
||||
# self.new_flow_data[idx][0] \
|
||||
# = self.current_time_download_data[idx_a] - self.current_time_download_data[idx_b]
|
||||
# else:
|
||||
# self.new_flow_data[idx][0] = self.current_time_download_data[monitor_patterns_id[idx]]
|
||||
# for data_id in monitor_patterns_id[idx].split(','):
|
||||
# if (self.current_time_download_data_flag[data_id] is False) \
|
||||
# and (idx not in [pipe for pipe_list in regions_demand_patterns.values()
|
||||
# for pipe in pipe_list]): # 无法获取实时数据
|
||||
# self.new_flow_data[idx][0] \
|
||||
# = self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
||||
# break
|
||||
# 根据设定用水量修改新流量
|
||||
if (self.region_demand_control is not None) \
|
||||
and (region_name in self.region_demand_control.keys()):
|
||||
for pattern_idx in self.pattern_index_list:
|
||||
if pattern_idx == self.pattern_start_index: # 起始时刻
|
||||
self.new_flow_data[idx][0] \
|
||||
= self.region_demand_control_factor[region_name] * self.old_flow_data[idx][0]
|
||||
else:
|
||||
self.new_flow_data[idx].append(
|
||||
self.region_demand_control_factor[region_name]
|
||||
* self.old_flow_data[idx][self.pattern_index_list.index(pattern_idx)]
|
||||
)
|
||||
|
||||
def set_new_pattern_factor(self):
|
||||
"""更新计算选定时段(设定用水量)/时刻的pattern factor"""
|
||||
pattern_index_list = self.pattern_index_list \
|
||||
if self.region_demand_control is not None \
|
||||
else [self.pattern_start_index]
|
||||
for idx in monitor_patterns: # 遍历patterns
|
||||
for pattern_idx in pattern_index_list: # 遍历需要修改的pattern(index)
|
||||
pattern_idx_cls = pattern_index_list.index(pattern_idx) # 转换index(类表存储结构)
|
||||
old_flow_data = self.old_flow_data[idx][pattern_idx_cls]
|
||||
old_pattern_factor = self.old_pattern_factor[idx][pattern_idx_cls]
|
||||
if pattern_idx_cls == 0: # 起始时刻
|
||||
if idx in monitor_single_patterns:
|
||||
if not np.isnan(self.new_flow_data[idx][0]):
|
||||
self.new_pattern_factor[idx][0] = (self.new_flow_data[idx][0] * 1000 / 3600) # m3/h to L/s
|
||||
if idx in monitor_unity_patterns:
|
||||
if not np.isnan(self.new_flow_data[idx][0]):
|
||||
self.new_pattern_factor[idx][0] \
|
||||
= old_pattern_factor * self.new_flow_data[idx][0] / old_flow_data
|
||||
else:
|
||||
if idx in monitor_single_patterns:
|
||||
if len(self.new_flow_data[idx]) > pattern_idx_cls:
|
||||
self.new_pattern_factor[idx].append(
|
||||
(self.new_flow_data[idx][pattern_idx_cls] * 1000 / 3600)) # m3/h to L/s
|
||||
if idx in monitor_unity_patterns:
|
||||
if len(self.new_flow_data[idx]) > pattern_idx_cls:
|
||||
self.new_pattern_factor[idx].append(
|
||||
old_pattern_factor
|
||||
* self.new_flow_data[idx][pattern_idx_cls]
|
||||
/ old_flow_data)
|
||||
|
||||
def set_reservoirs(self):
|
||||
"""设置清水池"""
|
||||
if self.updating_data_flag is True:
|
||||
for idx in self.reservoir_data.keys():
|
||||
if self.current_time_download_data_flag[reservoirs_id[idx]] is False: # 无法获取实时数据
|
||||
print('There is no current data of reservoir: {}.'.format(idx))
|
||||
else:
|
||||
self.reservoir_data[idx] \
|
||||
= self.current_time_download_data[reservoirs_id[idx]] + RESERVOIR_BASIC_HEIGHT
|
||||
|
||||
def set_tanks(self):
|
||||
"""设置调节池"""
|
||||
for idx in self.tank_data.keys():
|
||||
if self.updating_data_flag is True:
|
||||
if self.current_time_download_data_flag[tanks_id[idx]] is False: # 无法获取实时数据
|
||||
print('There is no current data of tank: {}.'.format(idx))
|
||||
else:
|
||||
self.tank_data[idx] = self.current_time_download_data[tanks_id[idx]]
|
||||
self.tank_data[idx] = self.tank_initial_level_control[idx] \
|
||||
if (self.tank_initial_level_control is not None) and (idx in self.tank_initial_level_control) \
|
||||
else self.tank_data[idx]
|
||||
|
||||
def set_pumps(self):
|
||||
"""设置水泵"""
|
||||
for idx in self.pump_data.keys():
|
||||
if self.updating_data_flag is True:
|
||||
if self.current_time_download_data_flag[pumps_id[idx]] is False: # 无法获取实时数据
|
||||
print('There is no current data of pump: {}.'.format(idx))
|
||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
||||
self.pump_data[idx] = self.pump_control[idx]
|
||||
else:
|
||||
self.pump_data[idx] = [self.current_time_download_data[pumps_id[idx]]]
|
||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
||||
self.pump_data[idx] = self.pump_data[idx] + self.pump_control[idx] \
|
||||
if len(self.pump_control[idx]) < len(self.pattern_index_list) \
|
||||
else self.pump_control[idx] # 水泵设定
|
||||
else:
|
||||
if (self.pump_control is not None) and (idx in self.pump_control.keys()):
|
||||
self.pump_data[idx] = self.pump_control[idx]
|
||||
self.pump_data[idx] \
|
||||
= list(np.array(self.pump_data[idx]) / 50) \
|
||||
if idx in variable_frequency_pumps else self.pump_data[idx]
|
||||
|
||||
def set_valves(self):
|
||||
"""设置阀门"""
|
||||
pass
|
||||
|
||||
def download_real_data(self, ids: str):
|
||||
"""加载实时数据"""
|
||||
# 数据接口的地址
|
||||
global url_real
|
||||
# 设置GET请求的参数
|
||||
params = {'ids': ids}
|
||||
# 发送GET请求获取数据
|
||||
response = requests.get(url_real, params=params)
|
||||
# 检查响应状态码,200表示请求成功
|
||||
if response.status_code == 200:
|
||||
newest_data_time = None # 下载记录数据的最新时间
|
||||
# 解析响应的JSON数据
|
||||
data = response.json()
|
||||
for realValue in data: # 取出逐个id的数据
|
||||
data_time = convert_utc_to_bj(realValue['datadt']) # datetime
|
||||
self.current_time_download_data[str(realValue['id'])] \
|
||||
= float(realValue['realValue']) # {id(str): value(float)}
|
||||
if data_time > self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 下载数据为实时数据
|
||||
self.current_time_download_data_flag[str(realValue['id'])] = True
|
||||
if newest_data_time is None:
|
||||
newest_data_time = data_time
|
||||
else:
|
||||
newest_data_time = data_time if data_time > newest_data_time else newest_data_time # 更新最新时间
|
||||
if newest_data_time <= self.current_round_time.replace(tzinfo=None) - timedelta(minutes=5): # 最新记录时间早于当前时间
|
||||
warning_text = 'There is no current data with newest timestamp: {}.'.format(
|
||||
newest_data_time.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
delta_time = self.current_round_time.replace(tzinfo=None) - newest_data_time
|
||||
if delta_time < timedelta(minutes=PATTERN_TIME_STEP): # 时间接近(可等待再次下载)
|
||||
raise Exception(warning_text)
|
||||
else:
|
||||
print(warning_text)
|
||||
self.updating_data_flag = False
|
||||
else:
|
||||
for idx in monitor_unity_patterns[-3:]: # 出水管流量
|
||||
if self.current_time_download_data_flag[monitor_patterns_id[idx]] is False: # 无法获取出水管流量的实时数据
|
||||
print('There is no current data of outflow: {}.'.format(idx))
|
||||
self.updating_data_flag = False
|
||||
if self.updating_data_flag is False:
|
||||
print('Abandon updating data with downloaded data.')
|
||||
return newest_data_time
|
||||
else:
|
||||
# 如果请求不成功,打印错误信息
|
||||
print("请求失败,状态码:", response.status_code)
|
||||
raise ConnectionError('Cannot download data.')
|
||||
|
||||
@ staticmethod
|
||||
def init_dict_of_list(dict_of_list):
|
||||
"""初始化值为列表的字典(重新生成列表地址, 防止指向同一列表)"""
|
||||
for idx in dict_of_list.keys():
|
||||
dict_of_list[idx] = dict_of_list[idx].copy()
|
||||
return dict_of_list
|
||||
|
||||
@ staticmethod
|
||||
def get_download_id():
|
||||
"""生成下载数据项的id"""
|
||||
# id_list = (list(monitor_single_patterns_id.values())
|
||||
# + list(monitor_unity_patterns_id.values())
|
||||
# + list(tanks_id.values())
|
||||
# + list(reservoirs_id.values())
|
||||
# + list(pumps_id.values()))
|
||||
id_list = (list(monitor_unity_patterns_id.values())[-3:]
|
||||
+ list(tanks_id.values())
|
||||
+ list(reservoirs_id.values())
|
||||
+ list(pumps_id.values()))
|
||||
id_list = sorted(set(id_list), key=id_list.index)
|
||||
if None in id_list:
|
||||
id_list.remove(None)
|
||||
return ','.join(id_list)
|
||||
|
||||
@ staticmethod
|
||||
def get_history_pattern_info(project_name, pattern_name):
|
||||
"""读取选定pattern的保存的历史pattern信息(flow, factor)"""
|
||||
factors_list = []
|
||||
flow_list = []
|
||||
patterns_info = read_all(
|
||||
project_name,
|
||||
"select flow, factor from network.pattern_flow_samples "
|
||||
"where pattern_id = %s order by sequence_no",
|
||||
(pattern_name,),
|
||||
)
|
||||
for item in patterns_info:
|
||||
flow_list.append(float(item['flow']))
|
||||
factors_list.append(float(item['factor']))
|
||||
return flow_list, factors_list
|
||||
|
||||
@ staticmethod
|
||||
def judge_time(current_time, time_index_list):
|
||||
"""时间判断"""
|
||||
current_index \
|
||||
= time_index_list.index(current_time) if (current_time in time_index_list) else None
|
||||
return current_index
|
||||
|
||||
@staticmethod
|
||||
def get_time_index_list(start_time: datetime, end_time: datetime, step: int):
|
||||
"""生成时间索引"""
|
||||
time_index_list = [] # 时间索引[str]
|
||||
time_index = start_time
|
||||
while time_index <= end_time:
|
||||
time_index_list.append(time_index)
|
||||
time_index += timedelta(minutes=step)
|
||||
return time_index_list
|
||||
|
||||
@ staticmethod
|
||||
def round_time(time_: datetime, interval=5):
|
||||
"""时间向下取整到整n分钟(北京时间): 四舍六入五留双/向下取整"""
|
||||
# return datetime.fromtimestamp(round(time_.timestamp() / (60 * interval)) * (60 * interval))
|
||||
return datetime.fromtimestamp(int((time_.timestamp()) // (60 * interval)) * (60 * interval))
|
||||
|
||||
|
||||
def convert_utc_to_bj(utc_time_str):
|
||||
"""将utc时间(str)转换成北京时间(datetime)"""
|
||||
# 解析UTC时间字符串为datetime对象
|
||||
utc_time = datetime.strptime(utc_time_str, '%Y-%m-%dT%H:%M:%SZ')
|
||||
# 设定UTC时区
|
||||
utc_timezone = pytz.timezone('UTC')
|
||||
# 转换为北京时间
|
||||
beijing_timezone = pytz.timezone('Asia/Shanghai')
|
||||
beijing_time = utc_time.replace(tzinfo=utc_timezone).astimezone(beijing_timezone).replace(tzinfo=None)
|
||||
return beijing_time
|
||||
|
||||
|
||||
def get_datetime(cur_datetime:str):
|
||||
str_format = "%Y-%m-%d %H:%M:%S"
|
||||
return datetime.strptime(cur_datetime, str_format)
|
||||
|
||||
|
||||
def get_strftime(cur_datetime: datetime):
|
||||
str_format = "%Y-%m-%d %H:%M:%S"
|
||||
return cur_datetime.strftime(str_format)
|
||||
|
||||
|
||||
def step_time(cur_datetime:str, step=5):
|
||||
str_format="%Y-%m-%d %H:%M:%S"
|
||||
dt=datetime.strptime(cur_datetime,str_format)
|
||||
dt=dt+timedelta(minutes=step)
|
||||
return datetime.strftime(dt,str_format)
|
||||
|
||||
|
||||
def get_pattern_index(cur_datetime:str)->int:
|
||||
str_format="%Y-%m-%d %H:%M:%S"
|
||||
dt=datetime.strptime(cur_datetime,str_format)
|
||||
hr=dt.hour
|
||||
mnt=dt.minute
|
||||
i=int((hr*60+mnt)/PATTERN_TIME_STEP)
|
||||
return i
|
||||
|
||||
def get_pattern_index_str(cur_datetime:str)->str:
|
||||
i=get_pattern_index(cur_datetime)
|
||||
[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))
|
||||
hrN_str=hrN_str.zfill(2)
|
||||
str_i='{}:{}:00'.format(hrN_str,minN_str)
|
||||
return str_i
|
||||
|
||||
|
||||
def from_seconds_to_clock (secs: int)->str:
|
||||
hrs=int(secs/3600)
|
||||
minutes=int((secs-hrs*3600)/60)
|
||||
seconds=(secs-hrs*3600-minutes*60)
|
||||
hrs_str=str(hrs).zfill(2)
|
||||
minutes_str=str(minutes).zfill(2)
|
||||
seconds_str=str(seconds).zfill(2)
|
||||
str_clock='{}:{}:{}'.format(hrs_str,minutes_str,seconds_str)
|
||||
return str_clock
|
||||
|
||||
def from_clock_to_seconds (clock: str)->int:
|
||||
str_format="%Y-%m-%d %H:%M:%S"
|
||||
dt=datetime.strptime(clock,str_format)
|
||||
hr=dt.hour
|
||||
mnt=dt.minute
|
||||
seconds=dt.second
|
||||
return hr*3600+mnt*60+seconds
|
||||
|
||||
def from_clock_to_seconds_2 (clock: str)->int:
|
||||
return parse_clock_duration_seconds(clock)
|
||||
|
||||
|
||||
def from_clock_to_seconds_3 (clock: str)->int:
|
||||
return parse_clock_duration_seconds(clock)
|
||||
|
||||
|
||||
###convert datetimestring
|
||||
##"XXXX-XX-XXT00:00:00Z" ->"XXXX-XX-XX 00:00:00"
|
||||
def trim_time_flag(url_date_time:str)->str:
|
||||
str_datetime=str.replace(url_date_time,'T',' ')
|
||||
str_datetime=str.replace(str_datetime,'Z','')
|
||||
return str_datetime
|
||||
|
||||
# 单时间步长模拟
|
||||
def run_simulation(name:str,start_datetime:str,end_datetime:str=None, duration:int=900)->str:
|
||||
#get_current_data(cur_datetime)
|
||||
#extract the patternindex from datetime
|
||||
#e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
|
||||
start_datetime=trim_time_flag(start_datetime)
|
||||
if(end_datetime!=None):
|
||||
end_datetime=trim_time_flag(end_datetime)
|
||||
|
||||
|
||||
|
||||
## redistribute the basedemand according to the currentTotalQ and the base_totalQ
|
||||
# step 1. get_real _data
|
||||
if end_datetime==None or start_datetime==end_datetime:
|
||||
end_datetime=step_time(start_datetime)
|
||||
|
||||
# # ids=['2498','3854','3853','2510','2514','4780','4854']
|
||||
# # real_data=get_real_data(ids,start_datetime,end_datetime)
|
||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--获取实时数据完毕\n")
|
||||
# #step 2. re-distribute the real q to base demand of the node region_sa by region_sa
|
||||
# regions=get_all_service_area_ids(name)
|
||||
# total_demands={}
|
||||
# for region in regions:
|
||||
# total_demands[region]=get_total_base_demand(name,region)
|
||||
# region_demand_factor={}
|
||||
# #Region_ID:SA_ZBBDJSCP000002 高区;SA_R00003+SA_ZBBDTJSC000001 低区
|
||||
# H_region_real_demands=real_data[DN_900_ID][start_datetime]+real_data[DN_500_ID][start_datetime]
|
||||
# L_region_real_demands=real_data[DN_1000_ID][start_datetime]
|
||||
# factor_H_zone=H_region_real_demands/total_demands[H_REGION_1]/3.6 #3.6: m3/h->L/s
|
||||
# factor_L_zone=L_region_real_demands/(total_demands[L_REGION_1]+total_demands[L_REGION_2])/3.6
|
||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--流量因子计算完毕完毕\n")
|
||||
# for region in regions:
|
||||
# region_nodes=get_nodes_in_region(name,region)
|
||||
# factor=1
|
||||
# if region==H_REGION_1 or H_REGION_2:
|
||||
# factor=factor_H_zone
|
||||
# else:
|
||||
# factor=factor_L_zone
|
||||
#
|
||||
# for node in region_nodes:
|
||||
# d=get_demand(name,node)
|
||||
# for r in d['demands']:
|
||||
# r['demand']=factor*r['demand']
|
||||
# cs=ChangeSet()
|
||||
# cs.append(d)
|
||||
# set_demand(name,cs)
|
||||
#
|
||||
# #
|
||||
# #
|
||||
# print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+"--节点流量重分配完毕\n")
|
||||
|
||||
#step 3. set pattern index to the current time,and set duration to 300 secs
|
||||
#
|
||||
str_pattern_start=get_pattern_index_str(start_datetime)
|
||||
dic_time=get_time(name)
|
||||
dic_time['PATTERN START']=str_pattern_start
|
||||
|
||||
if duration !=None:
|
||||
dic_time['DURATION']=from_seconds_to_clock(duration)
|
||||
else:
|
||||
dic_time['DURATION']=dic_time['HYDRAULIC TIMESTEP']
|
||||
cs=ChangeSet()
|
||||
cs.operations.append(dic_time)
|
||||
set_time(name,cs)
|
||||
|
||||
# step4. run simulation and save the result to name-time.out for download
|
||||
#inp_file = 'inp\\'+name+'.inp'
|
||||
#db_name=name
|
||||
#dump_inp(db_name,inp_file,'2')
|
||||
# result=run_inp(db_name)
|
||||
result=run_project(name)
|
||||
#json string format
|
||||
# simulation_result, output, report
|
||||
result_data=json.loads(result)
|
||||
#print(result_data['simulation_result'])
|
||||
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S")+'run finished successfully\n')
|
||||
#print(result_data['report'])
|
||||
return result
|
||||
|
||||
|
||||
# 在线模拟
|
||||
def _clean_extended_simulation(func):
|
||||
@wraps(func)
|
||||
def wrapper(name: str, simulation_type: str, *args, **kwargs):
|
||||
if simulation_type.upper() != "EXTENDED":
|
||||
return func(name, simulation_type, *args, **kwargs)
|
||||
with temporary_project_database(name, "extended_simulation") as temporary:
|
||||
kwargs["_temporary_project"] = temporary
|
||||
return func(name, simulation_type, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@_clean_extended_simulation
|
||||
def run_simulation_ex(name: str, simulation_type: str, start_datetime: str,
|
||||
end_datetime: str = None, duration: int = 0,
|
||||
pump_control: dict[str, list] = None, tank_initial_level_control: dict[str, float] = None,
|
||||
region_demand_control: dict[str, float] = None, valve_control: dict[str, dict] = None,
|
||||
downloading_prohibition: bool = False,
|
||||
_temporary_project: str | None = None) -> str:
|
||||
time_cost_start = time.perf_counter()
|
||||
print('{} -- Hydraulic simulation started.'.format(
|
||||
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S')))
|
||||
|
||||
if simulation_type.upper() == 'REALTIME': # 实时模拟(修改原数据库)
|
||||
name_c = name
|
||||
elif simulation_type.upper() == 'EXTENDED': # 扩展模拟(复制数据库)
|
||||
if _temporary_project is None:
|
||||
raise RuntimeError("Extended simulation isolation was not prepared")
|
||||
name_c = _temporary_project
|
||||
else:
|
||||
raise Exception('Incorrect simulation type, choose in (realtime, extended)')
|
||||
|
||||
# 时间处理
|
||||
# extract the pattern index from datetime
|
||||
# e.g. 0: the first time step for 00:00-00:14; 1: the second step for 00:15-00:30
|
||||
# start_datetime = get_strftime(convert_utc_to_bj(start_datetime))
|
||||
start_datetime = trim_time_flag(start_datetime)
|
||||
if end_datetime is not None:
|
||||
# end_datetime = get_strftime(convert_utc_to_bj(end_datetime))
|
||||
end_datetime = trim_time_flag(end_datetime)
|
||||
|
||||
# pump name转化/输入值规范化
|
||||
if pump_control is not None:
|
||||
for key in list(pump_control.keys()):
|
||||
pump_control[key] = [pump_control[key]] if type(pump_control[key]) is not list else pump_control[key]
|
||||
pump_control[pumps[pumps_name.index(key)]] = pump_control.pop(key)
|
||||
|
||||
# 重新分配节点(nodes)水量
|
||||
# 1) (single)base_demand_new=1, pattern_new=real_data
|
||||
# 2) (unity)base_demand_new=base_demand_old, pattern_new=factor*pattern_old(factor=flow_new/flow_old)
|
||||
# 获取需水量数据
|
||||
# a) 历史pattern对应水量(读取保存数据库)
|
||||
# b) 实时水量(数据接口下载)
|
||||
|
||||
# 修改node demand = 1, pattern factor *= demand(monitor single patterns对应node)
|
||||
# nodes = get_nodes(name_c) # nodes
|
||||
# for node_name in nodes: # 遍历nodes
|
||||
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
|
||||
# for demands in demands_dict['demands']:
|
||||
# if (demands['pattern'] in monitor_single_patterns) and (demands['demand'] != 1): # 1)
|
||||
# pattern = get_pattern(name_c, demands['pattern'])
|
||||
# pattern['factors'] = list(demands['demand'] * np.array(pattern['factors'])) # 修改pattern
|
||||
# cs = ChangeSet()
|
||||
# cs.append(pattern)
|
||||
# set_pattern(name_c, cs)
|
||||
# demands_dict['demands'][
|
||||
# demands_dict['demands'].index(demands)
|
||||
# ]['demand'] = 1 # 修改demand
|
||||
# cs = ChangeSet()
|
||||
# cs.append(demands_dict)
|
||||
# set_demand(name_c, cs)
|
||||
|
||||
start_time = get_datetime(start_datetime) # datetime
|
||||
end_time = get_datetime(end_datetime) \
|
||||
if end_datetime is not None \
|
||||
else get_datetime(start_datetime) + timedelta(seconds=duration) # datetime
|
||||
# modify_pattern_start_index = get_pattern_index(start_datetime) # 待修改pattern的起始索引(int)
|
||||
|
||||
dataset_loader = DataLoader(project_name=name_c,
|
||||
start_time=start_time, end_time=end_time,
|
||||
pumps_control=pump_control, tank_initial_level_control=tank_initial_level_control,
|
||||
region_demand_control=region_demand_control,
|
||||
downloading_prohibition=downloading_prohibition) # 实例化数据加载器
|
||||
modify_index \
|
||||
= dataset_loader.load_data() # 加载数据(index: 需要修改pattern的factor index, None: 无需修改除水泵和调节池外pattern)
|
||||
new_patterns \
|
||||
= dataset_loader.new_pattern_factor # {name: float,} pattern factor(实时: 更新, 其他: 保持/更新(设定用水量时))
|
||||
tank_init_level = dataset_loader.tank_data # {name: float,} 调节池初始液位(实时: 更新, 其他: 保持/更新(设定液位时))
|
||||
reservoir_level = dataset_loader.reservoir_data # {name: float,} 水库液位(实时: 更新, 其他: 保持)
|
||||
pump_freq = dataset_loader.pump_data # {name: [float,]} 水泵频率(实时: 更新, 其他: 保持/更新(设定状态时))
|
||||
print(datetime.now(pytz.timezone('Asia/Shanghai')).strftime("%Y-%m-%d %H:%M:%S") + " -- Loading data ok.\n")
|
||||
|
||||
pattern_name_list = get_patterns(name_c) # 所有pattern
|
||||
# 修改node pattern/demand
|
||||
# nodes = get_nodes(name_c) # nodes
|
||||
# for node_name in nodes: # 遍历nodes
|
||||
# demands_dict = get_demand(name_c, node_name) # {'demands':[{'demand':, 'pattern':}]}
|
||||
# for demands in demands_dict['demands']:
|
||||
# if demands['pattern'] in monitor_single_patterns: # 1)
|
||||
# demands_dict['demands'][
|
||||
# demands_dict['demands'].index(demands)
|
||||
# ]['demand'] = 1 # 修改demand
|
||||
# pattern = get_pattern(name_c, demands['pattern'])
|
||||
# pattern['factors'][modify_index] = flow_new[demands['pattern']] # 修改pattern
|
||||
# cs = ChangeSet()
|
||||
# cs.append(pattern)
|
||||
# set_pattern(name_c, cs)
|
||||
# if demands['pattern'] in pattern_name_list:
|
||||
# pattern_name_list.remove(demands['pattern']) # 移出待修改pattern列表
|
||||
# else: # 2)
|
||||
# continue
|
||||
# cs = ChangeSet()
|
||||
# cs.append(demands_dict)
|
||||
# set_demand(name_c, cs)
|
||||
for pattern_name in monitor_patterns: # 遍历patterns
|
||||
if not np.isnan(new_patterns[pattern_name][0]):
|
||||
pattern = get_pattern(name_c, pattern_name)
|
||||
pattern['factors'][modify_index:
|
||||
modify_index + len(new_patterns[pattern_name])] \
|
||||
= new_patterns[pattern_name]
|
||||
cs = ChangeSet()
|
||||
cs.append(pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if pattern_name in pattern_name_list:
|
||||
pattern_name_list.remove(pattern_name) # 移出待修改pattern列表
|
||||
|
||||
# 修改清水池(reservoir)液位pattern
|
||||
for reservoir_name in reservoirs: # 遍历reservoirs
|
||||
if (not np.isnan(reservoir_level[reservoir_name])) and (reservoir_level[reservoir_name] != 0):
|
||||
reservoir_pattern = get_pattern(name_c, get_reservoir(name_c, reservoir_name)['pattern'])
|
||||
reservoir_pattern['factors'][modify_index] = reservoir_level[reservoir_name]
|
||||
cs = ChangeSet()
|
||||
cs.append(reservoir_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if reservoir_pattern['id'] in pattern_name_list:
|
||||
pattern_name_list.remove(reservoir_pattern['id']) # 移出待修改pattern列表
|
||||
|
||||
# 修改调节池(tank)初始液位
|
||||
for tank_name in tanks: # 遍历tanks
|
||||
if (not np.isnan(tank_init_level[tank_name])) and (tank_init_level[tank_name] != 0):
|
||||
tank = get_tank(name_c, tank_name)
|
||||
tank['init_level'] = tank_init_level[tank_name]
|
||||
cs = ChangeSet()
|
||||
cs.append(tank)
|
||||
set_tank(name_c, cs)
|
||||
|
||||
# 修改水泵(pump)pattern
|
||||
for pump_name in pumps: # 遍历pumps
|
||||
if not np.isnan(pump_freq[pump_name][0]):
|
||||
pump_pattern = get_pattern(name_c, get_pump(name_c, pump_name)['pattern'])
|
||||
pump_pattern['factors'][modify_index
|
||||
:modify_index + len(pump_freq[pump_name])] \
|
||||
= pump_freq[pump_name]
|
||||
cs = ChangeSet()
|
||||
cs.append(pump_pattern)
|
||||
set_pattern(name_c, cs)
|
||||
if pump_pattern['id'] in pattern_name_list:
|
||||
pattern_name_list.remove(pump_pattern['id']) # 移出待修改pattern列表
|
||||
# 修改阀门(valve)status和setting
|
||||
if valve_control is not None:
|
||||
for valve in valve_control.keys():
|
||||
status = get_status(name_c, valve)
|
||||
if 'status' in valve_control[valve].keys():
|
||||
status['status'] = valve_control[valve]['status']
|
||||
if 'setting' in valve_control[valve].keys():
|
||||
status['setting'] = valve_control[valve]['setting']
|
||||
if 'k' in valve_control[valve].keys():
|
||||
valve_k = valve_control[valve]['k']
|
||||
if valve_k == 0:
|
||||
status['status'] = 'CLOSED'
|
||||
else:
|
||||
status['setting'] = 0.1036 * pow(valve_k, -3.105)
|
||||
cs = ChangeSet()
|
||||
cs.append(status)
|
||||
set_status(name_c, cs)
|
||||
|
||||
print('Finish demands amending, unmodified patterns: {}.'.format(pattern_name_list))
|
||||
|
||||
# 修改时间信息
|
||||
str_pattern_start = get_pattern_index_str(
|
||||
DataLoader.round_time(start_time, int(PATTERN_TIME_STEP)).strftime("%Y-%m-%d %H:%M:%S"))
|
||||
dic_time = get_time(name_c)
|
||||
dic_time['PATTERN START'] = str_pattern_start
|
||||
|
||||
if duration is not None:
|
||||
dic_time['DURATION'] = from_seconds_to_clock(duration)
|
||||
else:
|
||||
dic_time['DURATION'] = dic_time['HYDRAULIC TIMESTEP']
|
||||
cs = ChangeSet()
|
||||
cs.operations.append(dic_time)
|
||||
set_time(name_c, cs)
|
||||
|
||||
# 运行并返回结果
|
||||
result = run_project(name_c)
|
||||
|
||||
time_cost_end = time.perf_counter()
|
||||
print('{} -- Hydraulic simulation finished, cost time: {:.2f} s.'.format(
|
||||
datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S'),
|
||||
time_cost_end - time_cost_start))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# if get_current_data()==True:
|
||||
# tQ=get_current_total_Q()
|
||||
# print(f"the current tQ is {tQ}\n")
|
||||
# data=get_hist_data(ids,conver_beingtime_to_ucttime('2024-04-10 15:05:00'),conver_beingtime_to_ucttime('2024-04-10 15:10:00'))
|
||||
# read_inp("beibeizone","beibeizone-export_nochinese.inp")
|
||||
# run_simulation("beibeizone","2024-04-01T08:00:00Z")
|
||||
# read_inp('bb_server', 'model20_en.inp')
|
||||
run_simulation_ex(
|
||||
name=project_info.name, simulation_type='extended', start_datetime='2024-11-09T02:30:00Z',
|
||||
# end_datetime='2024-05-30T16:00:00Z',
|
||||
# duration=0,
|
||||
# pump_control={'PU00006': [45, 40]}
|
||||
# region_demand_control={'hp': 6000, 'lp': 2000}
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.algorithms.valve_isolation.topology_search import valve_isolation_analysis
|
||||
|
||||
__all__ = ["valve_isolation_analysis"]
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Topology-only valve isolation search."""
|
||||
|
||||
from collections import defaultdict, deque
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
VALVE_LINK_TYPE = "valve"
|
||||
|
||||
|
||||
def _parse_link_entry(link_entry: str) -> tuple[str, str, str, str]:
|
||||
parts = link_entry.split(":", 3)
|
||||
if len(parts) != 4:
|
||||
raise ValueError(f"Invalid link entry format: {link_entry}")
|
||||
return parts[0], parts[1], parts[2], parts[3]
|
||||
|
||||
|
||||
def valve_isolation_analysis(
|
||||
link_entries: Iterable[str],
|
||||
accident_elements: str | list[str],
|
||||
disabled_valves: list[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Determine boundary valves and affected nodes from a topology snapshot."""
|
||||
|
||||
disabled_valves_set = set(disabled_valves or [])
|
||||
target_elements = (
|
||||
[accident_elements]
|
||||
if isinstance(accident_elements, str)
|
||||
else accident_elements
|
||||
)
|
||||
|
||||
pipe_adj: dict[str, set[str]] = defaultdict(set)
|
||||
all_valves: dict[str, tuple[str, str]] = {}
|
||||
link_lookup: dict[str, tuple[str, str, str]] = {}
|
||||
node_set: set[str] = set()
|
||||
for link_entry in link_entries:
|
||||
link_id, link_type, node1, node2 = _parse_link_entry(link_entry)
|
||||
link_type_name = str(link_type).lower()
|
||||
link_lookup[link_id] = (node1, node2, link_type_name)
|
||||
node_set.update((node1, node2))
|
||||
if link_type_name == VALVE_LINK_TYPE:
|
||||
all_valves[link_id] = (node1, node2)
|
||||
else:
|
||||
pipe_adj[node1].add(node2)
|
||||
pipe_adj[node2].add(node1)
|
||||
|
||||
start_nodes: set[str] = set()
|
||||
for element in target_elements:
|
||||
if element in node_set:
|
||||
start_nodes.add(element)
|
||||
elif element in link_lookup:
|
||||
node1, node2, _ = link_lookup[element]
|
||||
start_nodes.update((node1, node2))
|
||||
else:
|
||||
raise ValueError(f"Accident element {element} was not found in topology")
|
||||
|
||||
extra_adj: dict[str, list[str]] = defaultdict(list)
|
||||
boundary_valves: dict[str, tuple[str, str]] = {}
|
||||
for valve_id, (node1, node2) in all_valves.items():
|
||||
if valve_id in disabled_valves_set:
|
||||
extra_adj[node1].append(node2)
|
||||
extra_adj[node2].append(node1)
|
||||
else:
|
||||
boundary_valves[valve_id] = (node1, node2)
|
||||
|
||||
affected_nodes: set[str] = set()
|
||||
queue = deque(start_nodes)
|
||||
while queue:
|
||||
node = queue.popleft()
|
||||
if node in affected_nodes:
|
||||
continue
|
||||
affected_nodes.add(node)
|
||||
queue.extend(pipe_adj.get(node, set()) - affected_nodes)
|
||||
queue.extend(
|
||||
neighbor
|
||||
for neighbor in extra_adj.get(node, ())
|
||||
if neighbor not in affected_nodes
|
||||
)
|
||||
|
||||
must_close_valves: list[str] = []
|
||||
optional_valves: list[str] = []
|
||||
for valve_id, (node1, node2) in boundary_valves.items():
|
||||
node1_affected = node1 in affected_nodes
|
||||
node2_affected = node2 in affected_nodes
|
||||
if node1_affected and node2_affected:
|
||||
optional_valves.append(valve_id)
|
||||
elif node1_affected or node2_affected:
|
||||
must_close_valves.append(valve_id)
|
||||
|
||||
must_close_valves.sort()
|
||||
optional_valves.sort()
|
||||
isolatable = bool(must_close_valves)
|
||||
result: dict[str, Any] = {
|
||||
"accident_elements": target_elements,
|
||||
"disabled_valves": disabled_valves,
|
||||
"affected_nodes": sorted(affected_nodes) if isolatable else [],
|
||||
"affected_node_count": len(affected_nodes),
|
||||
"must_close_valves": must_close_valves,
|
||||
"optional_valves": optional_valves,
|
||||
"isolatable": isolatable,
|
||||
}
|
||||
if len(target_elements) == 1:
|
||||
result["accident_element"] = target_elements[0]
|
||||
return result
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Water-demand distribution algorithms."""
|
||||
|
||||
from .service import (
|
||||
calculate_demand_to_network,
|
||||
calculate_demand_to_nodes,
|
||||
calculate_demand_to_region,
|
||||
distribute_demand_to_nodes,
|
||||
distribute_demand_to_region,
|
||||
get_total_base_demand,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"calculate_demand_to_network",
|
||||
"calculate_demand_to_nodes",
|
||||
"calculate_demand_to_region",
|
||||
"distribute_demand_to_nodes",
|
||||
"distribute_demand_to_region",
|
||||
"get_total_base_demand",
|
||||
]
|
||||
@@ -1,104 +0,0 @@
|
||||
from app.native.wndb.commands.executor import execute_batch_command
|
||||
from app.native.wndb.core.database import ChangeSet
|
||||
from app.native.wndb.gis.region_geometry import Topology, get_nodes_in_region
|
||||
from app.native.wndb.gis.network_views import (
|
||||
get_junction_demands,
|
||||
sum_junction_base_demand,
|
||||
)
|
||||
from app.native.wndb.model.elements import get_nodes
|
||||
|
||||
|
||||
DISTRIBUTION_TYPE_ADD = 'ADD'
|
||||
DISTRIBUTION_TYPE_OVERRIDE = 'OVERRIDE'
|
||||
|
||||
|
||||
def calculate_demand_to_nodes(name: str, demand: float, nodes: list[str]) -> dict[str, float]:
|
||||
if len(nodes) == 0 or demand == 0.0:
|
||||
return {}
|
||||
|
||||
topology = Topology(name, nodes)
|
||||
t_nodes = topology.nodes()
|
||||
t_links = topology.links()
|
||||
|
||||
length_sum = 0.0
|
||||
for value in t_links.values():
|
||||
length_sum += abs(value['length'])
|
||||
|
||||
if length_sum <= 0.0:
|
||||
return {}
|
||||
|
||||
demand_per_length = demand / length_sum
|
||||
|
||||
result: dict[str, float] = {}
|
||||
for node, value in t_nodes.items():
|
||||
if value["type"] != "junction":
|
||||
continue
|
||||
demand_per_node = 0.0
|
||||
for link in value['links']:
|
||||
demand_per_node += abs(t_links[link]['length']) * demand_per_length * 0.5
|
||||
result[node] = demand_per_node
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def calculate_demand_to_region(name: str, demand: float, region: str) -> dict[str, float]:
|
||||
nodes = get_nodes_in_region(name, region)
|
||||
return calculate_demand_to_nodes(name, demand, nodes)
|
||||
|
||||
|
||||
def calculate_demand_to_network(name: str, demand: float) -> dict[str, float]:
|
||||
nodes = get_nodes(name)
|
||||
return calculate_demand_to_nodes(name, demand, nodes)
|
||||
|
||||
|
||||
def distribute_demand_to_nodes(name: str, demand: float, nodes: list[str], type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet:
|
||||
if len(nodes) == 0 or demand == 0.0:
|
||||
return ChangeSet()
|
||||
if type != DISTRIBUTION_TYPE_ADD and type != DISTRIBUTION_TYPE_OVERRIDE:
|
||||
return ChangeSet()
|
||||
|
||||
topology = Topology(name, nodes)
|
||||
t_nodes = topology.nodes()
|
||||
t_links = topology.links()
|
||||
|
||||
length_sum = 0.0
|
||||
for value in t_links.values():
|
||||
length_sum += abs(value['length'])
|
||||
|
||||
if length_sum <= 0.0:
|
||||
return ChangeSet()
|
||||
|
||||
demand_per_length = demand / length_sum
|
||||
|
||||
cs = ChangeSet()
|
||||
demands_by_junction = get_junction_demands(
|
||||
name,
|
||||
[node for node, value in t_nodes.items() if value["type"] == "junction"],
|
||||
)
|
||||
|
||||
for node, value in t_nodes.items():
|
||||
if value["type"] != "junction":
|
||||
continue
|
||||
demand_per_node = 0.0
|
||||
for link in value['links']:
|
||||
demand_per_node += abs(t_links[link]['length']) * demand_per_length * 0.5
|
||||
|
||||
ds = demands_by_junction.get(node, [])
|
||||
if len(ds) == 0:
|
||||
ds = [{'demand': demand_per_node, 'pattern': None, 'category': None}]
|
||||
elif type == DISTRIBUTION_TYPE_ADD:
|
||||
ds[0]['demand'] += demand_per_node
|
||||
else:
|
||||
ds[0]['demand'] = demand_per_node
|
||||
cs.update({'type': 'demand', 'junction': node, 'demands': ds})
|
||||
|
||||
return execute_batch_command(name, cs)
|
||||
|
||||
|
||||
def distribute_demand_to_region(name: str, demand: float, region: str, type: str = DISTRIBUTION_TYPE_ADD) -> ChangeSet:
|
||||
nodes = get_nodes_in_region(name, region)
|
||||
return distribute_demand_to_nodes(name, demand, nodes, type)
|
||||
|
||||
def get_total_base_demand(name: str, region: str) -> float:
|
||||
nodes = get_nodes_in_region(name, region)
|
||||
return sum_junction_base_demand(name, nodes)
|
||||
@@ -4,6 +4,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.burst_detection import (
|
||||
@@ -73,6 +74,10 @@ async def detect_burst(
|
||||
HTTPException: 当处理过程中发生错误时
|
||||
"""
|
||||
try:
|
||||
return run_burst_detection(**data.model_dump(), username=username)
|
||||
return await run_in_threadpool(
|
||||
run_burst_detection,
|
||||
**data.model_dump(),
|
||||
username=username,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -6,6 +6,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.burst_location import (
|
||||
@@ -63,6 +64,10 @@ async def locate_burst(
|
||||
HTTPException: 当数据类型或值不正确时
|
||||
"""
|
||||
try:
|
||||
return run_burst_location_by_network(**data.model_dump(), username=username)
|
||||
return await run_in_threadpool(
|
||||
run_burst_location_by_network,
|
||||
**data.model_dump(),
|
||||
username=username,
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -3,34 +3,49 @@ from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Body
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
from app.services.leakage_identifier import (
|
||||
from app.services.dma_leakage_estimation import (
|
||||
run_leakage_identification,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
DEFAULT_N_WORKERS = max(1, min((os.cpu_count() or 1) - 1, 4))
|
||||
MAX_POPULATION_SIZE = 1_000
|
||||
MAX_GENERATIONS = 1_000
|
||||
MAX_DURATION_HOURS = 168
|
||||
|
||||
|
||||
class LeakageIdentifyRequest(BaseModel):
|
||||
"""漏损识别请求模型"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||
observed_pressure_data: str | dict[str, list[Any]] | list[dict[str, Any]] | None = Field(
|
||||
None, description="观测的压力数据"
|
||||
observed_pressure_data: dict[str, list[Any]] | list[dict[str, Any]] | None = (
|
||||
Field(None, description="观测的压力数据;文件路径不属于公共 API 输入")
|
||||
)
|
||||
start_time: float = Field(0, description="起始时间(小时)")
|
||||
duration: float = Field(24, description="持续时间(小时)")
|
||||
timestep: float = Field(5, description="时间步长(分钟)")
|
||||
q_sum: float = Field(0.2, description="总流量(m3/s)")
|
||||
start_time: float = Field(0, ge=0, description="起始时间(小时)")
|
||||
duration: float = Field(
|
||||
24, gt=0, le=MAX_DURATION_HOURS, description="持续时间(小时)"
|
||||
)
|
||||
timestep: float = Field(5, gt=0, le=1440, description="时间步长(分钟)")
|
||||
q_sum: float = Field(0.2, ge=0, description="总流量(m3/s)")
|
||||
q_sum_unit: str = Field("m3/s", description="流量单位")
|
||||
output_dir: str = Field("db_inp", description="输出目录")
|
||||
pop_size: int = Field(50, description="种群大小")
|
||||
max_gen: int = Field(100, description="最大代数")
|
||||
n_workers: int = Field(DEFAULT_N_WORKERS, description="工作线程数")
|
||||
pop_size: int = Field(
|
||||
50, ge=2, le=MAX_POPULATION_SIZE, description="种群大小"
|
||||
)
|
||||
max_gen: int = Field(100, ge=1, le=MAX_GENERATIONS, description="最大代数")
|
||||
n_workers: int = Field(
|
||||
DEFAULT_N_WORKERS,
|
||||
ge=1,
|
||||
le=DEFAULT_N_WORKERS,
|
||||
description="工作进程数",
|
||||
)
|
||||
output_flow_unit: str = Field("m3/s", description="输出流量单位")
|
||||
dma_count: int | None = Field(None, description="DMA区域数量")
|
||||
dma_count: int | None = Field(None, ge=1, description="DMA区域数量")
|
||||
scada_start: datetime | None = Field(None, description="SCADA数据起始时间")
|
||||
scada_end: datetime | None = Field(None, description="SCADA数据结束时间")
|
||||
sensor_nodes: list[str] | None = Field(None, description="传感器节点列表")
|
||||
@@ -63,6 +78,10 @@ async def identify_leakage(
|
||||
HTTPException: 当处理过程中发生错误时
|
||||
"""
|
||||
try:
|
||||
return run_leakage_identification(**data.model_dump(), username=username)
|
||||
return await run_in_threadpool(
|
||||
run_leakage_identification,
|
||||
**data.model_dump(),
|
||||
username=username,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
|
||||
@@ -7,10 +7,6 @@ from fastapi import APIRouter, Depends, HTTPException, Path, Query, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from app.algorithms.sensor import (
|
||||
pressure_sensor_placement_kmeans,
|
||||
pressure_sensor_placement_sensitivity,
|
||||
)
|
||||
from app.auth.metadata_dependencies import get_current_metadata_user
|
||||
from app.auth.project_dependencies import (
|
||||
ProjectContext,
|
||||
@@ -34,6 +30,8 @@ from app.services.sensor_placement import (
|
||||
get_sensor_placement_candidate,
|
||||
get_sensor_placement_run,
|
||||
list_sensor_placement_runs,
|
||||
optimize_sensor_placement_by_kmeans,
|
||||
optimize_sensor_placement_by_sensitivity,
|
||||
update_sensor_placement_run,
|
||||
)
|
||||
|
||||
@@ -136,18 +134,18 @@ async def optimize_sensor_placement_scheme(
|
||||
network = _project_network(payload.network, project_context)
|
||||
_require_project_write(project_context)
|
||||
optimizer = (
|
||||
pressure_sensor_placement_sensitivity
|
||||
optimize_sensor_placement_by_sensitivity
|
||||
if payload.method == "sensitivity"
|
||||
else pressure_sensor_placement_kmeans
|
||||
else optimize_sensor_placement_by_kmeans
|
||||
)
|
||||
try:
|
||||
created = await run_in_threadpool(
|
||||
optimizer,
|
||||
name=network,
|
||||
scheme_name=payload.run_name,
|
||||
sensor_number=payload.sensor_count,
|
||||
project_code=network,
|
||||
run_name=payload.run_name,
|
||||
sensor_count=payload.sensor_count,
|
||||
min_diameter=payload.min_diameter,
|
||||
username=current_user.username,
|
||||
created_by=current_user.username,
|
||||
)
|
||||
run = get_sensor_placement_run(network, created["run_id"])
|
||||
return {**run, "can_edit": True}
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
from typing import Any, List, Literal, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import threading
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, Body
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from app.auth.keycloak_dependencies import get_current_keycloak_username
|
||||
import app.services.simulation as simulation
|
||||
from app.services.tjnetwork import (
|
||||
run_project,
|
||||
run_project_return_dict,
|
||||
run_inp,
|
||||
dump_output,
|
||||
)
|
||||
from app.algorithms.simulation.scenarios import (
|
||||
from app.services.simulation_scenarios import (
|
||||
burst_analysis,
|
||||
valve_close_analysis,
|
||||
flushing_analysis,
|
||||
contaminant_simulation,
|
||||
age_analysis,
|
||||
# scheduling_analysis,
|
||||
pressure_regulation,
|
||||
)
|
||||
from app.services.simulation_ops import (
|
||||
project_management,
|
||||
scheduling_simulation,
|
||||
daily_scheduling_simulation,
|
||||
)
|
||||
from app.services.valve_isolation import analyze_valve_isolation
|
||||
from app.services.time_api import (
|
||||
from app.domain.time import (
|
||||
parse_aware_time,
|
||||
parse_clock_duration_seconds,
|
||||
parse_utc_time,
|
||||
@@ -49,57 +38,13 @@ class RunSimulationManuallyByDate(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class BurstAnalysis(BaseModel):
|
||||
name: str = Field(..., description="管网名称(或数据库名称)")
|
||||
modify_pattern_start_time: str = Field(..., description="模式修改开始时间 (ISO 8601)")
|
||||
burst_ID: List[str] | str | None = Field(None, description="爆管节点/管段ID列表")
|
||||
burst_size: List[float] | float | int | None = Field(None, description="爆管流量大小")
|
||||
modify_total_duration: int = Field(900, description="模拟总时长 (秒)")
|
||||
modify_fixed_pump_pattern: Optional[dict[str, list]] = Field(None, description="定速泵模式修改")
|
||||
modify_variable_pump_pattern: Optional[dict[str, list]] = Field(None, description="变速泵模式修改")
|
||||
modify_valve_opening: Optional[dict[str, float]] = Field(None, description="阀门开度修改")
|
||||
scheme_name: Optional[str] = Field(None, description="方案名称")
|
||||
|
||||
|
||||
class SchedulingAnalysis(BaseModel):
|
||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||
start_time: str = Field(..., description="开始时间")
|
||||
pump_control: dict = Field(..., description="泵控制策略")
|
||||
tank_id: str = Field(..., description="水箱ID")
|
||||
water_plant_output_id: str = Field(..., description="水厂出水ID")
|
||||
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
|
||||
|
||||
|
||||
class PressureRegulation(BaseModel):
|
||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||
start_time: str = Field(..., description="开始时间")
|
||||
pump_control: dict = Field(..., description="泵控制策略")
|
||||
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
|
||||
duration: Optional[int] = Field(900, description="持续时间 (秒)")
|
||||
scheme_name: Optional[str] = Field(None, description="方案名称")
|
||||
|
||||
|
||||
class ProjectManagement(BaseModel):
|
||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||
start_time: str = Field(..., description="开始时间")
|
||||
pump_control: dict = Field(..., description="泵控制策略")
|
||||
tank_init_level: Optional[dict] = Field(None, description="水箱初始水位")
|
||||
region_demand: Optional[dict] = Field(None, description="区域需水量控制")
|
||||
|
||||
|
||||
class DailySchedulingAnalysis(BaseModel):
|
||||
network: str = Field(..., description="管网名称(或数据库名称)")
|
||||
start_time: str = Field(..., description="开始时间")
|
||||
pump_control: dict = Field(..., description="泵控制策略")
|
||||
reservoir_id: str = Field(..., description="水库ID")
|
||||
tank_id: str = Field(..., description="水箱ID")
|
||||
water_plant_output_id: str = Field(..., description="水厂出水ID")
|
||||
time_delta: Optional[int] = Field(300, description="时间步长 (秒)")
|
||||
|
||||
|
||||
class PumpFailureState(BaseModel):
|
||||
time: str = Field(..., description="故障发生时间")
|
||||
pump_status: dict = Field(..., description="泵状态字典")
|
||||
scheme_name: str = Field(..., min_length=1, description="方案名称")
|
||||
|
||||
|
||||
def run_simulation_manually_by_date(
|
||||
@@ -161,41 +106,15 @@ def run_project_return_dict_endpoint(network: str = Query(..., description="管
|
||||
return run_project_return_dict(network)
|
||||
|
||||
|
||||
# put in inp folder, name without extension
|
||||
@router.post("/inp-runs", summary="运行INP文件", description="运行指定INP文件格式的管网模型进行水力模拟。INP文件应该放在inp文件夹中,参数为文件名不含扩展名。")
|
||||
def run_inp_endpoint(network: str = Query(..., description="inp文件名(不含扩展名)")) -> str:
|
||||
"""
|
||||
运行INP文件
|
||||
|
||||
- **network**: inp文件名(不含扩展名)
|
||||
|
||||
从inp文件夹中读取指定的INP文件并运行模拟。
|
||||
"""
|
||||
return run_inp(network)
|
||||
|
||||
|
||||
# path is absolute path
|
||||
@router.get("/outputs", summary="导出模拟输出", description="导出指定路径的模拟输出文件内容。参数应为绝对路径。")
|
||||
def dump_output_endpoint(output: str = Query(..., description="模拟输出文件的绝对路径")) -> str:
|
||||
"""
|
||||
导出模拟输出
|
||||
|
||||
- **output**: 模拟输出文件的绝对路径
|
||||
|
||||
读取并返回指定路径的模拟输出内容。
|
||||
"""
|
||||
return dump_output(output)
|
||||
|
||||
|
||||
# Analysis Endpoints
|
||||
@router.post("/burst-analyses", summary="爆管分析(高级)", description="高级版本的爆管分析,支持在指定时间点修改泵控制模式和阀门开度,以分析这些改变对爆管影响的作用。支持固定泵和变速泵的独立控制。")
|
||||
def fastapi_burst_analysis(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
modify_pattern_start_time: str = Query(..., description="模式修改开始时间(ISO 8601格式)"),
|
||||
burst_ID: list[str] = Query(..., description="爆管节点/管段ID列表"),
|
||||
burst_size: list[float] = Query(..., description="对应各爆管点的爆管流量大小列表(L/s)"),
|
||||
modify_total_duration: int = Query(..., description="模拟总时长(秒)"),
|
||||
scheme_name: str = Query(..., description="分析方案名称"),
|
||||
burst_ID: list[str] = Query(..., min_length=1, description="爆管节点/管段ID列表"),
|
||||
burst_size: list[float] = Query(..., min_length=1, description="对应各爆管点的爆管流量大小列表(L/s)"),
|
||||
modify_total_duration: int = Query(..., gt=0, description="模拟总时长(秒)"),
|
||||
scheme_name: str = Query(..., min_length=1, description="分析方案名称"),
|
||||
username: str = Depends(get_current_keycloak_username),
|
||||
) -> str:
|
||||
"""
|
||||
@@ -210,6 +129,11 @@ def fastapi_burst_analysis(
|
||||
|
||||
支持在指定时间修改泵控制模式和阀门开度。
|
||||
"""
|
||||
if len(burst_ID) != len(burst_size):
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="burst_id 与 burst_size 的数量必须一致",
|
||||
)
|
||||
burst_analysis(
|
||||
name=network,
|
||||
modify_pattern_start_time=modify_pattern_start_time,
|
||||
@@ -442,48 +366,6 @@ def fastapi_contaminant_simulation(
|
||||
return result or "success"
|
||||
|
||||
|
||||
@router.post("/water-age-analyses", response_class=PlainTextResponse, summary="水龄分析(高级)", description="高级版本的水龄分析,在指定时间点进行分析,支持自定义模拟持续时间。返回纯文本格式的分析结果。")
|
||||
def fastapi_age_analysis(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
start_time: str = Query(..., description="分析开始时间(ISO 8601格式)"),
|
||||
duration: int = Query(..., description="模拟持续时间(秒)"),
|
||||
) -> str:
|
||||
"""
|
||||
水龄分析(高级版本)
|
||||
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **start_time**: 分析开始时间
|
||||
- **duration**: 模拟持续时间(秒)
|
||||
|
||||
分析指定时间段内管网中各节点的水体停留时间。
|
||||
"""
|
||||
result = age_analysis(network, start_time, duration)
|
||||
return result or "success"
|
||||
|
||||
|
||||
# @router.get("/schedulinganalysis/")
|
||||
# async def scheduling_analysis_endpoint(network: str):
|
||||
# return scheduling_analysis(network)
|
||||
|
||||
|
||||
@router.post("/pressure-regulation-calculations", summary="压力调节(基础)", description="对管网的压力进行调节分析,通过控制泵的运行来维持目标节点的目标压力。此为基础版本。")
|
||||
def pressure_regulation_endpoint(
|
||||
network: str = Query(..., description="管网名称(或数据库名称)"),
|
||||
target_node: str = Query(..., description="目标节点ID"),
|
||||
target_pressure: float = Query(..., description="目标压力值(kPa)"),
|
||||
):
|
||||
"""
|
||||
压力调节(基础版本)
|
||||
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **target_node**: 目标节点ID
|
||||
- **target_pressure**: 目标压力值(kPa)
|
||||
|
||||
通过泵控制维持目标节点的压力。
|
||||
"""
|
||||
return pressure_regulation(network, target_node, target_pressure)
|
||||
|
||||
|
||||
@router.post("/pressure-regulation-analyses", summary="压力调节(高级)", description="高级版本的压力调节分析,通过JSON请求体提供详细的控制参数,包括固定泵和变速泵的独立控制、水箱初始水位等。")
|
||||
def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description="压力调节控制参数")) -> str:
|
||||
"""
|
||||
@@ -495,7 +377,7 @@ def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description
|
||||
- **pump_control**: 泵控制策略字典
|
||||
- **tank_init_level**: 水箱初始水位字典(可选)
|
||||
- **duration**: 模拟持续时间(秒,可选,默认900)
|
||||
- **scheme_name**: 控制方案名称(可选)
|
||||
- **scheme_name**: 控制方案名称
|
||||
|
||||
支持固定泵和变速泵的独立控制。
|
||||
"""
|
||||
@@ -525,137 +407,6 @@ def fastapi_pressure_regulation(data: PressureRegulation = Body(..., description
|
||||
return "success"
|
||||
|
||||
|
||||
@router.post("/project-managements", summary="项目管理(高级)", description="高级版本的项目管理,通过JSON请求体提供详细的控制参数,包括泵控制策略、水箱初始水位和区域需水量控制。")
|
||||
def fastapi_project_management(data: ProjectManagement = Body(..., description="项目管理控制参数")) -> str:
|
||||
"""
|
||||
项目管理(高级版本)
|
||||
|
||||
请求体参数:
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **start_time**: 管理开始时间
|
||||
- **pump_control**: 泵控制策略字典
|
||||
- **tank_init_level**: 水箱初始水位字典(可选)
|
||||
- **region_demand**: 区域需水量控制字典(可选)
|
||||
|
||||
支持多维度的项目管理。
|
||||
"""
|
||||
item = data.model_dump()
|
||||
return project_management(
|
||||
prj_name=item["network"],
|
||||
start_datetime=item["start_time"],
|
||||
pump_control=item["pump_control"],
|
||||
tank_initial_level_control=item["tank_init_level"],
|
||||
region_demand_control=item["region_demand"],
|
||||
)
|
||||
|
||||
|
||||
# @router.get("/dailyschedulinganalysis/")
|
||||
# async def daily_scheduling_analysis_endpoint(network: str):
|
||||
# return daily_scheduling_analysis(network)
|
||||
|
||||
|
||||
@router.post("/scheduling-analyses", summary="排程分析", description="对管网的供水排程进行分析,优化泵的运行时间和出水流量,平衡水厂出水、水箱进出水,满足用户需求。")
|
||||
def fastapi_scheduling_analysis(data: SchedulingAnalysis = Body(..., description="排程分析参数")) -> str:
|
||||
"""
|
||||
排程分析
|
||||
|
||||
请求体参数:
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **start_time**: 分析开始时间
|
||||
- **pump_control**: 泵控制策略字典
|
||||
- **tank_id**: 水箱ID
|
||||
- **water_plant_output_id**: 水厂出水ID
|
||||
- **time_delta**: 时间步长(秒,可选,默认300)
|
||||
|
||||
用于优化供水排程。
|
||||
"""
|
||||
item = data.model_dump()
|
||||
return scheduling_simulation(
|
||||
item["network"],
|
||||
item["start_time"],
|
||||
item["pump_control"],
|
||||
item["tank_id"],
|
||||
item["water_plant_output_id"],
|
||||
item["time_delta"],
|
||||
)
|
||||
|
||||
|
||||
@router.post("/daily-scheduling-analyses", summary="日排程分析", description="对管网的每日供水排程进行分析,优化水库、水厂、水箱和用户需求的协调,制定合理的每日排程方案。")
|
||||
def fastapi_daily_scheduling_analysis(data: DailySchedulingAnalysis = Body(..., description="日排程分析参数")) -> str:
|
||||
"""
|
||||
日排程分析
|
||||
|
||||
请求体参数:
|
||||
- **network**: 管网名称(或数据库名称)
|
||||
- **start_time**: 分析开始时间
|
||||
- **pump_control**: 泵控制策略字典
|
||||
- **reservoir_id**: 水库ID
|
||||
- **tank_id**: 水箱ID
|
||||
- **water_plant_output_id**: 水厂出水ID
|
||||
- **time_delta**: 时间步长(秒,可选,默认300)
|
||||
|
||||
用于制定每日供水排程方案。
|
||||
"""
|
||||
item = data.model_dump()
|
||||
return daily_scheduling_simulation(
|
||||
item["network"],
|
||||
item["start_time"],
|
||||
item["pump_control"],
|
||||
item["reservoir_id"],
|
||||
item["tank_id"],
|
||||
item["water_plant_output_id"],
|
||||
)
|
||||
|
||||
|
||||
# @router.get("/pumpfailure/")
|
||||
# async def pump_failure_endpoint(network: str, pump_id: str, time: str):
|
||||
# return pump_failure(network, pump_id, time)
|
||||
|
||||
|
||||
@router.post("/pump-failure-events", summary="泵故障管理", description="记录和管理泵的故障状态,包括故障发生时间和受影响的泵列表。系统将记录故障日志并更新泵状态。")
|
||||
def fastapi_pump_failure(data: PumpFailureState = Body(..., description="泵故障状态信息")) -> str:
|
||||
"""
|
||||
泵故障管理
|
||||
|
||||
请求体参数:
|
||||
- **time**: 故障发生时间
|
||||
- **pump_status**: 泵状态字典,包含第一阶段和第二阶段泵的故障状态
|
||||
|
||||
系统将验证泵信息的有效性并更新故障状态文件。
|
||||
"""
|
||||
item = data.model_dump()
|
||||
with open("./pump_failure_message.txt", "a", encoding="utf-8-sig") as f1:
|
||||
f1.write("[{}] {}\n".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), item))
|
||||
with open("./pump_failure_status.txt", "r", encoding="utf-8-sig") as f2:
|
||||
lines = f2.readlines()
|
||||
first_stage_pump_status_dict = json.loads(json.dumps(eval(lines[0])))
|
||||
second_stage_pump_status_dict = json.loads(json.dumps(eval(lines[-1])))
|
||||
pump_status_dict = {
|
||||
"first": first_stage_pump_status_dict,
|
||||
"second": second_stage_pump_status_dict,
|
||||
}
|
||||
status_info = item.copy()
|
||||
for pump_type in status_info["pump_status"].keys():
|
||||
if pump_type in pump_status_dict.keys():
|
||||
if all(
|
||||
pump_id in pump_status_dict[pump_type].keys()
|
||||
for pump_id in status_info["pump_status"][pump_type].keys()
|
||||
):
|
||||
for pump_id in status_info["pump_status"][pump_type].keys():
|
||||
pump_status_dict[pump_type][pump_id] = int(
|
||||
status_info["pump_status"][pump_type][pump_id]
|
||||
)
|
||||
else:
|
||||
return json.dumps("ERROR: Wrong Pump ID")
|
||||
else:
|
||||
return json.dumps("ERROR: Wrong Pump Type")
|
||||
with open("./pump_failure_status.txt", "w", encoding="utf-8-sig") as f2_:
|
||||
f2_.write(
|
||||
"{}\n{}".format(pump_status_dict["first"], pump_status_dict["second"])
|
||||
)
|
||||
return json.dumps("SUCCESS")
|
||||
|
||||
|
||||
@router.post("/simulation-runs", summary="手动运行日期指定模拟", description="根据指定的开始时间和持续时间,手动运行水力模拟。开始时间必须是显式带时区的 ISO 8601 / RFC3339 时间。")
|
||||
def fastapi_run_simulation_manually_by_date(
|
||||
data: RunSimulationManuallyByDate = Body(..., description="模拟运行参数"),
|
||||
|
||||
@@ -3,7 +3,7 @@ from datetime import datetime
|
||||
from psycopg import AsyncConnection
|
||||
from uuid import UUID
|
||||
|
||||
from app.infra.db.timescaledb.composite_queries import CompositeQueries
|
||||
from app.services.timeseries_analysis import TimeseriesAnalysisService
|
||||
from .dependencies import get_timescale_connection, get_postgres_connection
|
||||
|
||||
router = APIRouter()
|
||||
@@ -46,7 +46,7 @@ async def get_scada_associated_simulation_data(
|
||||
)
|
||||
|
||||
if run_id is not None:
|
||||
result = await CompositeQueries.get_scada_associated_analysis_simulation_data(
|
||||
result = await TimeseriesAnalysisService.get_scada_associated_analysis_simulation_data(
|
||||
timescale_conn,
|
||||
postgres_conn,
|
||||
device_ids_list,
|
||||
@@ -56,7 +56,7 @@ async def get_scada_associated_simulation_data(
|
||||
)
|
||||
else:
|
||||
result = (
|
||||
await CompositeQueries.get_scada_associated_realtime_simulation_data(
|
||||
await TimeseriesAnalysisService.get_scada_associated_realtime_simulation_data(
|
||||
timescale_conn,
|
||||
postgres_conn,
|
||||
device_ids_list,
|
||||
@@ -116,7 +116,7 @@ async def get_feature_simulation_data(
|
||||
raise HTTPException(status_code=400, detail="feature_infos cannot be empty")
|
||||
|
||||
if run_id is not None:
|
||||
result = await CompositeQueries.get_analysis_simulation_data(
|
||||
result = await TimeseriesAnalysisService.get_analysis_simulation_data(
|
||||
timescale_conn,
|
||||
feature_infos_list,
|
||||
start_time,
|
||||
@@ -124,7 +124,7 @@ async def get_feature_simulation_data(
|
||||
run_id,
|
||||
)
|
||||
else:
|
||||
result = await CompositeQueries.get_realtime_simulation_data(
|
||||
result = await TimeseriesAnalysisService.get_realtime_simulation_data(
|
||||
timescale_conn,
|
||||
feature_infos_list,
|
||||
start_time,
|
||||
@@ -168,7 +168,7 @@ async def get_element_associated_scada_data(
|
||||
HTTPException: 当查询参数无效时返回400错误,未找到关联数据返回404错误
|
||||
"""
|
||||
try:
|
||||
result = await CompositeQueries.get_element_associated_scada_data(
|
||||
result = await TimeseriesAnalysisService.get_element_associated_scada_data(
|
||||
timescale_conn, postgres_conn, element_id, start_time, end_time, use_cleaned
|
||||
)
|
||||
if result is None:
|
||||
@@ -216,7 +216,7 @@ async def clean_scada_data(
|
||||
if device_ids
|
||||
else []
|
||||
)
|
||||
return await CompositeQueries.clean_scada_data(
|
||||
return await TimeseriesAnalysisService.clean_scada_data(
|
||||
timescale_conn, postgres_conn, device_ids_list, start_time, end_time
|
||||
)
|
||||
except ValueError as e:
|
||||
@@ -246,7 +246,7 @@ async def predict_pipeline_health(
|
||||
HTTPException: 当模型文件不存在返回404错误,其他错误返回400或500错误
|
||||
"""
|
||||
try:
|
||||
return await CompositeQueries.predict_pipeline_health(
|
||||
return await TimeseriesAnalysisService.predict_pipeline_health(
|
||||
timescale_conn, postgres_conn, query_time
|
||||
)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Read repositories for project network assets."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from psycopg import AsyncConnection
|
||||
|
||||
|
||||
class NetworkAssetRepository:
|
||||
@staticmethod
|
||||
async def get_pipes_by_ids(
|
||||
conn: AsyncConnection,
|
||||
pipe_ids: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not pipe_ids:
|
||||
return []
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"""
|
||||
SELECT id, diameter, start_node_id AS node1,
|
||||
end_node_id AS node2
|
||||
FROM gis.pipes
|
||||
WHERE id = ANY(%s)
|
||||
ORDER BY id
|
||||
""",
|
||||
(pipe_ids,),
|
||||
)
|
||||
return await cur.fetchall()
|
||||
@@ -1 +1 @@
|
||||
from .composite_queries import CompositeQueries
|
||||
"""TimescaleDB repositories and connection infrastructure."""
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.infra.db.timescaledb.sync_pool import timescale_connection
|
||||
from app.infra.db.timescaledb.repositories.analysis import AnalysisResultsRepository
|
||||
from app.infra.db.timescaledb.repositories.realtime import RealtimeRepository
|
||||
from app.infra.db.timescaledb.repositories.scada import ScadaRepository
|
||||
from app.services.time_api import parse_utc_time
|
||||
from app.domain.time import parse_utc_time
|
||||
|
||||
|
||||
class InternalStorage:
|
||||
|
||||
@@ -4,7 +4,7 @@ from uuid import UUID
|
||||
|
||||
from psycopg import AsyncConnection, Connection, sql
|
||||
|
||||
from app.services.time_api import parse_utc_time
|
||||
from app.domain.time import parse_utc_time
|
||||
|
||||
|
||||
class AnalysisResultsRepository:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List, Any, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from collections import defaultdict
|
||||
from psycopg import AsyncConnection, Connection, sql
|
||||
from app.services.time_api import parse_utc_time
|
||||
from app.domain.time import parse_utc_time
|
||||
|
||||
|
||||
class RealtimeRepository:
|
||||
|
||||
@@ -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)
|
||||
@@ -8,33 +8,27 @@ from math import pi, sqrt
|
||||
import pytz
|
||||
|
||||
import app.services.simulation as simulation
|
||||
from app.algorithms.simulation.runner import (
|
||||
run_simulation_ex,
|
||||
from_clock_to_seconds_2,
|
||||
)
|
||||
from app.native.wndb.core.projects import temporary_project_database
|
||||
from app.services.tjnetwork import (
|
||||
ChangeSet,
|
||||
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,
|
||||
SOURCE_TYPE_SETPOINT,
|
||||
add_pattern,
|
||||
add_source,
|
||||
get_demand,
|
||||
get_emitter,
|
||||
get_node_links,
|
||||
get_option,
|
||||
get_pattern,
|
||||
get_pipe,
|
||||
get_source,
|
||||
get_time,
|
||||
is_junction,
|
||||
set_demand,
|
||||
set_emitter,
|
||||
set_option,
|
||||
set_source,
|
||||
set_time,
|
||||
)
|
||||
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):
|
||||
@@ -283,7 +277,7 @@ def valve_close_analysis(
|
||||
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_type="valve_close_analysis",
|
||||
scheme_name=scheme_name,
|
||||
result_db_name=name,
|
||||
)
|
||||
@@ -371,7 +365,7 @@ def flushing_analysis(
|
||||
# 新建 pattern
|
||||
time_option = get_time(new_name)
|
||||
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
|
||||
secs = from_clock_to_seconds_2(hydraulic_step)
|
||||
secs = parse_clock_duration_seconds(hydraulic_step)
|
||||
cs_pattern = ChangeSet()
|
||||
pt = {}
|
||||
factors = []
|
||||
@@ -511,7 +505,7 @@ def contaminant_simulation(
|
||||
set_time(new_name, cs) # set QUALITY TIMESTEP
|
||||
time_option = get_time(new_name)
|
||||
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
|
||||
secs = from_clock_to_seconds_2(hydraulic_step)
|
||||
secs = parse_clock_duration_seconds(hydraulic_step)
|
||||
operation_step = 0
|
||||
# step 1. set duration
|
||||
if modify_total_duration == None:
|
||||
@@ -590,51 +584,6 @@ def contaminant_simulation(
|
||||
# execute_undo(name)
|
||||
|
||||
|
||||
############################################################
|
||||
# age analysis 05 ***水龄模拟目前还没和实时模拟打通,不确定是否需要,先不要使用***
|
||||
############################################################
|
||||
|
||||
|
||||
def age_analysis(
|
||||
name: str, modify_pattern_start_time: str, modify_total_duration: int = 900
|
||||
) -> None:
|
||||
"""
|
||||
水龄模拟
|
||||
:param name: 模型名称,数据库中对应的名字
|
||||
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
|
||||
:param modify_total_duration: 模拟总历时,秒
|
||||
:return:
|
||||
"""
|
||||
print(
|
||||
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
|
||||
+ " -- Start Analysis."
|
||||
)
|
||||
with temporary_project_database(name, "age_analysis") as new_name:
|
||||
result = run_simulation_ex(
|
||||
new_name,
|
||||
"realtime",
|
||||
modify_pattern_start_time,
|
||||
duration=modify_total_duration,
|
||||
downloading_prohibition=True,
|
||||
)
|
||||
simulation_result = json.loads(result)
|
||||
output_data = simulation_result.get("output")
|
||||
if not isinstance(output_data, dict):
|
||||
raise RuntimeError("run_simulation_ex did not return JSON output content")
|
||||
|
||||
nodes_age = []
|
||||
node_result = output_data.get("node_results") or []
|
||||
for node in node_result:
|
||||
nodes_age.append(node["result"][-1]["quality"])
|
||||
links_age = []
|
||||
link_result = output_data.get("link_results") or []
|
||||
for link in link_result:
|
||||
links_age.append(link["result"][-1]["quality"])
|
||||
age_result = {"nodes": nodes_age, "links": links_age}
|
||||
# age_result = {'nodes': nodes_age, 'links': links_age, 'nodeIDs': node_name, 'linkIDs': link_name}
|
||||
return json.dumps(age_result)
|
||||
|
||||
|
||||
############################################################
|
||||
# pressure regulation 06
|
||||
############################################################
|
||||
+16
-21
@@ -7,16 +7,19 @@ import pandas as pd
|
||||
from psycopg import AsyncConnection
|
||||
from uuid import UUID
|
||||
|
||||
from app.algorithms.cleaning.flow import clean_flow_data_df_kf
|
||||
from app.algorithms.cleaning.pressure import clean_pressure_data_df_km
|
||||
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
|
||||
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 CompositeQueries:
|
||||
class TimeseriesAnalysisService:
|
||||
"""
|
||||
复合查询类,提供跨表查询功能
|
||||
"""
|
||||
@@ -55,7 +58,7 @@ class CompositeQueries:
|
||||
Raises:
|
||||
ValueError: 当 SCADA 设备未找到或字段无效时
|
||||
"""
|
||||
scada_by_id = await CompositeQueries._get_project_scada_index(postgres_conn)
|
||||
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:
|
||||
@@ -118,7 +121,7 @@ class CompositeQueries:
|
||||
Raises:
|
||||
ValueError: 当 SCADA 设备未找到或字段无效时
|
||||
"""
|
||||
scada_by_id = await CompositeQueries._get_project_scada_index(postgres_conn)
|
||||
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:
|
||||
@@ -293,7 +296,7 @@ class CompositeQueries:
|
||||
ValueError: 当元素类型无效时
|
||||
"""
|
||||
|
||||
scada_by_id = await CompositeQueries._get_project_scada_index(postgres_conn)
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
associated_scada = next(
|
||||
(
|
||||
scada
|
||||
@@ -342,7 +345,7 @@ class CompositeQueries:
|
||||
Raises:
|
||||
ValueError: 当前项目没有可清洗设备或指定时间范围内没有监测数据
|
||||
"""
|
||||
scada_by_id = await CompositeQueries._get_project_scada_index(postgres_conn)
|
||||
scada_by_id = await TimeseriesAnalysisService._get_project_scada_index(postgres_conn)
|
||||
supported_types = {"pressure", "pipe_flow", "flow"}
|
||||
|
||||
if device_ids:
|
||||
@@ -509,17 +512,9 @@ class CompositeQueries:
|
||||
valid_link_ids = list(velocity_data.keys())
|
||||
|
||||
# GIS 物化视图是低频更新管网的查询面;只读取本次有结果的管道。
|
||||
async with postgres_conn.cursor() as cur:
|
||||
await cur.execute(
|
||||
"""
|
||||
SELECT id, diameter, start_node_id AS node1,
|
||||
end_node_id AS node2
|
||||
FROM gis.pipes
|
||||
WHERE id = ANY(%s)
|
||||
""",
|
||||
(valid_link_ids,),
|
||||
)
|
||||
all_links = await cur.fetchall()
|
||||
all_links = await NetworkAssetRepository.get_pipes_by_ids(
|
||||
postgres_conn, valid_link_ids
|
||||
)
|
||||
|
||||
# 转换为字典以快速查找
|
||||
links_dict = {str(link["id"]): link for link in all_links}
|
||||
@@ -597,8 +592,8 @@ class CompositeQueries:
|
||||
}
|
||||
)
|
||||
|
||||
# 7. 使用PipelineHealthAnalyzer进行预测
|
||||
analyzer = PipelineHealthAnalyzer()
|
||||
# 7. 使用生存模型进行预测
|
||||
analyzer = PipeHealthSurvivalPredictor()
|
||||
survival_functions = analyzer.predict_survival(data)
|
||||
# 8. 组合结果
|
||||
results = []
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
This module is reserved for general utility functions and helpers.
|
||||
|
||||
Currently, specific utilities are often implemented within their respective service modules
|
||||
(e.g., time handling in `app.services.time_api`) or core modules.
|
||||
(e.g., time handling in `app.domain.time`) or core modules.
|
||||
|
||||
Future expansion may include:
|
||||
- Date and time manipulation helpers
|
||||
|
||||
Reference in New Issue
Block a user