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:
2026-09-04 17:30:55 +08:00
parent 9b095c7439
commit 5966d039de
91 changed files with 1418 additions and 4020 deletions
+4 -44
View File
@@ -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`.
"""
+2 -2
View File
@@ -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"]
@@ -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"]
@@ -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,
)
@@ -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="爆管时压力 CSVid,value"
)
parser.add_argument(
"--normal-pressure-csv", required=True, help="正常时压力 CSVid,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()
@@ -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"]
-59
View File
@@ -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"]
@@ -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)
]
-3
View File
@@ -1,3 +0,0 @@
from app.algorithms.health.analyzer import PipelineHealthAnalyzer
__all__ = ["PipelineHealthAnalyzer"]
-3
View File
@@ -1,3 +0,0 @@
from app.algorithms.isolation.valve import valve_isolation_analysis
__all__ = ["valve_isolation_analysis"]
-167
View File
@@ -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
-3
View File
@@ -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"]
@@ -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()
@@ -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("测试完成:函数运行正常")
@@ -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("测试完成:函数运行正常")
-131
View File
@@ -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,
)
-71
View File
@@ -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)
-19
View File
@@ -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",
]
-867
View File
@@ -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}
)
-712
View File
@@ -1,712 +0,0 @@
from __future__ import annotations
import json
from datetime import datetime
from functools import wraps
from math import pi, sqrt
import pytz
import app.services.simulation as simulation
from app.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,
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,
)
def _isolated_analysis(purpose: str):
def decorator(func):
@wraps(func)
def wrapper(name: str, *args, **kwargs):
with temporary_project_database(name, purpose) as temporary:
kwargs["_temporary_project"] = temporary
return func(name, *args, **kwargs)
return wrapper
return decorator
############################################################
# burst analysis 01
############################################################
def convert_to_local_unit(proj: str, emitters: float) -> float:
proj_opt = get_option(proj)
str_unit = proj_opt.get("UNITS")
if str_unit == "CMH":
return emitters * 3.6
elif str_unit == "LPS":
return emitters
elif str_unit == "CMS":
return emitters / 1000.0
elif str_unit == "MGD":
return emitters * 0.0438126
# Unknown unit: log and return original value
print(str_unit)
return emitters
@_isolated_analysis("burst_analysis")
def burst_analysis(
name: str,
modify_pattern_start_time: str,
burst_ID: list | str = None,
burst_size: list | float | int = None,
modify_total_duration: int = 900,
modify_fixed_pump_pattern: dict[str, list] = None,
modify_variable_pump_pattern: dict[str, list] = None,
modify_valve_opening: dict[str, float] = None,
scheme_name: str = None,
username: str | None = None,
_temporary_project: str | None = None,
) -> None:
"""
爆管模拟
:param name: 模型名称,数据库中对应的名字
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
:param burst_ID: 爆管管道的ID,选取的是管道,单独传入一个爆管管道,可以是str或list,传入多个爆管管道是用list
:param burst_size: 爆管管道破裂的孔口面积,和burst_ID列表各位置的ID对应,以cm*cm计算
:param modify_total_duration: 模拟总历时,秒
:param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern
:param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
:param scheme_name: 方案名称
:return:
"""
if not username:
raise ValueError("username is required when storing burst analysis scheme")
scheme_detail: dict = {
"burst_ID": burst_ID,
"burst_size": burst_size,
"modify_total_duration": modify_total_duration,
"modify_fixed_pump_pattern": modify_fixed_pump_pattern,
"modify_variable_pump_pattern": modify_variable_pump_pattern,
"modify_valve_opening": modify_valve_opening,
}
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Analysis."
)
if _temporary_project is None:
raise RuntimeError("Burst analysis isolation was not prepared")
new_name = _temporary_project
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Opening Database."
)
simulation.run_simulation(
name=new_name,
simulation_type="manually_temporary",
modify_pattern_start_time=modify_pattern_start_time,
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Database Loading OK."
)
##step 1 set the emitter coefficient of end node of busrt pipe
if isinstance(burst_ID, list):
if (burst_size is not None) and (type(burst_size) is not list):
return json.dumps("Type mismatch.")
# 转化为列表形式
elif isinstance(burst_ID, str):
burst_ID = [burst_ID]
if burst_size is not None:
if isinstance(burst_size, float) or isinstance(burst_size, int):
burst_size = [burst_size]
else:
return json.dumps("Type mismatch.")
else:
return json.dumps("Type mismatch.")
if burst_size is None:
burst_size = [-1] * len(burst_ID)
elif len(burst_size) < len(burst_ID):
burst_size += [-1] * (len(burst_ID) - len(burst_size))
elif len(burst_size) > len(burst_ID):
# burst_size = burst_size[:len(burst_ID)]
return json.dumps("Length mismatch.")
for burst_ID_, burst_size_ in zip(burst_ID, burst_size):
pipe = get_pipe(new_name, burst_ID_)
str_start_node = pipe["node1"]
str_end_node = pipe["node2"]
d_pipe = pipe["diameter"] / 1000.0
if burst_size_ <= 0:
burst_size_ = 3.14 * d_pipe * d_pipe / 4 / 8
else:
burst_size_ = burst_size_ / 10000
emitter_coeff = (
0.65 * burst_size_ * sqrt(19.6) * 1000
) # 1/8开口面积作为coeff,单位 L/S
emitter_coeff = convert_to_local_unit(new_name, emitter_coeff)
emitter_node = ""
if is_junction(new_name, str_end_node):
emitter_node = str_end_node
elif is_junction(new_name, str_start_node):
emitter_node = str_start_node
old_emitter = get_emitter(new_name, emitter_node)
if old_emitter != None:
old_emitter["coefficient"] = emitter_coeff # 爆管的emitter coefficient设置
else:
old_emitter = {"junction": emitter_node, "coefficient": emitter_coeff}
new_emitter = ChangeSet()
new_emitter.append(old_emitter)
set_emitter(new_name, new_emitter)
# step 2. run simulation
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
options = get_option(new_name)
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
options["REQUIRED PRESSURE"] = "10.0000"
cs_options = ChangeSet()
cs_options.append(options)
set_option(new_name, cs_options)
# valve_control = None
# if modify_valve_opening is not None:
# valve_control = {}
# for valve in modify_valve_opening:
# valve_control[valve] = {'status': 'CLOSED'}
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time,
# end_datetime=modify_pattern_start_time,
# modify_total_duration=modify_total_duration,
# modify_pump_pattern=modify_pump_pattern,
# valve_control=valve_control,
# downloading_prohibition=True)
simulation.run_simulation(
name=new_name,
simulation_type="extended",
modify_pattern_start_time=modify_pattern_start_time,
modify_total_duration=modify_total_duration,
modify_fixed_pump_pattern=modify_fixed_pump_pattern,
modify_variable_pump_pattern=modify_variable_pump_pattern,
modify_valve_opening=modify_valve_opening,
scheme_type="burst_analysis",
scheme_name=scheme_name,
result_db_name=name,
scheme_username=username,
scheme_detail=scheme_detail,
)
# step 3. restore the base model status
# execute_undo(name) #有疑惑
############################################################
# valve closing analysis 02
############################################################
@_isolated_analysis("valve_close_analysis")
def valve_close_analysis(
name: str,
modify_pattern_start_time: str,
modify_total_duration: int = 900,
modify_valve_opening: dict[str, float] = None,
scheme_name: str = None,
_temporary_project: str | None = None,
) -> None:
"""
关阀模拟
:param name: 模型名称,数据库中对应的名字
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
:param modify_total_duration: 模拟总历时,秒
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
:param scheme_name: 方案名称
:return:
"""
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Analysis."
)
if _temporary_project is None:
raise RuntimeError("Valve-close analysis isolation was not prepared")
new_name = _temporary_project
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Opening Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Database Loading OK."
)
# step 1. change the valves status to 'closed'
# for valve in valves:
# if not is_valve(new_name,valve):
# result='ID:{}is not a valve type'.format(valve)
# return result
# cs=ChangeSet()
# status=get_status(new_name,valve)
# status['status']='CLOSED'
# cs.append(status)
# set_status(new_name,cs)
# step 2. run simulation
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
options = get_option(new_name)
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
options["REQUIRED PRESSURE"] = "20.0000"
cs_options = ChangeSet()
cs_options.append(options)
set_option(new_name, cs_options)
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
# downloading_prohibition=True)
simulation.run_simulation(
name=new_name,
simulation_type="extended",
modify_pattern_start_time=modify_pattern_start_time,
modify_total_duration=modify_total_duration,
modify_valve_opening=modify_valve_opening,
scheme_type="valve_close_Analysis",
scheme_name=scheme_name,
result_db_name=name,
)
# step 3. restore the base model
# for valve in valves:
# execute_undo(name)
# return result
############################################################
# flushing analysis 03
# Pipe_Flushing_Analysis(prj_name,date_time, Valve_id_list, Drainage_Node_Id, Flushing_flow[opt], Flushing_duration[opt])->out_file:string
############################################################
@_isolated_analysis("flushing_analysis")
def flushing_analysis(
name: str,
modify_pattern_start_time: str,
modify_total_duration: int = 900,
modify_valve_opening: dict[str, float] = None,
drainage_node_ID: str = None,
flushing_flow: float = 0,
scheme_name: str = None,
username: str | None = None,
valve_control: dict[str, dict] = None,
_temporary_project: str | None = None,
) -> None:
"""
管道冲洗模拟
:param name: 模型名称,数据库中对应的名字
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
:param modify_total_duration: 模拟总历时,秒
:param modify_valve_opening: dict中包含多个阀门开启度,str为阀门的id,float为修改后的阀门开启度
:param valve_control: dict中可分别指定阀门的status、setting和k
:param drainage_node_ID: 冲洗排放口所在节点ID
:param flushing_flow: 冲洗水量,传入参数单位为m3/h
:param scheme_name: 方案名称
:return:
"""
if not username:
raise ValueError("username is required when storing flushing analysis scheme")
scheme_detail: dict = {
"duration": modify_total_duration,
"valve_opening": modify_valve_opening,
"valve_control": valve_control,
"drainage_node_ID": drainage_node_ID,
"flushing_flow": flushing_flow,
}
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Analysis."
)
if _temporary_project is None:
raise RuntimeError("Flushing analysis isolation was not prepared")
new_name = _temporary_project
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Opening Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Database Loading OK."
)
if not is_junction(new_name, drainage_node_ID):
return "Wrong Drainage node type"
# step 1. change the valves status to 'closed'
# for valve, valve_k in zip(valves, valves_k):
# cs=ChangeSet()
# status=get_status(new_name,valve)
# # status['status']='CLOSED'
# if valve_k == 0:
# status['status'] = 'CLOSED'
# elif valve_k < 1:
# status['status'] = 'OPEN'
# status['setting'] = 0.1036 * pow(valve_k, -3.105)
# cs.append(status)
# set_status(new_name,cs)
options = get_option(new_name)
units = options["UNITS"]
# step 2. set the emitter coefficient of drainage node or add flush flow to the drainage node
# 新建 pattern
time_option = get_time(new_name)
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
secs = from_clock_to_seconds_2(hydraulic_step)
cs_pattern = ChangeSet()
pt = {}
factors = []
tmp_duration = modify_total_duration
while tmp_duration > 0:
factors.append(1.0)
tmp_duration = tmp_duration - secs
pt["id"] = "flushing_pt"
pt["factors"] = factors
cs_pattern.append(pt)
add_pattern(new_name, cs_pattern)
# 为 emitter_demand 添加新的 pattern
emitter_demand = get_demand(new_name, drainage_node_ID)
cs = ChangeSet()
if flushing_flow > 0:
if units == "LPS":
emitter_demand["demands"].append(
{
"demand": flushing_flow / 3.6,
"pattern": "flushing_pt",
"category": None,
}
)
elif units == "CMH":
emitter_demand["demands"].append(
{"demand": flushing_flow, "pattern": "flushing_pt", "category": None}
)
cs.append(emitter_demand)
set_demand(new_name, cs)
else:
pipes = get_node_links(new_name, drainage_node_ID)
flush_diameter = 50
for pipe in pipes:
d = get_pipe(new_name, pipe)["diameter"]
if flush_diameter < d:
flush_diameter = d
flush_diameter /= 1000
emitter_coeff = (
0.65 * 3.14 * (flush_diameter * flush_diameter / 4) * sqrt(19.6) * 1000
) # 全开口面积作为coeff
old_emitter = get_emitter(new_name, drainage_node_ID)
if old_emitter != None:
old_emitter["coefficient"] = emitter_coeff # 爆管的emitter coefficient设置
else:
old_emitter = {"junction": drainage_node_ID, "coefficient": emitter_coeff}
new_emitter = ChangeSet()
new_emitter.append(old_emitter)
set_emitter(new_name, new_emitter)
# step 3. run simulation
# 涉及关阀计算,可能导致关阀后仍有流量,改为压力驱动PDA
options = get_option(new_name)
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
options["REQUIRED PRESSURE"] = "20.0000"
cs_options = ChangeSet()
cs_options.append(options)
set_option(new_name, cs_options)
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
# downloading_prohibition=True)
simulation.run_simulation(
name=new_name,
simulation_type="extended",
modify_pattern_start_time=modify_pattern_start_time,
modify_total_duration=modify_total_duration,
modify_valve_opening=modify_valve_opening,
valve_control=valve_control,
scheme_type="flushing_analysis",
scheme_name=scheme_name,
result_db_name=name,
scheme_username=username,
scheme_detail=scheme_detail,
)
# step 4. restore the base model
# return result
############################################################
# Contaminant simulation 04
#
############################################################
@_isolated_analysis("contaminant_simulation")
def contaminant_simulation(
name: str,
modify_pattern_start_time: str, # 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
modify_total_duration: int, # 模拟总历时,秒
source: str, # 污染源节点ID
concentration: float, # 污染源浓度,单位mg/L
scheme_name: str = None,
source_pattern: str = None, # 污染源时间变化模式名称
username: str | None = None,
_temporary_project: str | None = None,
) -> None:
"""
污染模拟
:param name: 模型名称,数据库中对应的名字
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
:param modify_total_duration: 模拟总历时,秒
:param source: 污染源所在的节点ID
:param concentration: 污染源位置处的浓度,单位mg/L。默认的污染模拟setting为SOURCE_TYPE_CONCEN(改为SOURCE_TYPE_SETPOINT
:param source_pattern: 污染源的时间变化模式,若不传入则默认以恒定浓度持续模拟,时间长度等于duration;
若传入,则格式为{1.0,0.5,1.1}等系数列表pattern_step模拟等于模型的hydraulic time step
:param scheme_name: 方案名称
:return:
"""
if not username:
raise ValueError("username is required when storing contaminant analysis scheme")
scheme_detail: dict = {
"source": source,
"concentration": concentration,
"duration": modify_total_duration,
"pattern": source_pattern,
}
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Analysis."
)
if _temporary_project is None:
raise RuntimeError("Contaminant simulation isolation was not prepared")
new_name = _temporary_project
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Opening Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Database Loading OK."
)
dic_time = get_time(new_name)
dic_time["QUALITY TIMESTEP"] = "0:05:00"
cs = ChangeSet()
cs.operations.append(dic_time)
set_time(new_name, cs) # set QUALITY TIMESTEP
time_option = get_time(new_name)
hydraulic_step = time_option["HYDRAULIC TIMESTEP"]
secs = from_clock_to_seconds_2(hydraulic_step)
operation_step = 0
# step 1. set duration
if modify_total_duration == None:
modify_total_duration = secs
# step 2. set pattern
if source_pattern != None:
pt = get_pattern(new_name, source_pattern)
if len(pt) == 0:
str_response = str("cant find source_pattern")
return str_response
else:
cs_pattern = ChangeSet()
pt = {}
factors = []
tmp_duration = modify_total_duration
while tmp_duration > 0:
factors.append(1.0)
tmp_duration = tmp_duration - secs
pt["id"] = "contam_pt"
pt["factors"] = factors
cs_pattern.append(pt)
add_pattern(new_name, cs_pattern)
operation_step += 1
# step 3. set source/initial quality
# source quality
cs_source = ChangeSet()
source_schema = {
"node": source,
"s_type": SOURCE_TYPE_SETPOINT,
"strength": concentration,
"pattern": pt["id"],
}
cs_source.append(source_schema)
source_node = get_source(new_name, source)
if len(source_node) == 0:
add_source(new_name, cs_source)
else:
set_source(new_name, cs_source)
dict_demand = get_demand(new_name, source)
for demands in dict_demand["demands"]:
dict_demand["demands"][dict_demand["demands"].index(demands)]["demand"] = -1
dict_demand["demands"][dict_demand["demands"].index(demands)]["pattern"] = None
cs = ChangeSet()
cs.append(dict_demand)
set_demand(new_name, cs) # set inflow node
# # initial quality
# dict_quality = get_quality(new_name, source)
# dict_quality['quality'] = concentration
# cs = ChangeSet()
# cs.append(dict_quality)
# set_quality(new_name, cs)
operation_step += 1
# step 4 set option of quality to chemical
opt = get_option(new_name)
opt["QUALITY"] = OPTION_QUALITY_CHEMICAL
cs_option = ChangeSet()
cs_option.append(opt)
set_option(new_name, cs_option)
operation_step += 1
# step 5. run simulation
# result = run_simulation_ex(new_name,'realtime', modify_pattern_start_time, modify_pattern_start_time, modify_total_duration,
# downloading_prohibition=True)
simulation.run_simulation(
name=new_name,
simulation_type="extended",
modify_pattern_start_time=modify_pattern_start_time,
modify_total_duration=modify_total_duration,
scheme_type="contaminant_analysis",
scheme_name=scheme_name,
result_db_name=name,
scheme_username=username,
scheme_detail=scheme_detail,
)
# for i in range(1,operation_step):
# execute_undo(name)
############################################################
# 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
############################################################
@_isolated_analysis("pressure_regulation")
def pressure_regulation(
name: str,
modify_pattern_start_time: str,
modify_total_duration: int = 900,
modify_tank_initial_level: dict[str, float] = None,
modify_fixed_pump_pattern: dict[str, list] = None,
modify_variable_pump_pattern: dict[str, list] = None,
scheme_name: str = None,
scada_mappings: simulation.ScadaElementMappings | None = None,
_temporary_project: str | None = None,
) -> None:
"""
区域调压模拟,用来模拟未来15分钟内,开关水泵对区域压力的影响
:param name: 模型名称,数据库中对应的名字
:param modify_pattern_start_time: 模拟开始时间,格式为'2024-11-25T09:00:00+08:00'
:param modify_total_duration: 模拟总历时,秒
:param modify_tank_initial_level: dict中包含多个水塔,str为水塔的idfloat为修改后的initial_level
:param modify_fixed_pump_pattern: dict中包含多个水泵模式,str为工频水泵的id,list为修改后的pattern
:param modify_variable_pump_pattern: dict中包含多个水泵模式,str为变频水泵的id,list为修改后的pattern
:param scheme_name: 模拟方案名称
:return:
"""
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Analysis."
)
if _temporary_project is None:
raise RuntimeError("Pressure-regulation isolation was not prepared")
new_name = _temporary_project
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Copying Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Start Opening Database."
)
print(
datetime.now(pytz.timezone("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
+ " -- Database Loading OK."
)
# 全部关泵后,压力计算不合理,改为压力驱动PDA
options = get_option(new_name)
options["DEMAND MODEL"] = OPTION_DEMAND_MODEL_PDA
options["REQUIRED PRESSURE"] = "15.0000"
cs_options = ChangeSet()
cs_options.append(options)
set_option(new_name, cs_options)
# result = run_simulation_ex(name=new_name,
# simulation_type='realtime',
# start_datetime=start_datetime,
# duration=900,
# pump_control=pump_control,
# tank_initial_level_control=tank_initial_level_control,
# downloading_prohibition=True)
simulation.run_simulation(
name=new_name,
simulation_type="extended",
modify_pattern_start_time=modify_pattern_start_time,
modify_total_duration=modify_total_duration,
modify_tank_initial_level=modify_tank_initial_level,
modify_fixed_pump_pattern=modify_fixed_pump_pattern,
modify_variable_pump_pattern=modify_variable_pump_pattern,
scheme_type="pressure_regulation",
scheme_name=scheme_name,
result_db_name=name,
scada_mappings=scada_mappings,
)
# return result
@@ -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
-19
View File
@@ -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",
]
-104
View File
@@ -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)