简介:这份资源面向无线传感器网络课程学习、仿真实验与毕业设计人群,提供基于Matlab与Python双语言实现的完整仿真代码与数据。内容覆盖RSSI测距多边定位、五种网络拓扑结构生成、CSMA/CA五种退避策略对比、粒子群算法优化节点覆盖率,以及改进LEACH路由协议与经典LEACH的仿真比较,适合具备一定编程基础、需要复现或拓展WSN实验的读者。压缩包共85个文件,约1.46MB,以png结果图、py与m源码、md说明文档为主,另含mat数据、npy数组与xml配置,便于直接运行与二次开发。目前已有602人学习下载。各实验模块均配有独立说明,读者可据此快速搭建仿真环境、对比算法性能、分析拓扑与覆盖效果,并借助现成脚本与数据完成实验报告或课题验证。
1. 无线传感器网络仿真的五种拓扑到底差在哪
做无线传感器网络(WSN)仿真的人,绕不开一个前置问题:节点怎么摆。很多人一上来就调路由协议、改 MAC 层参数,结果曲线怎么都不对,最后发现是拓扑结构选错了。基于 Matlab 和 Python 实现无线传感器网络仿真、生成五种网络拓扑结构,本质上解决的就是这件事——把随机拓扑、网格拓扑、环形拓扑、星型拓扑、分簇拓扑这五种常见结构用代码生成出来,输出节点坐标、邻接关系和可视化图,再喂给后续的能耗、覆盖、连通性仿真。
这套东西适合两类人:一类是刚进实验室、要快速搭一个可复现仿真环境的研究生;另一类是做协议对比、需要固定拓扑做消融实验的工程师。五种拓扑不是随便凑数,它们分别对应不同的物理部署场景和协议假设,选错了后面全是玄学。下面我按「先讲清每种拓扑的适用边界,再给可抄的生成代码,最后说踩过的坑」这个顺序展开,Matlab 和 Python 两套实现都会给到。
2. 五种拓扑的生成逻辑与适用场景拆解
2.1 随机拓扑:最像真实部署,也最容易翻车
随机拓扑是 WSN 仿真的默认起点。常见做法是在一个 100m×100m 或 200m×200m 的矩形区域内,用均匀分布撒 N 个节点。它的价值在于模拟无规划投放,比如飞机撒播、灾区临时布设。但随机拓扑有个隐蔽问题:节点可能重叠,也可能出现孤立节点,导致连通性直接崩掉。
Matlab 里生成随机拓扑的核心就三行,但边界处理要自己加:
% 随机拓扑生成:N个节点,区域L×L N = 100; L = 100; rng(42); % 固定随机种子,保证可复现 x = L * rand(1, N); y = L * rand(1, N); % 去重叠:任意两节点距离小于d_min则重新撒该点 d_min = 2; for i = 1:N for j = i+1:N while norm([x(i)-x(j), y(i)-y(j)]) < d_min x(i) = L * rand(); y(i) = L * rand(); end end end scatter(x, y, 30, 'filled'); axis([0 L 0 L]); grid on;逻辑说明:rng(42)是后悔药,不固定种子每次跑出来的拓扑都不一样,实验没法对比。d_min是节点最小间距,物理上对应节点体积,仿真上避免距离矩阵出现零导致除零。参数上,N 一般取 50 到 200,L 取 100 到 500,节点密度 N/L² 决定了平均度,密度低于 0.005 时连通性大概率不满足。
Python 版本用 numpy 更顺手:
import numpy as np import matplotlib.pyplot as plt N, L, d_min = 100, 100, 2 rng = np.random.default_rng(42) pts = rng.uniform(0, L, size=(N, 2)) # 简单去重叠:迭代重撒 for i in range(N): for j in range(i+1, N): while np.linalg.norm(pts[i]-pts[j]) < d_min: pts[i] = rng.uniform(0, L, size=2) plt.scatter(pts[:,0], pts[:,1], s=30) plt.xlim(0, L); plt.ylim(0, L); plt.grid(True) plt.show()参数说明:default_rng(42)是 numpy 新式随机接口,比老的np.random.seed更干净。uniform(0, L, size=(N,2))一次生成坐标矩阵,比循环快一个量级。去重叠的双层循环在 N=200 时还能接受,N 再大就要换网格哈希,否则 O(N²) 会拖慢。
2.2 网格拓扑:可控性最强,但别死板照搬
网格拓扑把节点按行列等距排列,适合规则部署场景,比如农田监测、厂房设备监控。它的最大好处是连通性和覆盖可以解析计算,做协议对比时变量最少。常见做法是 sqrt(N)×sqrt(N) 的方阵,间距 d 由区域边长除以行列数决定。
% 网格拓扑:rows×cols,间距d rows = 10; cols = 10; d = 10; [X, Y] = meshgrid(0:d:(cols-1)*d, 0:d:(rows-1)*d); x = X(:)'; y = Y(:)'; % 可选:加轻微扰动,避免完全对称导致的路径碰撞 jitter = 0.5; x = x + jitter * (rand(size(x)) - 0.5); y = y + jitter * (rand(size(y)) - 0.5); plot(x, y, 'o'); axis equal; grid on;逻辑说明:meshgrid生成规则网格,(:)'拉平成行向量。加jitter是我一般会做的,完全对称的网格在基于地理路由的协议里会出现大量等长路径,仿真结果过于理想。扰动幅度取间距的 5% 以内,太大就破坏网格特性了。
Python 对应实现:
rows, cols, d = 10, 10, 10 xs = np.arange(cols) * d ys = np.arange(rows) * d X, Y = np.meshgrid(xs, ys) pts = np.column_stack([X.ravel(), Y.ravel()]) jitter = 0.5 pts = pts + rng.uniform(-jitter, jitter, pts.shape) plt.scatter(pts[:,0], pts[:,1], s=30) plt.axis('equal'); plt.grid(True)参数说明:d要大于通信半径的 1/√2 才能保证对角邻居也连通,否则网格会退化成多个孤立簇。jitter用均匀分布而不是高斯,避免出现离群点。
2.3 环形与星型:两种极端,各有各的坑
环形拓扑节点排成一个圆,适合管道监测、边界巡逻这类线性场景。星型拓扑所有节点连到一个中心节点,适合单跳汇聚的小规模网络。这两种结构简单,但边界条件容易写错。
环形生成的关键是半径和角度:
% 环形拓扑:N个节点均匀分布在半径R的圆上 N = 50; R = 50; theta = linspace(0, 2*pi, N+1); theta(end) = []; x = R * cos(theta); y = R * sin(theta); % 邻接:每个节点只与前后两个邻居相连 adj = zeros(N); for i = 1:N adj(i, mod(i, N)+1) = 1; adj(mod(i, N)+1, i) = 1; end逻辑说明:linspace(0, 2*pi, N+1)再去掉最后一个点,保证首尾不重合。mod(i, N)+1是环形索引的标准写法,注意 Matlab 索引从 1 开始,Python 从 0 开始,移植时这里最容易翻车。
星型拓扑更简单,但要注意中心节点的位置:
N, R = 30, 50 theta = np.linspace(0, 2*np.pi, N, endpoint=False) leaf = np.column_stack([R*np.cos(theta), R*np.sin(theta)]) center = np.array([[0, 0]]) pts = np.vstack([center, leaf]) # 邻接矩阵:中心与所有叶子相连 adj = np.zeros((N+1, N+1)) adj[0, 1:] = 1 adj[1:, 0] = 1参数说明:endpoint=False避免首尾重叠,这是 numpy 里生成环形坐标的常见写法。星型的中心节点能耗是瓶颈,仿真时要把它的初始能量设高一些,否则跑不了几轮就死了。
2.4 分簇拓扑:最贴近实际协议,也最考验参数
分簇拓扑是 LEACH 这类协议的标配。常见做法是用 K-means 把节点聚成若干簇,每簇选一个簇头。它的难点不在生成,而在簇头数量和簇内距离的平衡。
% 分簇拓扑:先随机撒点,再用kmeans聚成K簇 N = 100; L = 100; K = 5; x = L * rand(1, N); y = L * rand(1, N); [idx, C] = kmeans([x' y'], K, 'Replicates', 5); % 每簇选离质心最近的节点作为簇头 heads = zeros(K, 2); for k = 1:K members = find(idx == k); [~, mi] = min(sum(([x(members)' y(members)'] - C(k,:)).^2, 2)); heads(k,:) = [x(members(mi)) y(members(mi))]; end逻辑说明:kmeans的Replicates参数设 5 是为了避免陷入局部最优,默认只跑一次,结果可能很差。簇头选离质心最近的节点,而不是直接用语质心,因为质心位置可能没有实际节点。参数上,K 一般取 sqrt(N) 左右,N=100 时 K=5 到 10 比较合理,K 太大簇内通信省了但簇头开销上去了。
Python 用 scipy 或 sklearn 都可以:
from sklearn.cluster import KMeans N, L, K = 100, 100, 5 pts = rng.uniform(0, L, size=(N, 2)) km = KMeans(n_clusters=K, n_init=10, random_state=42).fit(pts) labels = km.labels_ centers = km.cluster_centers_ heads = [] for k in range(K): members = pts[labels == k] d = np.linalg.norm(members - centers[k], axis=1) heads.append(members[np.argmin(d)]) heads = np.array(heads)参数说明:n_init=10对应 Matlab 的Replicates,显式指定比默认值更稳。random_state=42保证可复现。簇头数量 K 的选择可以用肘部法,但仿真里一般直接按经验给。
3. 用 Matlab 和 Python 跑通拓扑生成的最小工程
3.1 统一的数据结构:节点表加邻接矩阵
五种拓扑生成方式不同,但输出要统一,否则后续仿真没法复用。我一般会定义两个核心结构:节点表nodes(N×2 的坐标矩阵)和邻接矩阵adj(N×N 的 0/1 矩阵)。邻接矩阵的生成依赖通信半径,不是拓扑本身决定的,但要在生成拓扑后立刻算出来。
% 统一输出:nodes坐标 + adj邻接矩阵 function [nodes, adj] = build_topology(type, N, L, R) switch type case 'random' nodes = L * rand(N, 2); case 'grid' side = ceil(sqrt(N)); [X, Y] = meshgrid(linspace(0, L, side)); nodes = [X(:), Y(:)]; nodes = nodes(1:N, :); case 'ring' theta = linspace(0, 2*pi, N+1)'; theta(end) = []; nodes = [L/2*cos(theta)+L/2, L/2*sin(theta)+L/2]; case 'star' theta = linspace(0, 2*pi, N)'; nodes = [[L/2, L/2]; [L/2*cos(theta)+L/2, L/2*sin(theta)+L/2]]; case 'cluster' nodes = L * rand(N, 2); end % 基于通信半径R生成邻接矩阵 D = pdist2(nodes, nodes); adj = (D <= R) & (D > 0); end逻辑说明:pdist2算距离矩阵,(D <= R) & (D > 0)同时排除自身和超距节点。参数上,R 的取值直接决定平均度,R 太小拓扑会碎成多个连通分量,R 太大就退化成全连接。一般让平均度落在 6 到 10 之间比较合理,可以用mean(sum(adj))检查。
Python 版本:
from scipy.spatial.distance import cdist def build_topology(kind, N, L, R, seed=42): rng = np.random.default_rng(seed) if kind == 'random': nodes = rng.uniform(0, L, size=(N, 2)) elif kind == 'grid': side = int(np.ceil(np.sqrt(N))) xs = np.linspace(0, L, side) X, Y = np.meshgrid(xs, xs) nodes = np.column_stack([X.ravel(), Y.ravel()])[:N] elif kind == 'ring': theta = np.linspace(0, 2*np.pi, N, endpoint=False) nodes = np.column_stack([L/2*np.cos(theta)+L/2, L/2*np.sin(theta)+L/2]) elif kind == 'star': theta = np.linspace(0, 2*np.pi, N, endpoint=False) leaf = np.column_stack([L/2*np.cos(theta)+L/2, L/2*np.sin(theta)+L/2]) nodes = np.vstack([[L/2, L/2], leaf]) elif kind == 'cluster': nodes = rng.uniform(0, L, size=(N, 2)) D = cdist(nodes, nodes) adj = (D <= R) & (D > 0) return nodes, adj参数说明:cdist比手写双层循环快很多,N=500 时差距明显。endpoint=False在环形和星型里都要加,否则首尾节点会重叠。[:N]是网格拓扑的截断,因为side²可能大于 N。
3.2 可视化:一张图看出拓扑是否合理
生成完拓扑,第一件事是画图检查。我一般会画三张子图:节点分布、邻接关系、度分布直方图。度分布能快速暴露问题,比如随机拓扑出现大量度为 0 的孤立节点,或者网格拓扑度分布过于集中。
% 可视化:节点 + 边 + 度分布 figure; subplot(1,3,1); scatter(nodes(:,1), nodes(:,2), 20, 'filled'); axis equal; title('节点分布'); subplot(1,3,2); gplot(adj, nodes, '-'); hold on; scatter(nodes(:,1), nodes(:,2), 10, 'filled'); axis equal; title('邻接关系'); subplot(1,3,3); histogram(sum(adj), 0:max(sum(adj))); title('度分布');逻辑说明:gplot是 Matlab 画邻接图的老牌函数,输入邻接矩阵和坐标即可。sum(adj)按列求和得到每个节点的度。度分布直方图的 bin 用0:max保证每个整数度一个柱子,不然默认分箱会糊在一起。
Python 用 networkx 更直观:
import networkx as nx G = nx.from_numpy_array(adj) pos = {i: nodes[i] for i in range(len(nodes))} nx.draw(G, pos, node_size=20, with_labels=False)参数说明:from_numpy_array把邻接矩阵转成图对象,注意它默认生成无向图,如果邻接矩阵不对称要先处理。pos用真实坐标而不是布局算法,这样拓扑的空间结构才看得出来。
3.3 批量生成与保存:一次跑完五种拓扑
做对比实验时,五种拓扑要能批量生成并保存成统一格式。我一般存成.mat或.npz,包含nodes、adj、type三个字段。
% 批量生成五种拓扑并保存 types = {'random', 'grid', 'ring', 'star', 'cluster'}; N = 100; L = 100; R = 25; for i = 1:length(types) [nodes, adj] = build_topology(types{i}, N, L, R); save(sprintf('topo_%s.mat', types{i}), 'nodes', 'adj'); end逻辑说明:sprintf拼文件名,避免手动改。参数上,R=25 在 L=100 的区域里平均度大概在 8 左右,是个比较稳的起点。如果跑出来度分布太偏,先调 R 再调 N。
Python 对应:
for kind in ['random', 'grid', 'ring', 'star', 'cluster']: nodes, adj = build_topology(kind, 100, 100, 25) np.savez(f'topo_{kind}.npz', nodes=nodes, adj=adj)参数说明:np.savez不压缩,np.savez_compressed压缩但慢,数据量小的时候无所谓。文件名带类型,后续加载时不用记。
4. 拓扑生成里最容易踩的五个坑
4.1 随机种子没固定,实验没法复现
现象:同一份代码跑两次,连通性曲线差很多,审稿人问为什么结果不一致。原因:rand或np.random默认用系统时间做种子。解决:在生成拓扑前显式设种子,Matlab 用rng(42),Python 用default_rng(42)。注意rng要放在所有随机操作之前,中途设没用。
4.2 网格拓扑的间距小于通信半径的 1/√2
现象:网格拓扑跑出来连通分量不止一个,对角节点连不上。原因:网格间距 d 太大,对角距离 d√2 超过通信半径 R。解决:保证 d ≤ R/√2,或者把邻接矩阵的生成改成考虑对角连接。我一般直接让 d = R/√2 再留 10% 余量。
4.3 环形拓扑首尾节点重叠
现象:环形拓扑画出来少一个节点,或者第一个和最后一个节点坐标一样。原因:linspace(0, 2*pi, N)包含终点,首尾重合。解决:用linspace(0, 2*pi, N+1)再去掉最后一个,或者 Python 里用endpoint=False。这个坑在 Matlab 和 Python 之间移植时特别常见。
4.4 分簇拓扑的簇头选在空位置
现象:簇头坐标不在任何实际节点上,后续仿真找不到对应节点。原因:直接用语质心当簇头。解决:选簇内离质心最近的实际节点,代码里就是argmin那一步。另外 K-means 的Replicates或n_init要设大一点,不然簇划分本身就不稳。
4.5 邻接矩阵把自身算成邻居
现象:度分布里每个节点度都至少是 1,孤立节点检测不出来。原因:距离矩阵对角线为 0,D <= R把自身也算进去了。解决:加D > 0条件,或者生成后把对角线置零。这个坑在随机拓扑里最隐蔽,因为节点密集时看不出来。
5. 从拓扑到仿真:一个可验证的连通性检查技巧
生成完拓扑,别急着跑路由协议,先做连通性检查。这一步能省掉后面大量无效实验。我一般用广度优先搜索(BFS)从第一个节点出发,看能覆盖多少节点,覆盖率低于 90% 就说明拓扑有问题,先调 R 或 N。
% 连通性检查:BFS从节点1出发 function ratio = connectivity_ratio(adj) N = size(adj, 1); visited = false(1, N); queue = 1; visited(1) = true; while ~isempty(queue) u = queue(1); queue(1) = []; neighbors = find(adj(u, :) & ~visited); visited(neighbors) = true; queue = [queue, neighbors]; end ratio = sum(visited) / N; end逻辑说明:find(adj(u,:) & ~visited)一次找出所有未访问邻居,比逐个判断快。queue用数组模拟,N 不大时够用,N 上千就换真正的队列结构。参数上,连通性比率低于 0.9 就要警惕,低于 0.7 基本没法用。
Python 版本用 collections.deque:
from collections import deque def connectivity_ratio(adj): N = adj.shape[0] visited = np.zeros(N, dtype=bool) visited[0] = True q = deque([0]) while q: u = q.popleft() for v in np.where(adj[u] & ~visited)[0]: visited[v] = True q.append(v) return visited.sum() / N参数说明:deque的popleft是 O(1),比 list 的pop(0)快。np.where返回索引数组,直接迭代。这个检查跑一次只要几毫秒,但能挡掉大部分低级错误。
五种拓扑里,随机拓扑的连通性最不稳定,网格和环形最稳,星型取决于中心节点是否在通信半径内覆盖所有叶子,分簇拓扑要看簇头之间是否连通。我一般会把这五种拓扑的连通性比率打出来对比,作为实验的第一张表。
最后一个习惯:每次生成拓扑后,把nodes、adj、R、N、L和随机种子一起存进日志文件。后面结果不对时,能直接定位是哪次生成的拓扑有问题,不用重新跑一遍。这个习惯帮我省过很多次返工,希望帮到你。
本文还有配套的精品资源,点击获取