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.
207 lines
6.9 KiB
Python
207 lines
6.9 KiB
Python
"""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)
|
|
]
|