Files
TJWaterAgent/.opencode/skills/workflow/hydraulic-bottleneck-analysis/scripts/bottleneck_analysis.py
T
jiang 72ebf4d6c1
Generic Container CI/CD / test-build-publish (push) Failing after 1m20s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Failing after 1m20s
feat(agent): 完善分析编排与结果传输
2026-08-26 18:04:51 +08:00

199 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
水力瓶颈管道综合分析
数据源:管道属性 + 实时水力 + 节点压力 → 复合评分 → 改造建议
注:realtime links 的 setting 字段为无效值,已移除所有基于 setting 的判定。
schema 适配(2026-08 实测):realtime links 主键为 link_idrealtime nodes 主键为 node_id
time 为 UTC 格式,--target-time 需传 UTC 时刻(如北京时间 08:00 对应 00:00+00:00)。
"""
import json, sys, math, argparse
from collections import defaultdict
VELOCITY_THRESHOLDS = {"critical": 3.0, "severe": 2.0, "high": 1.5}
def pct(d, v):
if not d: return 0
k = (v/100)*(len(d)-1); f=math.floor(k); c=math.ceil(k)
return d[f] if f==c else d[f]*(c-k)+d[c]*(k-f)
def load_json(path):
with open(path, encoding='utf-8') as f: raw = f.read()
return json.loads(raw[raw.find('{'):])
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--pipe-props', required=True)
ap.add_argument('--realtime', required=True)
ap.add_argument('--node-pressures', required=True)
ap.add_argument('--target-time', required=True)
ap.add_argument('--top', type=int, default=100)
args = ap.parse_args()
TT = args.target_time
# 1. Load
print("[1/5] Loading data...", file=sys.stderr)
props = load_json(args.pipe_props).get('data', [])
pipe_map = {p['id']: p for p in props}
rt = load_json(args.realtime).get('data', [])
rt = [r for r in rt if r.get('time') == TT]
np = load_json(args.node_pressures).get('data', [])
np = [n for n in np if n.get('time') == TT]
node_pressure = {n['node_id']: n.get('pressure', n.get('value',0)) for n in np}
print(f" Pipes: {len(props)}, Realtime: {len(rt)}, Node pressures: {len(node_pressure)}", file=sys.stderr)
# 2. Merge
print("[2/5] Merging...", file=sys.stderr)
merged = []
for r in rt:
pid = r['link_id']; prop = pipe_map.get(pid)
if prop:
merged.append({**prop, **r, '_prop_id': prop['id'], '_rt_id': r['link_id']})
print(f" Merged: {len(merged)}", file=sys.stderr)
# 3. Score
print("[3/5] Scoring...", file=sys.stderr)
hl_vals = sorted([abs(m['headloss']) for m in merged])
p80 = pct(hl_vals, 80); p90 = pct(hl_vals, 90); p95 = pct(hl_vals, 95)
scored = []
for m in merged:
vel = abs(m['velocity']); hl = abs(m['headloss'])
diam = m.get('diameter', 0)
length = m.get('length', 0); roughness = m.get('roughness', 0)
n1, n2 = m['node1'], m['node2']
pid = m['id']
vs = 3 if vel>=3 else (2 if vel>=2 else (1 if vel>=1.5 else 0))
vg = "极危" if vs==3 else ("严重" if vs==2 else ("偏高" if vs==1 else "正常"))
hs = 2 if hl>p90 else (1 if hl>p80 else 0)
hg = "严重" if hs==2 else ("中度" if hs==1 else "正常")
composite = vs + hs
is_bn = (vs>=1 and hs>=1) or (vs>=2)
# Node pressures
p1 = node_pressure.get(n1); p2 = node_pressure.get(n2)
min_p = min(p1, p2) if (p1 is not None and p2 is not None) else None
# Pipe category
if diam <= 50: dcat = "微型(≤50mm)"
elif diam <= 100: dcat = "小型(51-100mm)"
elif diam <= 200: dcat = "中型(101-200mm)"
elif diam <= 400: dcat = "大型(201-400mm)"
elif diam <= 800: dcat = "主干(401-800mm)"
else: dcat = "干管(>800mm)"
scored.append({
'id': pid, 'node1': n1, 'node2': n2,
'velocity': round(vel, 4), 'flow': round(m.get('flow',0), 4),
'headloss': round(hl, 4), 'length': round(length, 4),
'diameter': int(diam), 'roughness': int(roughness),
'vel_grade': vg, 'hl_grade': hg, 'composite_score': composite,
'is_bottleneck': is_bn,
'n1_pressure': round(p1, 2) if p1 is not None else None,
'n2_pressure': round(p2, 2) if p2 is not None else None,
'min_pressure': round(min_p, 2) if min_p is not None else None,
'diam_cat': dcat,
})
# 4. Filter & sort
print("[4/5] Filtering bottlenecks...", file=sys.stderr)
bn = [s for s in scored if s['is_bottleneck']]
bn.sort(key=lambda x: (-x['composite_score'], -x['velocity']))
critical = [b for b in bn if b['vel_grade']=='极危']
severe = [b for b in bn if b['vel_grade']=='严重']
high_vel = [b for b in bn if b['vel_grade']=='偏高']
low_p = sum(1 for b in bn if b['min_pressure'] and b['min_pressure'] < 20)
lowish_p = sum(1 for b in bn if b['min_pressure'] and 20 <= b['min_pressure'] < 25)
high_rough = sum(1 for b in bn if b['roughness'] > 130)
# Diam distribution
dd = defaultdict(int)
for b in bn:
d = b['diameter']
if d <= 50: dd['≤50mm']+=1
elif d <= 100: dd['51-100mm']+=1
elif d <= 200: dd['101-200mm']+=1
elif d <= 400: dd['201-400mm']+=1
elif d <= 800: dd['401-800mm']+=1
else: dd['>800mm']+=1
# 5. Output
print("[5/5] Generating report...", file=sys.stderr)
# Print text summary to stderr
print(f"\n{'='*70}", file=sys.stderr)
print(f" 水力瓶颈分析报告 - {TT}", file=sys.stderr)
print(f"{'='*70}", file=sys.stderr)
print(f" 总管道数: {len(scored)}", file=sys.stderr)
print(f" 瓶颈管道: {len(bn)} ({len(bn)/len(scored)*100:.1f}%)", file=sys.stderr)
print(f" 极危(>3.0m/s): {len(critical)} 条", file=sys.stderr)
print(f" 严重(2.0-3.0): {len(severe)} 条", file=sys.stderr)
print(f" 偏高(1.5-2.0): {len(high_vel)} 条", file=sys.stderr)
print(f"\n 水头损失阈值: P80={p80:.4f}m P90={p90:.4f}m P95={p95:.4f}m", file=sys.stderr)
print(f" 平均={sum(hl_vals)/len(hl_vals):.4f}m 最大={max(hl_vals):.4f}m", file=sys.stderr)
print(f"\n 低压节点(<20m): {low_p} 条, 偏低(20-25m): {lowish_p} 条", file=sys.stderr)
print(f" 高粗糙度(>130): {high_rough} 条", file=sys.stderr)
print(f"\n 瓶颈管径分布:", file=sys.stderr)
for cat in ['≤50mm','51-100mm','101-200mm','201-400mm','401-800mm','>800mm']:
print(f" {cat}: {dd.get(cat,0)} 条", file=sys.stderr)
# Text table (Top 30) to stderr
print(f"\n{'='*120}", file=sys.stderr)
print(f"{'排名':<5} {'管道ID':<10} {'流速(m/s)':<10} {'水损(m)':<10} {'管径(mm)':<9} {'管长(km)':<10} {'评分':<4} {'压力1':<8} {'压力2':<8} {'管径类别':<18}", file=sys.stderr)
print('-'*120, file=sys.stderr)
for i, b in enumerate(bn[:30]):
p1s = f"{b['n1_pressure']:.1f}" if b['n1_pressure'] is not None else "-"
p2s = f"{b['n2_pressure']:.1f}" if b['n2_pressure'] is not None else "-"
print(f"{i+1:<5} {b['id']:<10} {b['velocity']:<10.4f} {b['headloss']:<10.2f} {b['diameter']:<9} {b['length']:<10.4f} {b['composite_score']:<4} {p1s:<8} {p2s:<8} {b['diam_cat']:<18}", file=sys.stderr)
# Generate suggestions for each bottleneck
for b in bn:
sug = []
if b['velocity'] > 2.0:
sug.append(f"流速{b['velocity']:.2f}m/s过高,需扩容或分流")
elif b['velocity'] > 1.5:
sug.append(f"流速{b['velocity']:.2f}m/s偏高")
hl = b['headloss']
if hl > p95:
sug.append(f"水头损失{hl:.2f}m(>P95)严重超标")
elif hl > p90:
sug.append(f"水头损失{hl:.2f}m(>P90)")
if b['diameter'] < 100:
sug.append(f"管径{b['diameter']}mm偏小,建议扩径至≥150mm")
elif b['diameter'] < 200:
sug.append(f"管径{b['diameter']}mm,评估扩容至250-300mm")
if b['roughness'] > 130:
sug.append(f"粗糙系数{b['roughness']}偏高,建议内衬修复")
if b['min_pressure'] is not None and b['min_pressure'] < 20:
sug.append(f"端节点压力{b['min_pressure']:.1f}m(<20m),低压区域需增压")
elif b['min_pressure'] is not None and b['min_pressure'] < 25:
sug.append(f"端节点压力{b['min_pressure']:.1f}m偏低")
if b['length'] < 0.01 and b['headloss'] > 0.5:
sug.append("短管高水损,检查是否存在模型异常或局部阻塞")
b['suggestions'] = sug
result = {
'target_time': TT,
'summary': {
'total_pipes': len(scored),
'bottleneck_count': len(bn),
'critical': len(critical), 'severe': len(severe), 'high_vel': len(high_vel),
'low_pressure_nodes': low_p, 'lowish_pressure_nodes': lowish_p,
'high_roughness_pipes': high_rough,
'headloss_p80': round(p80,4), 'headloss_p90': round(p90,4),
'headloss_p95': round(p95,4),
'diameter_distribution': dict(dd),
},
'top_bottlenecks': bn[:args.top],
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()