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