后端服务将通过tjwater-cli形式访问
This commit is contained in:
+281
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
compute_service_areas.py — 水源服务范围分析核心脚本
|
||||
|
||||
输入:
|
||||
sim_result.json — runprojectreturndict 的水力模拟结果
|
||||
pipes.json — getallpipeproperties 的管道属性列表
|
||||
reservoirs.json — getallreservoirproperties 的水库属性列表
|
||||
|
||||
输出:
|
||||
service_area_result.json — 包含 node_area_map 和统计信息
|
||||
|
||||
算法:
|
||||
1. 从 pipes 构建无向拓扑图 (node1 <-> node2)
|
||||
2. 从 sim link_results 确定流向,生成有向图
|
||||
3. 从 reservoirs 获取所有水源节点
|
||||
4. 多源 BFS 下游遍历,首次访问即归属性
|
||||
5. 生成渲染数据与统计
|
||||
|
||||
用法:
|
||||
python compute_service_areas.py sim_result.json pipes.json reservoirs.json \
|
||||
--output service_area_result.json --network tjwater
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
# ── 高区分度 20 色调色板 ──────────────────────────────────────────
|
||||
PALETTE = [
|
||||
"#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd",
|
||||
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf",
|
||||
"#aec7e8", "#ffbb78", "#98df8a", "#ff9896", "#c5b0d5",
|
||||
"#c49c94", "#f7b6d2", "#c7c7c7", "#dbdb8d", "#9edae5",
|
||||
]
|
||||
|
||||
|
||||
def load_json(path: str) -> dict | list:
|
||||
"""加载 JSON 文件,支持引用格式(含 result_ref 字段)或列表。"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
# 如果顶层是引用包装,展开到内部的列表
|
||||
if isinstance(data, dict) and "data" in data:
|
||||
data = data["data"]
|
||||
if isinstance(data, dict) and "items" in data:
|
||||
data = data["items"]
|
||||
return data
|
||||
|
||||
|
||||
def build_undirected_graph(pipes: list) -> tuple[dict, dict]:
|
||||
"""
|
||||
从管道列表构建无向拓扑图。
|
||||
返回:
|
||||
adj: { node_id: set(neighbor_node_ids) }
|
||||
edge_map: { (node1, node2): pipe_id } 用于查找管道 ID
|
||||
"""
|
||||
adj = {}
|
||||
edge_map = {}
|
||||
for p in pipes:
|
||||
pipe_id = str(p.get("id", p.get("pipe_id", p.get("name", ""))))
|
||||
n1 = str(p.get("node1", ""))
|
||||
n2 = str(p.get("node2", ""))
|
||||
if not n1 or not n2:
|
||||
continue
|
||||
adj.setdefault(n1, set()).add(n2)
|
||||
adj.setdefault(n2, set()).add(n1)
|
||||
edge_map[(n1, n2)] = pipe_id
|
||||
edge_map[(n2, n1)] = pipe_id
|
||||
return adj, edge_map
|
||||
|
||||
|
||||
def build_directed_graph(adj: dict, edge_map: dict, link_results: list) -> dict:
|
||||
"""
|
||||
基于模拟流向构建有向图。
|
||||
link_results 中每条记录含 link_id, flow, status。
|
||||
流向判定:
|
||||
flow > 0 → node1 → node2
|
||||
flow < 0 → node2 → node1
|
||||
flow ≈ 0 或 status != "Open" → 忽略(视为断连)
|
||||
|
||||
注意: link_results 中没有 node1/node2,需要通过 link_id 反查 edge_map。
|
||||
由于 edge_map 只有 (node1,node2) → pipe_id 的映射,
|
||||
这里需要先建立 pipe_id → (node1, node2) 的反向索引。
|
||||
"""
|
||||
# 建立 pipe_id → (node1, node2) 反向索引
|
||||
# 注意:edge_map 含有 (n1,n2) 和 (n2,n1) 两个方向,只保留第一个以避免反向覆写
|
||||
pipe_to_nodes = {}
|
||||
for (n1, n2), pid in edge_map.items():
|
||||
if pid not in pipe_to_nodes:
|
||||
pipe_to_nodes[pid] = (n1, n2)
|
||||
|
||||
digraph = {}
|
||||
missing = 0
|
||||
closed_or_zero = 0
|
||||
|
||||
for link in link_results:
|
||||
lid = str(link.get("link_id", link.get("id", "")))
|
||||
flow = link.get("flow", 0)
|
||||
status = str(link.get("status", "Open"))
|
||||
|
||||
# 忽略非 Open 管段和零流量管段(大小写不敏感)
|
||||
if status.upper() != "OPEN":
|
||||
closed_or_zero += 1
|
||||
continue
|
||||
if abs(flow) < 1e-6:
|
||||
closed_or_zero += 1
|
||||
continue
|
||||
|
||||
# 查找管段对应的节点对
|
||||
pair = pipe_to_nodes.get(lid)
|
||||
if pair is None:
|
||||
missing += 1
|
||||
continue
|
||||
|
||||
n1, n2 = pair
|
||||
if flow > 0:
|
||||
digraph.setdefault(n1, set()).add(n2)
|
||||
else:
|
||||
digraph.setdefault(n2, set()).add(n1)
|
||||
|
||||
if missing > 0:
|
||||
print(f"[WARN] {missing} 条模拟管段未在管道拓扑中找到对应节点对", file=sys.stderr)
|
||||
if closed_or_zero > 0:
|
||||
print(f"[INFO] 忽略 {closed_or_zero} 条 Closed/零流量管段", file=sys.stderr)
|
||||
|
||||
return digraph
|
||||
|
||||
|
||||
def bfs_from_sources(sources: list, digraph: dict) -> dict:
|
||||
"""
|
||||
多水源 BFS 下游遍历。首次访问的节点归属到对应水源。
|
||||
返回: { node_id: source_id }
|
||||
"""
|
||||
node_area = {}
|
||||
queue = deque()
|
||||
|
||||
# 初始化:所有水源入队,归属自身
|
||||
for src in sources:
|
||||
node_area[src] = src
|
||||
queue.append(src)
|
||||
|
||||
while queue:
|
||||
cur = queue.popleft()
|
||||
cur_source = node_area[cur]
|
||||
for nb in digraph.get(cur, set()):
|
||||
if nb not in node_area:
|
||||
node_area[nb] = cur_source
|
||||
queue.append(nb)
|
||||
|
||||
return node_area
|
||||
|
||||
|
||||
def generate_render_data(node_area: dict, sources: list) -> dict:
|
||||
"""
|
||||
生成 render_junctions 所需的渲染数据。
|
||||
返回:
|
||||
node_area_map: { node_id: area_id }
|
||||
area_ids: 水源 ID 列表
|
||||
area_colors: { area_id: color_hex }
|
||||
"""
|
||||
# 为每个水源分配颜色
|
||||
area_colors = {}
|
||||
for i, src in enumerate(sources):
|
||||
area_colors[src] = PALETTE[i % len(PALETTE)]
|
||||
|
||||
return {
|
||||
"node_area_map": node_area,
|
||||
"area_ids": sources,
|
||||
"area_colors": area_colors,
|
||||
}
|
||||
|
||||
|
||||
def compute_stats(node_area: dict, sources: list, all_nodes: set) -> dict:
|
||||
"""计算各水源服务节点数和覆盖率统计。"""
|
||||
source_counts = {}
|
||||
for src in sources:
|
||||
source_counts[src] = sum(1 for v in node_area.values() if v == src)
|
||||
|
||||
total = len(all_nodes)
|
||||
assigned = len(node_area)
|
||||
unassigned = total - assigned
|
||||
coverage = (assigned / total * 100) if total > 0 else 0
|
||||
|
||||
return {
|
||||
"total_nodes": total,
|
||||
"assigned_nodes": assigned,
|
||||
"unassigned_nodes": unassigned,
|
||||
"coverage_pct": round(coverage, 2),
|
||||
"source_counts": source_counts,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="水源服务范围分析 — 基于水力模拟流向的多源 BFS 分区"
|
||||
)
|
||||
parser.add_argument("sim_file", help="runprojectreturndict 模拟结果 JSON")
|
||||
parser.add_argument("pipes_file", help="getallpipeproperties 管道属性 JSON")
|
||||
parser.add_argument("reservoirs_file", help="getallreservoirproperties 水库属性 JSON")
|
||||
parser.add_argument("--output", "-o", default="/tmp/opencode/service_area_result.json",
|
||||
help="输出 JSON 文件路径")
|
||||
parser.add_argument("--network", "-n", default="tjwater",
|
||||
help="管网名称(仅用于报告标注)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# ── 1. 加载数据 ──
|
||||
print(f"[INFO] 加载模拟结果: {args.sim_file}")
|
||||
sim_data = load_json(args.sim_file)
|
||||
link_results = sim_data.get("link_results", [])
|
||||
|
||||
print(f"[INFO] 加载管道属性: {args.pipes_file}")
|
||||
pipes = load_json(args.pipes_file)
|
||||
|
||||
print(f"[INFO] 加载水库列表: {args.reservoirs_file}")
|
||||
reservoirs = load_json(args.reservoirs_file)
|
||||
|
||||
# 提取水源 ID 列表
|
||||
sources = []
|
||||
if isinstance(reservoirs, list):
|
||||
sources = [str(r.get("id", r.get("name", ""))) for r in reservoirs if r.get("id") or r.get("name")]
|
||||
elif isinstance(reservoirs, dict):
|
||||
# 可能是 { data: [...] } 格式
|
||||
for key in ["data", "items", "reservoirs"]:
|
||||
if key in reservoirs and isinstance(reservoirs[key], list):
|
||||
sources = [str(r.get("id", r.get("name", ""))) for r in reservoirs[key] if r.get("id") or r.get("name")]
|
||||
break
|
||||
if not sources:
|
||||
# 也可能是 { "551656": {...}, ... } 格式
|
||||
sources = [str(k) for k in reservoirs if isinstance(reservoirs[k], dict)]
|
||||
|
||||
if not sources:
|
||||
print("[ERROR] 未能从水库数据中提取水源 ID 列表", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[INFO] 水源数: {len(sources)}")
|
||||
print(f"[INFO] 水源列表: {sources}")
|
||||
|
||||
# ── 2. 构建无向图 ──
|
||||
adj, edge_map = build_undirected_graph(pipes)
|
||||
all_nodes = set(adj.keys())
|
||||
|
||||
# ── 3. 构建有向图 ──
|
||||
digraph = build_directed_graph(adj, edge_map, link_results)
|
||||
|
||||
# ── 4. BFS 下游遍历 ──
|
||||
print("[INFO] 执行多源 BFS 下游遍历...")
|
||||
node_area = bfs_from_sources(sources, digraph)
|
||||
|
||||
# ── 5. 统计 ──
|
||||
stats = compute_stats(node_area, sources, all_nodes)
|
||||
print(f"[INFO] 总节点数: {stats['total_nodes']}")
|
||||
print(f"[INFO] 已分配: {stats['assigned_nodes']} ({stats['coverage_pct']:.1f}%)")
|
||||
print(f"[INFO] 未分配: {stats['unassigned_nodes']}")
|
||||
|
||||
print("\n各水源服务节点数 (按数量降序):")
|
||||
for src, count in sorted(stats["source_counts"].items(), key=lambda x: -x[1]):
|
||||
pct = (count / stats["assigned_nodes"] * 100) if stats["assigned_nodes"] > 0 else 0
|
||||
print(f" {src}: {count} ({pct:.1f}%)")
|
||||
|
||||
# ── 6. 生成渲染数据 ──
|
||||
render = generate_render_data(node_area, sources)
|
||||
|
||||
# ── 7. 输出 ──
|
||||
output = {
|
||||
"network": args.network,
|
||||
"stats": stats,
|
||||
"render": render,
|
||||
"sources": sources,
|
||||
}
|
||||
|
||||
os.makedirs(os.path.dirname(args.output) or ".", exist_ok=True)
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
json.dump(output, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n[OK] 结果已写入: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user