Files
TJWaterServerBinary/app/algorithms/pressure_sensor_placement/kmeans_placement.py
T
jiang 5966d039de 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.
2026-09-04 17:30:55 +08:00

97 lines
3.6 KiB
Python

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()