Files
TJWaterAgent/.opencode/skills/workflow/hydraulic-bottleneck-analysis/scripts/bottleneck_analysis.py
T
jiang 80cfc1f2ab
Generic Container CI/CD / test-build-publish (push) Successful in 2m44s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Successful in 2m44s
feat(agent): sandbox conversation analysis
2026-08-25 16:08:33 +08:00

197 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
水力瓶颈管道综合分析
数据源:管道属性 + 实时水力 + 节点压力 → 复合评分 → 改造建议
注:realtime links 的 setting 字段为无效值,已移除所有基于 setting 的判定。
"""
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['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['id']; prop = pipe_map.get(pid)
if prop:
merged.append({**prop, **r, '_prop_id': prop['id'], '_rt_id': r['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()