絡(luò)仿真:五種拓?fù)渖膳c連通性檢查實戰(zhàn))
簡介這份資源面向無線傳感器網(wǎng)絡(luò)課程學(xué)習(xí)、仿真實驗與畢業(yè)設(shè)計人群提供基于Matlab與Python雙語言實現(xiàn)的完整仿真代碼與數(shù)據(jù)。內(nèi)容覆蓋RSSI測距多邊定位、五種網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)生成、CSMA/CA五種退避策略對比、粒子群算法優(yōu)化節(jié)點覆蓋率以及改進(jìn)LEACH路由協(xié)議與經(jīng)典LEACH的仿真比較適合具備一定編程基礎(chǔ)、需要復(fù)現(xiàn)或拓展WSN實驗的讀者。壓縮包共85個文件約1.46MB以png結(jié)果圖、py與m源碼、md說明文檔為主另含mat數(shù)據(jù)、npy數(shù)組與xml配置便于直接運行與二次開發(fā)。目前已有602人學(xué)習(xí)下載。各實驗?zāi)K均配有獨立說明讀者可據(jù)此快速搭建仿真環(huán)境、對比算法性能、分析拓?fù)渑c覆蓋效果并借助現(xiàn)成腳本與數(shù)據(jù)完成實驗報告或課題驗證。1. 無線傳感器網(wǎng)絡(luò)仿真的五種拓?fù)涞降撞钤谀淖鰺o線傳感器網(wǎng)絡(luò)WSN仿真的人繞不開一個前置問題節(jié)點怎么擺。很多人一上來就調(diào)路由協(xié)議、改 MAC 層參數(shù)結(jié)果曲線怎么都不對最后發(fā)現(xiàn)是拓?fù)浣Y(jié)構(gòu)選錯了?;?Matlab 和 Python 實現(xiàn)無線傳感器網(wǎng)絡(luò)仿真、生成五種網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)本質(zhì)上解決的就是這件事——把隨機(jī)拓?fù)?、網(wǎng)格拓?fù)洹h(huán)形拓?fù)?、星型拓?fù)?、分簇拓?fù)溥@五種常見結(jié)構(gòu)用代碼生成出來輸出節(jié)點坐標(biāo)、鄰接關(guān)系和可視化圖再喂給后續(xù)的能耗、覆蓋、連通性仿真。這套東西適合兩類人一類是剛進(jìn)實驗室、要快速搭一個可復(fù)現(xiàn)仿真環(huán)境的研究生另一類是做協(xié)議對比、需要固定拓?fù)渥鱿趯嶒灥墓こ處煛N宸N拓?fù)洳皇请S便湊數(shù)它們分別對應(yīng)不同的物理部署場景和協(xié)議假設(shè)選錯了后面全是玄學(xué)。下面我按「先講清每種拓?fù)涞倪m用邊界再給可抄的生成代碼最后說踩過的坑」這個順序展開Matlab 和 Python 兩套實現(xiàn)都會給到。2. 五種拓?fù)涞纳蛇壿嬇c適用場景拆解2.1 隨機(jī)拓?fù)渥钕裾鎸嵅渴鹨沧钊菀追囯S機(jī)拓?fù)涫?WSN 仿真的默認(rèn)起點。常見做法是在一個 100m×100m 或 200m×200m 的矩形區(qū)域內(nèi)用均勻分布撒 N 個節(jié)點。它的價值在于模擬無規(guī)劃投放比如飛機(jī)撒播、災(zāi)區(qū)臨時布設(shè)。但隨機(jī)拓?fù)溆袀€隱蔽問題節(jié)點可能重疊也可能出現(xiàn)孤立節(jié)點導(dǎo)致連通性直接崩掉。Matlab 里生成隨機(jī)拓?fù)涞暮诵木腿械吔缣幚硪约杭? 隨機(jī)拓?fù)渖蒒個節(jié)點區(qū)域L×L N 100; L 100; rng(42); % 固定隨機(jī)種子保證可復(fù)現(xiàn) x L * rand(1, N); y L * rand(1, N); % 去重疊任意兩節(jié)點距離小于d_min則重新撒該點 d_min 2; for i 1:N for j i1: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)是后悔藥不固定種子每次跑出來的拓?fù)涠疾灰粯訉嶒灈]法對比。d_min是節(jié)點最小間距物理上對應(yīng)節(jié)點體積仿真上避免距離矩陣出現(xiàn)零導(dǎo)致除零。參數(shù)上N 一般取 50 到 200L 取 100 到 500節(jié)點密度 N/L2 決定了平均度密度低于 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(i1, N): while np.linalg.norm(pts[i]-pts[j]) d_min: pts[i] rng.uniform(0, L, size2) plt.scatter(pts[:,0], pts[:,1], s30) plt.xlim(0, L); plt.ylim(0, L); plt.grid(True) plt.show()參數(shù)說明default_rng(42)是 numpy 新式隨機(jī)接口比老的np.random.seed更干凈。uniform(0, L, size(N,2))一次生成坐標(biāo)矩陣比循環(huán)快一個量級。去重疊的雙層循環(huán)在 N200 時還能接受N 再大就要換網(wǎng)格哈希否則 O(N2) 會拖慢。2.2 網(wǎng)格拓?fù)淇煽匦宰顝?qiáng)但別死板照搬網(wǎng)格拓?fù)浒压?jié)點按行列等距排列適合規(guī)則部署場景比如農(nóng)田監(jiān)測、廠房設(shè)備監(jiān)控。它的最大好處是連通性和覆蓋可以解析計算做協(xié)議對比時變量最少。常見做法是 sqrt(N)×sqrt(N) 的方陣間距 d 由區(qū)域邊長除以行列數(shù)決定。% 網(wǎng)格拓?fù)鋜ows×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(:); % 可選加輕微擾動避免完全對稱導(dǎo)致的路徑碰撞 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生成規(guī)則網(wǎng)格(:)拉平成行向量。加jitter是我一般會做的完全對稱的網(wǎng)格在基于地理路由的協(xié)議里會出現(xiàn)大量等長路徑仿真結(jié)果過于理想。擾動幅度取間距的 5% 以內(nèi)太大就破壞網(wǎng)格特性了。Python 對應(yīng)實現(xiàn)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], s30) plt.axis(equal); plt.grid(True)參數(shù)說明d要大于通信半徑的 1/√2 才能保證對角鄰居也連通否則網(wǎng)格會退化成多個孤立簇。jitter用均勻分布而不是高斯避免出現(xiàn)離群點。2.3 環(huán)形與星型兩種極端各有各的坑環(huán)形拓?fù)涔?jié)點排成一個圓適合管道監(jiān)測、邊界巡邏這類線性場景。星型拓?fù)渌泄?jié)點連到一個中心節(jié)點適合單跳匯聚的小規(guī)模網(wǎng)絡(luò)。這兩種結(jié)構(gòu)簡單但邊界條件容易寫錯。環(huán)形生成的關(guān)鍵是半徑和角度% 環(huán)形拓?fù)銷個節(jié)點均勻分布在半徑R的圓上 N 50; R 50; theta linspace(0, 2*pi, N1); theta(end) []; x R * cos(theta); y R * sin(theta); % 鄰接每個節(jié)點只與前后兩個鄰居相連 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, N1)再去掉最后一個點保證首尾不重合。mod(i, N)1是環(huán)形索引的標(biāo)準(zhǔn)寫法注意 Matlab 索引從 1 開始Python 從 0 開始移植時這里最容易翻車。星型拓?fù)涓唵蔚⒁庵行墓?jié)點的位置N, R 30, 50 theta np.linspace(0, 2*np.pi, N, endpointFalse) 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((N1, N1)) adj[0, 1:] 1 adj[1:, 0] 1參數(shù)說明endpointFalse避免首尾重疊這是 numpy 里生成環(huán)形坐標(biāo)的常見寫法。星型的中心節(jié)點能耗是瓶頸仿真時要把它的初始能量設(shè)高一些否則跑不了幾輪就死了。2.4 分簇拓?fù)渥钯N近實際協(xié)議也最考驗參數(shù)分簇拓?fù)涫?LEACH 這類協(xié)議的標(biāo)配。常見做法是用 K-means 把節(jié)點聚成若干簇每簇選一個簇頭。它的難點不在生成而在簇頭數(shù)量和簇內(nèi)距離的平衡。% 分簇拓?fù)湎入S機(jī)撒點再用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); % 每簇選離質(zhì)心最近的節(jié)點作為簇頭 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參數(shù)設(shè) 5 是為了避免陷入局部最優(yōu)默認(rèn)只跑一次結(jié)果可能很差。簇頭選離質(zhì)心最近的節(jié)點而不是直接用語質(zhì)心因為質(zhì)心位置可能沒有實際節(jié)點。參數(shù)上K 一般取 sqrt(N) 左右N100 時 K5 到 10 比較合理K 太大簇內(nèi)通信省了但簇頭開銷上去了。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_clustersK, n_init10, random_state42).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], axis1) heads.append(members[np.argmin(d)]) heads np.array(heads)參數(shù)說明n_init10對應(yīng) Matlab 的Replicates顯式指定比默認(rèn)值更穩(wěn)。random_state42保證可復(fù)現(xiàn)。簇頭數(shù)量 K 的選擇可以用肘部法但仿真里一般直接按經(jīng)驗給。3. 用 Matlab 和 Python 跑通拓?fù)渖傻淖钚」こ?.1 統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)節(jié)點表加鄰接矩陣五種拓?fù)渖煞绞讲煌敵鲆y(tǒng)一否則后續(xù)仿真沒法復(fù)用。我一般會定義兩個核心結(jié)構(gòu)節(jié)點表nodesN×2 的坐標(biāo)矩陣和鄰接矩陣adjN×N 的 0/1 矩陣。鄰接矩陣的生成依賴通信半徑不是拓?fù)浔旧頉Q定的但要在生成拓?fù)浜罅⒖趟愠鰜怼? 統(tǒng)一輸出nodes坐標(biāo) 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, N1); 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)同時排除自身和超距節(jié)點。參數(shù)上R 的取值直接決定平均度R 太小拓?fù)鋾槌啥鄠€連通分量R 太大就退化成全連接。一般讓平均度落在 6 到 10 之間比較合理可以用mean(sum(adj))檢查。Python 版本from scipy.spatial.distance import cdist def build_topology(kind, N, L, R, seed42): 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, endpointFalse) 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, endpointFalse) 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參數(shù)說明cdist比手寫雙層循環(huán)快很多N500 時差距明顯。endpointFalse在環(huán)形和星型里都要加否則首尾節(jié)點會重疊。[:N]是網(wǎng)格拓?fù)涞慕財嘁驗閟ide2可能大于 N。3.2 可視化一張圖看出拓?fù)涫欠窈侠砩赏晖負(fù)涞谝患率钱媹D檢查。我一般會畫三張子圖節(jié)點分布、鄰接關(guān)系、度分布直方圖。度分布能快速暴露問題比如隨機(jī)拓?fù)涑霈F(xiàn)大量度為 0 的孤立節(jié)點或者網(wǎng)格拓?fù)涠确植歼^于集中。% 可視化節(jié)點 邊 度分布 figure; subplot(1,3,1); scatter(nodes(:,1), nodes(:,2), 20, filled); axis equal; title(節(jié)點分布); subplot(1,3,2); gplot(adj, nodes, -); hold on; scatter(nodes(:,1), nodes(:,2), 10, filled); axis equal; title(鄰接關(guān)系); subplot(1,3,3); histogram(sum(adj), 0:max(sum(adj))); title(度分布);邏輯說明gplot是 Matlab 畫鄰接圖的老牌函數(shù)輸入鄰接矩陣和坐標(biāo)即可。sum(adj)按列求和得到每個節(jié)點的度。度分布直方圖的 bin 用0:max保證每個整數(shù)度一個柱子不然默認(rèn)分箱會糊在一起。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_size20, with_labelsFalse)參數(shù)說明from_numpy_array把鄰接矩陣轉(zhuǎn)成圖對象注意它默認(rèn)生成無向圖如果鄰接矩陣不對稱要先處理。pos用真實坐標(biāo)而不是布局算法這樣拓?fù)涞目臻g結(jié)構(gòu)才看得出來。3.3 批量生成與保存一次跑完五種拓?fù)渥鰧Ρ葘嶒灂r五種拓?fù)湟芘可刹⒈4娉山y(tǒng)一格式。我一般存成.mat或.npz包含nodes、adj、type三個字段。% 批量生成五種拓?fù)洳⒈4?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拼文件名避免手動改。參數(shù)上R25 在 L100 的區(qū)域里平均度大概在 8 左右是個比較穩(wěn)的起點。如果跑出來度分布太偏先調(diào) R 再調(diào) N。Python 對應(yīng)for kind in [random, grid, ring, star, cluster]: nodes, adj build_topology(kind, 100, 100, 25) np.savez(ftopo_{kind}.npz, nodesnodes, adjadj)參數(shù)說明np.savez不壓縮np.savez_compressed壓縮但慢數(shù)據(jù)量小的時候無所謂。文件名帶類型后續(xù)加載時不用記。4. 拓?fù)渖衫镒钊菀撞鹊奈鍌€坑4.1 隨機(jī)種子沒固定實驗沒法復(fù)現(xiàn)現(xiàn)象同一份代碼跑兩次連通性曲線差很多審稿人問為什么結(jié)果不一致。原因rand或np.random默認(rèn)用系統(tǒng)時間做種子。解決在生成拓?fù)淝帮@式設(shè)種子Matlab 用rng(42)Python 用default_rng(42)。注意rng要放在所有隨機(jī)操作之前中途設(shè)沒用。4.2 網(wǎng)格拓?fù)涞拈g距小于通信半徑的 1/√2現(xiàn)象網(wǎng)格拓?fù)渑艹鰜磉B通分量不止一個對角節(jié)點連不上。原因網(wǎng)格間距 d 太大對角距離 d√2 超過通信半徑 R。解決保證 d ≤ R/√2或者把鄰接矩陣的生成改成考慮對角連接。我一般直接讓 d R/√2 再留 10% 余量。4.3 環(huán)形拓?fù)涫孜补?jié)點重疊現(xiàn)象環(huán)形拓?fù)洚嫵鰜砩僖粋€節(jié)點或者第一個和最后一個節(jié)點坐標(biāo)一樣。原因linspace(0, 2*pi, N)包含終點首尾重合。解決用linspace(0, 2*pi, N1)再去掉最后一個或者 Python 里用endpointFalse。這個坑在 Matlab 和 Python 之間移植時特別常見。4.4 分簇拓?fù)涞拇仡^選在空位置現(xiàn)象簇頭坐標(biāo)不在任何實際節(jié)點上后續(xù)仿真找不到對應(yīng)節(jié)點。原因直接用語質(zhì)心當(dāng)簇頭。解決選簇內(nèi)離質(zhì)心最近的實際節(jié)點代碼里就是argmin那一步。另外 K-means 的Replicates或n_init要設(shè)大一點不然簇劃分本身就不穩(wěn)。4.5 鄰接矩陣把自身算成鄰居現(xiàn)象度分布里每個節(jié)點度都至少是 1孤立節(jié)點檢測不出來。原因距離矩陣對角線為 0D R把自身也算進(jìn)去了。解決加D 0條件或者生成后把對角線置零。這個坑在隨機(jī)拓?fù)淅镒铍[蔽因為節(jié)點密集時看不出來。5. 從拓?fù)涞椒抡嬉粋€可驗證的連通性檢查技巧生成完拓?fù)鋭e急著跑路由協(xié)議先做連通性檢查。這一步能省掉后面大量無效實驗。我一般用廣度優(yōu)先搜索BFS從第一個節(jié)點出發(fā)看能覆蓋多少節(jié)點覆蓋率低于 90% 就說明拓?fù)溆袉栴}先調(diào) R 或 N。% 連通性檢查BFS從節(jié)點1出發(fā) 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用數(shù)組模擬N 不大時夠用N 上千就換真正的隊列結(jié)構(gòu)。參數(shù)上連通性比率低于 0.9 就要警惕低于 0.7 基本沒法用。Python 版本用 collections.dequefrom collections import deque def connectivity_ratio(adj): N adj.shape[0] visited np.zeros(N, dtypebool) 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參數(shù)說明deque的popleft是 O(1)比 list 的pop(0)快。np.where返回索引數(shù)組直接迭代。這個檢查跑一次只要幾毫秒但能擋掉大部分低級錯誤。五種拓?fù)淅镫S機(jī)拓?fù)涞倪B通性最不穩(wěn)定網(wǎng)格和環(huán)形最穩(wěn)星型取決于中心節(jié)點是否在通信半徑內(nèi)覆蓋所有葉子分簇拓?fù)湟创仡^之間是否連通。我一般會把這五種拓?fù)涞倪B通性比率打出來對比作為實驗的第一張表。最后一個習(xí)慣每次生成拓?fù)浜蟀裯odes、adj、R、N、L和隨機(jī)種子一起存進(jìn)日志文件。后面結(jié)果不對時能直接定位是哪次生成的拓?fù)溆袉栴}不用重新跑一遍。這個習(xí)慣幫我省過很多次返工希望幫到你。本文還有配套的精品資源點擊獲取