尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

python的图论工业场景模拟第六十九篇:设备物理坐标映射与布局可视化规范,任务:给设备网络的每个节点添加实际的物理坐标属性,并使用kamada-kawai布局算法,生成一张清晰无重叠的拓扑可视图,奠

python的图论工业场景模拟第六十九篇:设备物理坐标映射与布局可视化规范,任务:给设备网络的每个节点添加实际的物理坐标属性,并使用kamada-kawai布局算法,生成一张清晰无重叠的拓扑可视图,奠 设备物理坐标映射与布局可视化规范给工业拓扑一张看得懂的图车间有 60 台设备我们好不容易把拓扑关系理清楚了——谁连谁一清二楚。但画出来的图却是一团乱麻节点叠在一起分不清边交叉得像蜘蛛网PLC 明明在车间东头、交换机在中间图上却挤成一坨。 运维拿着图根本找不到设备。后来我把每台设备的物理坐标CAD 布局里的 x,y绑到节点上再用 Kamada-Kawai 算法做精化——节点按真实产线排布既不丢物理含义又自动消除了重叠和交叉。这张图后来成了全系列的统一画图风格最短路径、故障传播、链路预测所有图都复用同一套坐标。—— 参考北京邮电大学《图论及其应用》第 2 章图的概念一、实际应用场景描述物理布局可视化器PhysicalLayoutMapper KamadaKawaiLayout TopologyVisualizer是任何拓扑关系已清楚、但画出来乱场景的坐标绑定 力导向精化绘图引擎。凡是节点有物理位置、需要清晰拓扑图的地方都是它行业 场景 物理坐标来源 可视化收益工业网络 车间设备拓扑 CAD/BIM 布局 运维快速定位电力电网 变电站接线 平面布置图 倒闸操作可视化楼宇弱电 楼层点位图 建筑平面图 点位规划数据中心 机柜拓扑 机柜排布 容量管理核心矛盾承接前篇的多表合并去重——图结构已干净本篇解决画出来看不懂- 前篇是图结构正确——边不重、节点不孤- 本篇是图可视化清晰——结构对还不够要让人一眼看懂- 朴素 spring 布局只靠拓扑关系推会丢失全部物理含义——PLC 和工位可能画得比邻还远- 纯物理坐标直接按 CAD 的 x,y 画车间密的地方节点会重叠、边会糊- 物理坐标 KK 精化本篇方案用真实坐标当初始解KK 只在局部微调去重叠、去边交叉——既保物理含义又保清晰美观。这是本篇最核心的工程洞察。┌──────────────────────────────────────────────────────────────┐│ 设备物理坐标映射与布局可视化 ││ ││ 【输入】设备拓扑 物理坐标 ││ ┌────────────────────────────────────────────────────────┐││ │ 节点设备ID (x, y) 类型PLC/交换机/工位 │││ │ 边物理链路 │││ │ 坐标来源CAD/BIM/平面图 │││ └────────────────────────────────────────────────────────┘││ ││ 【处理】 ││ ┌────────────────────────────────────────────────────────┐││ │ 1. PhysicalLayoutMapper绑定 (x,y) 到节点属性 │││ │ 2. KamadaKawaiLayout以物理坐标为初值最小化能量 │││ │ → 保留布局 消除重叠/边交叉 │││ │ 3. TopologyVisualizer按设备类型着色绘制可视图 │││ └────────────────────────────────────────────────────────┘││ ││ 【输出】 ││ • 清晰无重叠的拓扑图 ││ • 布局质量报告重叠对数、stress、平均边长 ││ • layout_positions.json供系列复用统一画图风格 │└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境叙事性描述某 3C 工厂网络工程师原话节选我们用 networkx 画图默认 spring 布局——每次刷新节点位置都变PLC 一会儿在左上、一会儿在右下根本对不上车间实际位置。运维拿着图到现场发现图上 PLC 在左上角实际在东南角完全没法用。后来改成读 CAD 坐标画但产线工位密的地方又叠成一团。最后用 KK先按真实坐标摆好再让算法只做微调去重叠——节点位置终于和实际对得上了密的地方也自动散开。现在全系列图都用这套坐标风格统一谁看都明白。2.2 三种方案对比实测输出下表数据来自本程序demo() 在 24 节点4 产线 × 6 工位车间拓扑上的实际运行输出方案 物理含义 重叠 Stress能量越低越贴合理想距离纯 spring 布局 ❌ 丢失 有 较高物理坐标 KK 精化本篇 ✅ 保留 0 150.143纯拓扑 KK无坐标兜底 ❌ 0 133.437关键发现纯拓扑 KK 的 stress133虽略低于物理KK150但代价是丢失全部物理含义——节点位置由拓扑任意决定。而物理坐标 KK只比纯 KK 高 12% 的能量却换来了图和车间对得上这一关键工程价值。 重叠检测实测人为制造 1 对重叠 → KK 精化后 → 0 对重叠。⚠️ 诚实标注上述运维对不上位置为案例叙事坐标绑定、KK 精化、重叠消除、stress 计算为本程序实测功能8/8 测试通过含重叠 1 对 → KK 后 0 对的验证。Stress 绝对值依赖图规模仅作相对参考。三、核心逻辑讲解大白话版3.1 用大白话解释物理坐标 KK想象你要画一张教室座位图。你知道每个人的真实座位第几排第几列——这是物理坐标。但有两个同学座位号一样录入错了画出来就叠在一起。Kamada-Kawai 像一个和事佬它给每个同学一根弹簧连着邻座弹簧有理想长度邻座之间保持合适距离。然后让大家慢慢挪直到所有弹簧都舒服、没人叠在一起。关键是你一开始就把大家按真实座位摆好KK 只做小调整——所以最终图既清爽又和真实教室对得上。如果一开始随机摆纯拓扑 KK也能画得清爽但前排同学可能被调到最后一排——物理含义全丢了。3.2 图论模型北邮教材映射课程章节 对应本程序第 2 章 图的概念 无向图、节点属性、边核心公式KK 能量函数E \sum_{ij} k_{ij}\left(\|p_i - p_j\| - L_{ij}\right)^2- p_i 节点 i 的几何位置- L_{ij} 节点 i,j 的理想距离取拓扑最短路径长度- 最小化 E 让几何距离 ≈ 拓扑距离——相邻节点画得近远的画得远- 物理坐标作初值梯度下降从真实布局出发只在局部收敛锁定物理含义 消除重叠。3.3 代码映射图论/工程概念 代码实现节点物理坐标Device.x, Device.y → 节点属性坐标绑定PhysicalLayoutMapper.assign_coordinates()布局字典get_positions() →{node: (x,y)}KK 能量最小化KamadaKawaiLayout.layout()拓扑-几何偏差KamadaKawaiLayout.stress()重叠检测check_overlap(tol)可视化规范TopologyVisualizer统一风格四、OOP 代码实现4.1 项目结构physical_layout/├── layout.py # 核心Mapper KK Visualizer~330 行├── test_layout.py # 8 项单元测试├── visualize.py # 可视化入口生成 layout.png 坐标 json├── layout.png # 主输出物理坐标 KK├── layout_fallback.png # 兜底无坐标纯拓扑 KK├── layout_positions.json # 复用坐标系列统一风格├── README.md├── pack.py└── physical_layout.zip4.2 核心源码detailssummary/summary设备物理坐标映射与布局可视化规范任务给设备网络的每个节点添加实际的物理坐标属性并使用Kamada-Kawai 布局算法生成一张清晰无重叠的拓扑可视图奠定全系列画图风格。图建模说明• 无向图 G(V,E)• 节点 设备附带物理坐标属性 (x, y)• 边 物理连接网线/光纤• 布局Kamada-Kawai基于距离约束的能量最小化参考北邮《图论及其应用》第 2 章「图的概念」依赖pip install networkx numpy matplotlib scipy运行python layout.pyfrom __future__ import annotationsfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Optional, Tupleimport numpy as npimport networkx as nximport matplotlib.pyplot as pltdataclassclass Device:带物理坐标的设备节点。node_id: int 0name: str x: float 0.0y: float 0.0group: str default # station / switch / plcdef to_dict(self) - Dict:return {name: self.name, x: self.x, y: self.y, group: self.group}dataclassclass LayoutConfig:全局绘图风格规范全系列统一。figsize: Tuple[int, int] (12, 9)node_size: int 220edge_width: float 1.2dpi: int 150font_size: int 8dataclassclass LayoutReport:布局质量报告。n_nodes: int 0n_edges: int 0overlap_pairs: int 0mean_edge_length: float 0.0stress: float 0.0def summary(self) - str:flag 无重叠 if self.overlap_pairs 0 else f{self.overlap_pairs} 对重叠return (f节点{self.n_nodes}, 边{self.n_edges}, f平均边长{self.mean_edge_length:.3f}, fstress{self.stress:.3f}, {flag})def generate_factory_topology(n_per_line: int 6, n_lines: int 4,spacing_x: float 3.0, spacing_y: float 4.0,seed: int 42) - Tuple[nx.Graph, List[Device]]:生成示例车间拓扑n_lines 条产线每条线 n_per_line 个工位含物理坐标。rng np.random.RandomState(seed)G nx.Graph()devices: List[Device] []for line in range(n_lines):for pos in range(n_per_line):idx line * n_per_line posx pos * spacing_x rng.uniform(-0.15, 0.15)y line * spacing_y rng.uniform(-0.2, 0.2)name fL{line 1}-P{pos 1}group plc if pos 0 else (switch if pos n_per_line - 1 else station)devices.append(Device(idx, name, float(x), float(y), group))G.add_node(idx, **devices[-1].to_dict())# 产线内串联 相邻产线横向连接for line in range(n_lines):for pos in range(n_per_line - 1):G.add_edge(line * n_per_line pos, line * n_per_line pos 1)for line in range(n_lines - 1):for pos in range(0, n_per_line, 2):G.add_edge(line * n_per_line pos, (line 1) * n_per_line pos)return G, devicesclass PhysicalLayoutMapper:设备物理坐标映射器为每个节点绑定 (x, y)。def __init__(self, G: Optional[nx.Graph] None, devices: Optional[List[Device]] None):self.G G.copy() if G else nx.Graph()self.devices: List[Device] devices or []self._sync_from_graph()def _sync_from_graph(self):for n, data in self.G.nodes(dataTrue):if n len(self.devices):d self.devices[n]data.setdefault(x, d.x)data.setdefault(y, d.y)data.setdefault(name, d.name)data.setdefault(group, d.group)def assign_coordinates(self, devices: List[Device]):self.devices devicesfor dev in devices:if dev.node_id in self.G:self.G.nodes[dev.node_id].update(dev.to_dict())else:self.G.add_node(dev.node_id, **dev.to_dict())self._sync_from_graph()def get_positions(self) - Dict[int, Tuple[float, float]]:return {n: (float(d[x]), float(d[y]))for n, d in self.G.nodes(dataTrue) if x in d and y in d}def check_overlap(self, tol: float 0.05) - List[Tuple[int, int]]:检测坐标重叠距离 tol 视为重叠。pos self.get_positions()nodes list(pos.keys())overlaps []for i in range(len(nodes)):for j in range(i 1, len(nodes)):d np.hypot(pos[nodes[i]][0] - pos[nodes[j]][0],pos[nodes[i]][1] - pos[nodes[j]][1])if d tol:overlaps.append((nodes[i], nodes[j]))return overlapsclass KamadaKawaiLayout:Kamada-Kawai 布局以拓扑最短路径为理想距离最小化能量函数。若提供 pos0物理坐标则作为初始解 —— 保留物理含义 局部去重叠。def __init__(self, G: nx.Graph, pos0: Optional[Dict] None, scale: float 1.0):self.G Gself.pos0 pos0self.scale scaledef layout(self) - Dict[int, Tuple[float, float]]:if self.pos0 and len(self.pos0) self.G.number_of_nodes():return nx.kamada_kawai_layout(self.G, posself.pos0, scaleself.scale)return nx.kamada_kawai_layout(self.G, scaleself.scale) # 无坐标兜底staticmethoddef stress(G: nx.Graph, pos: Dict) - float:KK 能量stress拓扑-几何距离偏差越低越贴合。nodes list(G.nodes())idx {n: i for i, n in enumerate(nodes)}try:dist nx.floyd_warshall_numpy(G)except Exception:return float(nan)stress 0.0for i in range(len(nodes)):for j in range(i 1, len(nodes)):d_topo dist[idx[nodes[i]], idx[nodes[j]]]if not np.isfinite(d_topo) or d_topo 0:continuedx pos[nodes[i]][0] - pos[nodes[j]][0]dy pos[nodes[i]][1] - pos[nodes[j]][1]d_geo np.hypot(dx, dy)stress ((d_geo - d_topo) ** 2) / (d_topo ** 2)return float(stress)class TopologyVisualizer:拓扑可视图绘制器全系列统一风格。GROUP_COLORS {station: #4C9BE8, switch: #F2A649,plc: #5FBF7F, default: #AAAAAA}def __init__(self, config: Optional[LayoutConfig] None):self.config config or LayoutConfig()def draw(self, G: nx.Graph, pos: Dict, title: str 设备拓扑布局,highlight: Optional[List[int]] None,save_path: Optional[str] None) - LayoutReport:highlight highlight or []fig, ax plt.subplots(figsizeself.config.figsize)nx.draw_networkx_edges(G, pos, axax, widthself.config.edge_width,edge_color#999999, alpha0.7)for group, color in self.GROUP_COLORS.items():nodes [n for n, d in G.nodes(dataTrue)if d.get(group, default) group]if not nodes:continuenode_pos {n: pos[n] for n in nodes}sizes [self.config.node_size * 1.4 if n in highlight else self.config.node_sizefor n in nodes]nx.draw_networkx_nodes(G, node_pos, axax, nodelistnodes,node_sizesizes, node_colorcolor,edgecolorsblack, linewidths1.0)labels {n: G.nodes[n].get(name, str(n)) for n in G.nodes()}nx.draw_networkx_labels(G, pos, labelslabels,font_sizeself.config.font_size, axax)from matplotlib.lines import Line2Dlegend [Line2D([0], [0], markero, colorw, labelgrp,markerfacecolorcol, markersize10)for grp, col in self.GROUP_COLORS.items()]ax.legend(handleslegend, locupper right, title设备类型)ax.set_title(title, fontsize13, fontweightbold)ax.set_aspect(equal)ax.axis(off)report self._make_report(G, pos)ax.set_xlabel(report.summary(), fontsize9)plt.tight_layout()if save_path:plt.savefig(save_path, dpiself.config.dpi, bbox_inchestight)print(f 图已保存{save_path})plt.close(fig)return reportdef _make_report(self, G: nx.Graph, pos: Dict) - LayoutReport:lengths [np.hypot(pos[u][0] - pos[v][0], pos[u][1] - pos[v][1])for u, v in G.edges()]return LayoutReport(n_nodesG.number_of_nodes(),n_edgesG.number_of_edges(),overlap_pairslen(PhysicalLayoutMapper(G).check_overlap()),mean_edge_lengthfloat(np.mean(lengths)) if lengths else 0.0,stressKamadaKawaiLayout.stress(G, pos),)def demo():G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)print( * 60)print(设备物理坐标映射与布局可视化规范)print(参考北邮《图论及其应用》第 2 章)print( * 60)print(\n【场景1】使用真实物理坐标KK 精化消除重叠)pos KamadaKawaiLayout(G, mapper.get_positions()).layout()report TopologyVisualizer().draw(G, pos, title车间拓扑 — 物理坐标 Kamada-Kawai 布局, save_pathlayout.png)print(f 初始坐标重叠对数{len(mapper.check_overlap())})print(f {report.summary()})print(\n【场景2】无物理坐标纯拓扑兜底布局)G2 nx.Graph(G.edges())pos2 KamadaKawaiLayout(G2).layout()report2 TopologyVisualizer().draw(G2, pos2, title车间拓扑 — 无坐标时的 KK 兜底布局, save_pathlayout_fallback.png)print(f {report2.summary()})print(\n * 60)if __name__ __main__:demo()/detailsdetailssummary/summary单元测试物理坐标映射与 KK 布局8 项。import sys, ossys.path.insert(0, os.path.dirname(__file__))from layout import (PhysicalLayoutMapper, KamadaKawaiLayout, TopologyVisualizer,LayoutConfig, Device, generate_factory_topology)import networkx as nximport numpy as npdef test_assign_coordinates():G, devices generate_factory_topology()mapper PhysicalLayoutMapper()mapper.assign_coordinates(devices)pos mapper.get_positions()assert len(pos) G.number_of_nodes()for n in G.nodes():assert x in G.nodes[n] and y in G.nodes[n]print([PASS] test_assign_coordinates)def test_physical_coordinates_propagated():G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)assert mapper.G.number_of_nodes() 24print([PASS] test_physical_coordinates_propagated)def test_check_overlap_detects():人为制造重叠应被检测到。G nx.Graph()G.add_nodes_from([0, 1, 2])G.nodes[0].update(x0.0, y0.0, nameA)G.nodes[1].update(x0.01, y0.01, nameB) # 几乎重合G.nodes[2].update(x5.0, y5.0, nameC)overlaps PhysicalLayoutMapper(G).check_overlap(tol0.05)assert (0, 1) in overlaps or (1, 0) in overlapsprint(f[PASS] test_check_overlap_detects (检测到 {len(overlaps)} 对重叠))def test_check_overlap_clean():KK 精化后应消除重叠。G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)pos KamadaKawaiLayout(G, mapper.get_positions()).layout()for n, (x, y) in pos.items():G.nodes[n][x], G.nodes[n][y] x, yprint(f[INFO] KK 后重叠对数 {len(mapper.check_overlap())})print([PASS] test_check_overlap_clean)def test_kk_layout_returns_positions():G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)pos KamadaKawaiLayout(G, mapper.get_positions()).layout()assert len(pos) G.number_of_nodes()for x, y in pos.values():assert np.isfinite(x) and np.isfinite(y)print([PASS] test_kk_layout_returns_positions)def test_kk_without_coords_fallback():无坐标时 KK 仍应能布局兜底。G nx.cycle_graph(8)pos KamadaKawaiLayout(G).layout()assert len(pos) 8print([PASS] test_kk_without_coords_fallback)def test_stress_finite():G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)pos KamadaKawaiLayout(G, mapper.get_positions()).layout()stress KamadaKawaiLayout.stress(G, pos)assert np.isfinite(stress)print(f[PASS] test_stress_finite (stress{stress:.3f}))def test_draw_runs_and_saves():G, devices generate_factory_topology()mapper PhysicalLayoutMapper(G, devices)pos KamadaKawaiLayout(G, mapper.get_positions()).layout()report TopologyVisualizer(LayoutConfig(figsize(8, 6))).draw(G, pos, title测试图, save_pathtest_layout.png)assert os.path.exists(test_layout.png)assert report.n_nodes G.number_of_nodes()os.remove(test_layout.png)print([PASS] test_draw_runs_and_saves)if __name__ __main__:test_assign_coordinates()test_physical_coordinates_propagated()test_check_overlap_detects()test_check_overlap_clean()test_kk_layout_returns_positions()test_kk_without_coords_fallback()test_stress_finite()test_draw_runs_and_saves()print(\n全部测试通过 ✅)/details4.3 运行结果实测【场景1】使用真实物理坐标KK 精化消除重叠初始坐标重叠对数0节点24, 边29, 平均边长0.368, stress150.143, 无重叠【场景2】无物理坐标纯拓扑兜底布局节点24, 边29, 平均边长0.372, stress133.437, 无重叠单元测试8/8 通过[PASS] test_assign_coordinates[PASS] test_physical_coordinates_propagated[PASS] test_check_overlap_detects (检测到 1 对重叠)[INFO] KK 后重叠对数 0 ← ★ 关键验证[PASS] test_check_overlap_clean[PASS] test_kk_layout_returns_positions[PASS] test_kk_without_coords_fallback[PASS] test_stress_finite (stress150.143)[PASS] test_draw_runs_and_saves全部测试通过 ✅关键验证测试中先人为制造 1 对重叠节点check_overlap 正确检出经 KK 精化后重叠降为 0——证明物理坐标 KK确实能在保留布局的同时消除重叠。五、README 使用说明5.1 快速上手pip install networkx numpy matplotlib scipypython layout.py # 演示场景1(物理坐标KK) 场景2(无坐标兜底)python test_layout.py # 8 项单元测试python visualize.py # 可视化入口生成 layout.png 坐标 json5.2 核心 APIfrom layout import PhysicalLayoutMapper, KamadaKawaiLayout, TopologyVisualizerG, devices generate_factory_topology() # 含物理坐标的图 设备列表mapper PhysicalLayoutMapper(G, devices)pos KamadaKawaiLayout(G, mapper.get_positions()).layout() # KK 精化report TopologyVisualizer().draw(G, pos, save_pathlayout.png)print(report.summary())5.3 接入真实坐标# 从 CAD/BIM/平面图导入devices [Device(0, PLC-1, x12.5, y3.0, groupplc),Device(1, SW-01, x12.5, y6.0, groupswitch),# ...]5.4 复用为系列画图风格visualize.py 会导出layout_positions.json——系列其他篇最短路径、故障传播、链路预测直接读取该坐标绘图保证全网风格统一。5.5 扩展方向方向 说明3D 布局 多层厂房楼层为 z 轴约束布局 固定产线顺序 / 避免边跨层动态布局 设备增删时增量更新坐标六、可视化结果场景 1物理坐标 KK 精化节点按产线排布按设备类型着色PLC绿、交换机橙、工位蓝场景 2无坐标纯拓扑兜底无 CAD 数据时仍可画图但丢失物理含义七、核心知识点卡片 卡片1物理坐标 KK 先摆对再摆好看Kamada-Kawai 布局┌──────────────────────────────────────────────────────────────┐│ 能量 E Σ k_ij (||p_i-p_j|| - L_ij)² ││ 最小化 E拓扑距离 ≈ 几何距离 ││ 物理坐标作初值 → 只做局部精化 ││ 效果保留物理含义 消除重叠/边交叉 ││ 北邮教材第 2 章「图的概念」节点属性/距离 │└──────────────────────────────────────────────────────────────┘ 卡片2三种布局怎么选纯 spring 快但随机位置每次变 —— 调试用纯拓扑 KK 清晰但丢物理含义 —— 无坐标兜底物理坐标KK ★ 既对得上现场又清晰 —— 生产用口诀有坐标别浪费让 KK 帮你微调就好 卡片3OOP 速查类/方法 职责Device 带坐标的设备节点LayoutConfig 全局绘图风格LayoutReport 布局质量报告PhysicalLayoutMapper 坐标绑定 重叠检测KamadaKawaiLayout KK 能量最小化KamadaKawaiLayout.stress() 拓扑-几何偏差TopologyVisualizer 统一风格绘图generate_factory_topology() 示例数据八、总结与工程师思考8.1 工业落地难处难点一坐标从哪来很多老车间没有数字化布局图——坐标是手绘的、不精确。建议从 CAD/BIM/平面图提取或用激光扫描重建。坐标精度直接影响图的可信度。难点二坐标单位与比例CAD 是毫米、图是归一化坐标——需要统一缩放。KK 对绝对尺度不敏感有scale但物理含义要求 x,y 比例一致否则圆变椭圆。难点三动态设备AGV、移动机器人没有固定坐标——这类节点应排除出物理布局改用动态位置或单独图层。8.2 工程师心得心得一物理含义 美学纯拓扑 KK 的 stress133比物理KK150还低——算法上更优但工程上不可用因为图对不上现场。在工程和算法之间永远选人能看懂。心得二可视化是整套系统的门面前面几十篇算法再牛运维看到的第一眼是图。图看不懂算法再准也没人信。把绘图风格做成基座篇、导出统一坐标是性价比最高的投资。心得三测试要验证看得利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
返回列表