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:
@@ -0,0 +1,238 @@
|
||||
import logging
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app.algorithms.burst_localization import leak_signature
|
||||
|
||||
from .candidate_ranking import (
|
||||
DN_search_multi_simple_add_flow_count_new,
|
||||
)
|
||||
from .topology_model import (
|
||||
_build_node_pipe_maps,
|
||||
cal_node_coordinate,
|
||||
construct_graph,
|
||||
load_inp,
|
||||
read_inf_inp,
|
||||
read_inf_inp_other,
|
||||
)
|
||||
|
||||
DEFAULT_N_WORKERS = max(1, min(cpu_count() - 1, 4))
|
||||
# DEFAULT_N_WORKERS = max(1, cpu_count() - 1)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _align_scada_series(
|
||||
series: pd.Series, ids: Iterable[str], series_name: str
|
||||
) -> pd.Series:
|
||||
ids = [str(item) for item in ids]
|
||||
aligned = series.copy()
|
||||
aligned.index = aligned.index.map(str)
|
||||
missing_ids = [item for item in ids if item not in aligned.index]
|
||||
if missing_ids:
|
||||
preview = ", ".join(missing_ids[:10])
|
||||
raise ValueError(f"{series_name} missing IDs: {preview}")
|
||||
aligned = pd.to_numeric(aligned.loc[ids], errors="coerce")
|
||||
invalid_ids = aligned[aligned.isna()].index.tolist()
|
||||
if invalid_ids:
|
||||
preview = ", ".join(invalid_ids[:10])
|
||||
raise ValueError(
|
||||
f"{series_name} contains non-numeric values for IDs: {preview}"
|
||||
)
|
||||
return aligned
|
||||
|
||||
|
||||
def _validate_flow_inputs(
|
||||
flow_scada_ids: list[str] | None,
|
||||
burst_flow: pd.Series | None,
|
||||
normal_flow: pd.Series | None,
|
||||
) -> tuple[bool, list[str]]:
|
||||
has_any_flow = any(
|
||||
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
|
||||
)
|
||||
has_all_flow = all(
|
||||
value is not None for value in [flow_scada_ids, burst_flow, normal_flow]
|
||||
)
|
||||
if has_any_flow and not has_all_flow:
|
||||
raise ValueError(
|
||||
"flow_scada_ids, burst_flow, and normal_flow must be provided together."
|
||||
)
|
||||
|
||||
if not has_all_flow:
|
||||
return False, []
|
||||
|
||||
flow_ids = [str(item) for item in (flow_scada_ids or [])]
|
||||
if len(flow_ids) == 0:
|
||||
raise ValueError("flow_scada_ids cannot be empty when flow data is provided.")
|
||||
return True, flow_ids
|
||||
|
||||
|
||||
def _build_top_candidates(similarity_series: pd.Series) -> list[dict[str, Any]]:
|
||||
top_series = similarity_series.iloc[:10]
|
||||
return [
|
||||
{"pipe_id": str(pipe_id), "similarity": float(score)}
|
||||
for pipe_id, score in top_series.items()
|
||||
]
|
||||
|
||||
|
||||
def run_burst_location(
|
||||
wn_inp_path: str,
|
||||
pressure_scada_ids: list[str],
|
||||
burst_pressure: pd.Series,
|
||||
normal_pressure: pd.Series,
|
||||
burst_leakage: float,
|
||||
flow_scada_ids: list[str] | None = None,
|
||||
burst_flow: pd.Series | None = None,
|
||||
normal_flow: pd.Series | None = None,
|
||||
min_dpressure: float = 2.0,
|
||||
basic_pressure: float = 10.0,
|
||||
n_workers: int = DEFAULT_N_WORKERS,
|
||||
partition_on_full_graph: bool = True,
|
||||
visualize_partition: bool = False,
|
||||
visualize_pause_seconds: float = 0.3,
|
||||
final_candidates_csv_path: (
|
||||
str | None
|
||||
) = "temp/burst_location/final_round_candidates.csv",
|
||||
) -> dict[str, Any]:
|
||||
if pressure_scada_ids is None or len(pressure_scada_ids) == 0:
|
||||
raise ValueError("pressure_scada_ids cannot be empty.")
|
||||
if burst_pressure is None or normal_pressure is None:
|
||||
raise ValueError("burst_pressure and normal_pressure are required.")
|
||||
|
||||
has_all_flow, flow_ids = _validate_flow_inputs(
|
||||
flow_scada_ids=flow_scada_ids,
|
||||
burst_flow=burst_flow,
|
||||
normal_flow=normal_flow,
|
||||
)
|
||||
|
||||
inp_path = Path(wn_inp_path)
|
||||
wn = load_inp(
|
||||
inp_name=inp_path.name,
|
||||
inp_location=str(inp_path.parent) + "/",
|
||||
inp_time=0,
|
||||
driven_mode="PDD",
|
||||
require_p=float(basic_pressure),
|
||||
minimum_p=0.0,
|
||||
)
|
||||
|
||||
(
|
||||
all_node,
|
||||
_,
|
||||
node_coordinates,
|
||||
all_pipe,
|
||||
_,
|
||||
_,
|
||||
pipe_length,
|
||||
pipe_diameter,
|
||||
) = read_inf_inp(wn)
|
||||
|
||||
candidate_pipe, _ = leak_signature.cal_possible_pipe(
|
||||
burst_leakage, all_pipe, pipe_diameter
|
||||
)
|
||||
|
||||
_, pipe_start_node_all, pipe_end_node_all = read_inf_inp_other(wn)
|
||||
node_x, node_y = cal_node_coordinate(all_node, node_coordinates)
|
||||
G0 = construct_graph(wn)
|
||||
node_pipe_dic, couple_node_length = _build_node_pipe_maps(
|
||||
all_node,
|
||||
all_pipe,
|
||||
pipe_start_node_all,
|
||||
pipe_end_node_all,
|
||||
pipe_length,
|
||||
)
|
||||
all_node_series = pd.Series(range(len(all_node)), index=all_node)
|
||||
|
||||
pressure_ids = [str(item) for item in pressure_scada_ids]
|
||||
normal_pressure_aligned = _align_scada_series(
|
||||
normal_pressure, pressure_ids, "normal_pressure"
|
||||
)
|
||||
burst_pressure_aligned = _align_scada_series(
|
||||
burst_pressure, pressure_ids, "burst_pressure"
|
||||
)
|
||||
pressure_normal = normal_pressure_aligned.to_frame().T
|
||||
pressure_monitor = burst_pressure_aligned.to_frame().T
|
||||
pressure_predict = pressure_normal.copy()
|
||||
timestep_list = list(pressure_normal.index)
|
||||
|
||||
if has_all_flow:
|
||||
normal_flow_aligned = _align_scada_series(normal_flow, flow_ids, "normal_flow")
|
||||
burst_flow_aligned = _align_scada_series(burst_flow, flow_ids, "burst_flow")
|
||||
flow_normal = normal_flow_aligned.to_frame().T
|
||||
flow_monitor = burst_flow_aligned.to_frame().T
|
||||
flow_predict = flow_normal.copy()
|
||||
similarity_mode = "CDF"
|
||||
max_flow = flow_normal.iloc[0, :].abs()
|
||||
else:
|
||||
flow_normal = pd.DataFrame(index=timestep_list)
|
||||
flow_monitor = pd.DataFrame(index=timestep_list)
|
||||
flow_predict = pd.DataFrame(index=timestep_list)
|
||||
similarity_mode = "CAD_new_gy"
|
||||
max_flow = pd.Series(dtype=float)
|
||||
|
||||
stage_timing: dict[str, Any] = {}
|
||||
try:
|
||||
(
|
||||
located_pipe,
|
||||
elapsed_seconds,
|
||||
simulation_times,
|
||||
_,
|
||||
similarity_series,
|
||||
exit_condition,
|
||||
final_candidates_csv,
|
||||
) = DN_search_multi_simple_add_flow_count_new(
|
||||
wn=wn,
|
||||
wn_inp_path=str(inp_path),
|
||||
G0=G0,
|
||||
all_node=all_node,
|
||||
node_x=node_x,
|
||||
node_y=node_y,
|
||||
pipe_start_node_all=pipe_start_node_all,
|
||||
pipe_end_node_all=pipe_end_node_all,
|
||||
pipe_diameter=pipe_diameter,
|
||||
couple_node_length=couple_node_length,
|
||||
node_pipe_dic=node_pipe_dic,
|
||||
all_node_series=all_node_series,
|
||||
top_group_ratio=0.3,
|
||||
top_pipe_num_max=80,
|
||||
top_pipe_num_min=10,
|
||||
candidate_pipe_input_initial=candidate_pipe,
|
||||
similarity_mode=similarity_mode,
|
||||
pressure_monitor=pressure_monitor,
|
||||
pressure_predict=pressure_predict,
|
||||
pressure_normal=pressure_normal,
|
||||
pressure_leak_all=None,
|
||||
flow_monitor=flow_monitor,
|
||||
flow_predict=flow_predict,
|
||||
flow_normal=flow_normal,
|
||||
flow_leak_all=None,
|
||||
timestep_list=timestep_list,
|
||||
max_flow=max_flow,
|
||||
group_basic_num=30,
|
||||
Top_sensor_num=min(5, len(pressure_ids)),
|
||||
if_gy=0,
|
||||
pressure_threshold=float(min_dpressure),
|
||||
leak_mag=float(burst_leakage),
|
||||
n_workers=max(1, int(n_workers)),
|
||||
stage_timing=stage_timing,
|
||||
partition_on_full_graph=partition_on_full_graph,
|
||||
visualize_partition=visualize_partition,
|
||||
visualize_pause_seconds=visualize_pause_seconds,
|
||||
final_candidates_csv_path=final_candidates_csv_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("Burst location algorithm execution failed.")
|
||||
raise RuntimeError(f"Failed to run burst location algorithm: {exc}") from exc
|
||||
|
||||
return {
|
||||
"located_pipe": located_pipe,
|
||||
"burst_leakage": float(burst_leakage),
|
||||
"elapsed_seconds": elapsed_seconds,
|
||||
"simulation_times": int(simulation_times),
|
||||
"top_candidates": _build_top_candidates(similarity_series),
|
||||
"similarity_mode": similarity_mode,
|
||||
"exit_condition": exit_condition,
|
||||
"final_candidates_csv": final_candidates_csv,
|
||||
"stage_timing_seconds": stage_timing,
|
||||
}
|
||||
Reference in New Issue
Block a user