news 2026/10/29 15:07:23

D2D信道仿真MATLAB实践:资源分配与SINR评估

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
D2D信道仿真MATLAB实践:资源分配与SINR评估

简介:面向无线通信课程设计、毕业设计与D2D研究入门者,这份MATLAB仿真资料聚焦设备直连通信的信道建模与资源分配问题,适合用来复现频谱复用、干扰协调和启发式优化等关键环节。压缩包共22个文件,以20个.m脚本为主,另有2个.asv备份文件,整体约16KB,体积轻量却覆盖主程序、路径损耗与干扰计算、用户及D2D对生成、随机/启发式/最优分配等模块。已有1214人学习,说明其在D2D仿真练习中具有一定参考价值。读者可据此搭建D2D与传统用户共存的系统模型,比较不同资源分配策略在吞吐量、覆盖率、误码率等指标上的表现,并理解空间复用、功率控制与干扰管理的基本思路,为后续算法改进和性能评估提供可运行的代码骨架。

1. D2D 信道仿真跑偏的那三天:先搞清楚这套 MATLAB 工程在算什么

去年帮一个做 D2D 的团队看仿真,CDF 曲线怎么调都比论文里的低 6 dB 左右,改功率、改门限、换算法,全都无效。最后查到根因:阴影衰落每次 drop 都重新生成了一遍,本该在一次 drop 内固定的慢衰落,被当成了快衰落叠加。这类玄学问题在 D2D 信道仿真里特别多,因为整条链路要经过撒点、路径损耗、阴影、小尺度衰落、SINR 聚合、资源分配、速率统计七个环节,任何一环的随机变量用法错了,结果都不会报错,只会安静地偏一点。

这套 MATLAB 工程干的就是把这条链路完整复现出来:单小区内布若干个蜂窝用户和若干对 D2D 用户,前者走基站,后者直通,共用同一批资源块,然后对比随机分配、图着色复用、带功控的复用三种策略下的 SINR 分布和吞吐量。适合两类人:一类是刚进实验室、需要一份能直接跑通的 D2D 资源分配仿真底稿的同学;另一类是已经在写自己的算法,但缺一个干净的评估框架做对照组的开发者。下面按我拆代码的顺序讲,先骨架,再信道,再分配,最后落坑和改造。

2. 工程骨架与参数入口:一次 drop 从撒点到出图的完整链路

拿到一个仿真工程,我第一件事不是看算法文件,而是找主入口,顺着它把调用顺序走一遍。D2D 类仿真最容易出问题的地方不在算法本身,而在调用顺序——信道增益是在撒点之前算的还是之后算的、阴影衰落是在 drop 循环内还是外生成的,这类顺序错了,代码照样能跑完,只是结论不可信。这一章先把文件职责和调用链路捋直。

2.1 目录结构与文件职责划分

这个工程按「配置 / 核心 / 算法 / 评估 / 工具」五层切分,好处是换算法的时候只动 alloc 目录,不动核心链路。常见做法是这样:

路径职责改动频率
run_main.m主入口,串起 drop 循环、调用分配算法、汇总统计低
conf/d2d_config.m所有仿真参数的唯一来源每次实验都改
core/gen_topology.m生成小区内 CU 与 D2D 对的坐标低
core/pathloss_dB.m路径损耗 + 阴影衰落,返回标量或向量低
core/build_channel.m组装大尺度增益矩阵 G低
core/compute_sinr.m给定拓扑与分配,算每条链路的 SINR中
alloc/alloc_random.m随机 RB 分配基线低
alloc/alloc_graph_coloring.m冲突图 + 贪心着色复用中
alloc/power_control.m目标 SINR 的开环功率控制中
eval/eval_throughput.mSINR 到链路速率、系统吞吐低
eval/plot_cdf.m累积分布曲线绘制低

这套划分的关键在于:core目录里不允许出现任何「策略」相关的分支判断。也就是说,信道怎么算和资源怎么分是彻底解耦的,换分配算法不需要动信道代码,反过来调信道参数也不会影响算法逻辑。我见过不少工程把功率控制直接写在 SINR 计算函数里,结果想对比「有无线功控」两种场景时,得复制一份函数出来改,后面两边代码逐渐分叉,血泪经验。

2.2 一次 drop 的调用顺序

主循环的结构决定了随机变量的生命周期。所谓一次 drop,指的是「重新撒一次用户位置」这一个快照,在快照内部,用户坐标、阴影衰落都是固定的,只有小尺度衰落可以在不同传输块之间变化。如果做的是长期统计(比如每秒调度一次),通常在一个 drop 内还会再套一层传输时间间隔 TTI 的循环。

% run_main.m —— 主循环骨架 cfg = d2d_config(); % 1. 读参数 rng(cfg.seed); % 2. 固定随机种子,保证可复现 result = struct('sinr', [], 'rate', []); for drop = 1:cfg.numDrop % 3. 撒点:一次 drop 内坐标固定 topo = gen_topology(cfg); % 4. 大尺度增益:路径损耗 + 阴影,一次 drop 只算一次 G = build_channel(topo, cfg); % 5. 分配资源(可换成任意算法) alloc = alloc_graph_coloring(cfg, topo, G); % 6. 可选的功率控制 pwr = power_control(cfg, G, alloc); % 7. 算 SINR 与速率 sinr = compute_sinr(cfg, topo, G, alloc, pwr); rate = eval_throughput(sinr, cfg); % 8. 累加统计量 result.sinr = [result.sinr; sinr]; result.rate = [result.rate; rate]; end plot_cdf(result); % 9. 统一出图

这段骨架里有两个容易被改坏的地方。第一是第 4 步的位置:build_channel必须在 drop 循环内部、但在分配算法之前,因为分配算法需要用到 G 来判断复用冲突;如果你把它挪到循环外面,所有 drop 共用一个信道,统计出来的曲线会异常平滑,看不出随机性。第二是第 7 步的入参顺序,compute_sinr同时吃拓扑和分配结果,意味着 SINR 完全由这两者决定,这个函数本身不做任何随机抽样——一旦你把randn写进 SINR 函数里,就是把自己送进了黑匣子,同一个配置跑两次结果不一样,排查起来非常难受。

2.3 参数集中管理:为什么别在脚本里写魔法数字

参数散落是仿真工程的头号腐蚀剂。今天在脚本里写个250表示小区半径,明天在另一个函数里写250表示最大距离,半个月后想改成 500,你只会记得改一处。所以配置函数要写成返回结构体的形式,所有下游函数只读不写。

function cfg = d2d_config() %D2D_CONFIG 仿真全局参数集中定义,下游函数只读不改 cfg.numCell = 1; % 小区数,先单小区跑通再扩 cfg.cellRadius = 250; % 小区半径 m cfg.numCU = 20; % 蜂窝用户数 cfg.numD2D = 40; % D2D 对数 cfg.d2dRadius = 25; % D2D 对之间的最大距离 m cfg.numRB = 20; % 资源块个数 cfg.rbBandwidth = 180e3; % 单个 RB 带宽 Hz cfg.txPowerCU = 23; % CU 发射功率 dBm cfg.txPowerD2D = 23; % D2D 发射功率 dBm cfg.noisePSD = -174; % 噪声功率谱密度 dBm/Hz cfg.noiseFigure = 9; % 接收机噪声系数 dB cfg.fc = 2e9; % 载频 2 GHz cfg.pathlossExp = 3.76; % 路径损耗指数 cfg.shadowStd = 8; % 阴影衰落标准差 dB cfg.sinrThresh = 5; % 复用冲突判定的 SINR 门限 dB cfg.numDrop = 1000; % 蒙特卡洛 drop 次数 cfg.seed = 20240101; % 随机种子 end

参数本身没什么稀奇的,关键是三个派生量要在配置阶段就算好,避免下游重复计算或者单位混乱:噪声功率、发射功率的线性值、以及单 RB 的噪声底。噪声功率的算法是noisePSD + 10*log10(rbBandwidth) + noiseFigure,按上面的取值算出来约 -112.5 dBm;功率从 dBm 转线性毫瓦用10^(P/10)。这两个转换如果放在 SINR 函数里做,很容易出现「分子是 dBm、分母是 mW」的翻车现场,算出来的 SINR 看着像模像样,实际差了十个数量级。

3. 信道建模拆解:路径损耗、阴影衰落与小尺度衰落的叠加顺序

信道这一层是整个仿真的地基,分配算法做得再漂亮,G 矩阵错了就是白搭。这里要讲清楚三件事:大尺度增益怎么组装成矩阵、小尺度衰落该在哪一层引入、以及 SINR 到速率的那一步用什么映射。顺序搞错是常态,我见过把阴影衰落加在 SINR 上的写法,等于每条链路只承受一次随机扰动,和真实的多链路干扰叠加完全不是一回事。

3.1 大尺度增益矩阵 G 的构建

先把链路角色定义清楚。一个单小区场景里,CU 的发射端是用户本身、接收端是基站;D2D 的发射端和接收端都是用户。为了让后续 SINR 计算能向量化,统一把发射端排成一个 N×2 的矩阵,前numCU行是 CU,后numD2D行是 D2D 发射端;接收端同样排成 N×2,前numCU行全部填基站坐标,后面对应各自的 D2D 接收端。N 就是总链路数。

G 是一个 N×N 的矩阵,G(i,j)表示第 j 个发射端到第 i 个接收端的大尺度增益(线性值,不是 dB)。对角元G(i,i)是有效信号,非对角元是潜在干扰。这一步必须用矩阵运算,写双重循环也能跑,但 60 条链路的规模下,1000 次 drop 的双重循环在 MATLAB 里会明显拖慢,后面讲提速的时候还会回到这一点。

function G = build_channel(topo, cfg) %BUILD_CHANNEL 组装大尺度增益矩阵(路径损耗 + 阴影衰落) N = cfg.numCU + cfg.numD2D; % 发射端与接收端坐标:CU 的接收端是基站,放在原点 tx = [topo.cuPos; topo.d2dTx]; % N x 2 rx = [repmat(topo.bsPos, cfg.numCU, 1); topo.d2dRx]; % 两两距离矩阵,diag 上就是链路自身距离 diffX = tx(:,1).' - rx(:,1); % N x N diffY = tx(:,2).' - rx(:,2); D = sqrt(diffX.^2 + diffY.^2); % D(i,j) = j 到 i 的距离 % 路径损耗 + 阴影:一次生成 N x N 的阴影矩阵,drop 内固定 PL = pathloss_dB(D, cfg); G = 10.^(-PL / 10); % dB 转线性 % 自身链路加一个很小的保护,避免 D 接近 0 时增益爆掉 G(1:N+1:end) = min(G(1:N+1:end), 1e3); end

D的计算用的是广播式的行列相减,diffX(:,1).'是 1×N 的行向量,减去 N×1 的列向量,自动扩展成 N×N,省掉了meshgrid和双重循环。G(1:N+1:end)是 MATLAB 提取对角元的写法,步长 N+1,这一点如果写成G(1:N,1:N)就变成了取左上角的子块,是完全不同的东西,新手很容易在这里静默出错。

pathloss_dB这一层负责把距离转成损耗,同时把阴影衰落加进去:

function pl = pathloss_dB(d, cfg) %PATHLOSS_DB 3GPP 风格的简化路损模型,d 单位 m d = max(d, 1); % 距离保护,避免 log10(0) fcGHz = cfg.fc / 1e9; pl = 32.4 + 20*log10(fcGHz) ... % 常数项与频段项 + 10*cfg.pathlossExp*log10(d) ... % 距离项 + cfg.shadowStd * randn(size(d)); % 阴影衰落,对数正态 end

常数 32.4 与 20log10(f) 是常见的城市微小区路损形式,距离项指数 3.76 对应视距概率较高的场景,如果要做 NLOS 为主的场景,把指数提到 4 以上并给阴影标准差加到 10 dB 左右。注意randn(size(d))的写法:它保证阴影矩阵和距离矩阵同尺寸,如果写成randn(1),那就成了所有链路共用一个阴影值,干扰结构会完全失真,这是我在一个开源工程里真实见过的写法。

3.2 小尺度衰落的生成与快照独立性

大尺度增益在一次 drop 内是不变的,但小尺度衰落( Rayleigh 或 Rician )应当在每个传输块上重新生成。很多论文级仿真为了简化,直接用大尺度 SINR 做统计,这在对比算法相对性能时问题不大,但如果要报绝对吞吐量,误差会很明显。

常见做法是给每条链路生成复高斯变量,取模平方得到功率增益,然后乘到 G 上:

function g = small_scale_fading(N, cfg) %SMALL_SCALE_FADING 瑞利衰落功率增益,逐传输块独立生成 % 每条链路一个复高斯样本,|h|^2 服从指数分布,均值为 1 h = (randn(N,1) + 1i*randn(N,1)) / sqrt(2); g = abs(h).^2; end

这里要注意的是小尺度衰落要不要做成 N×N 的矩阵。严格来说,干扰链路上也有各自的小尺度衰落,如果只给有效链路加、干扰链路不加,等于假设干扰是平滑的,会高估 SINR。工程上常见的折中是:有效链路用小尺度衰落,干扰用大尺度期望,然后在文档里写清楚这个假设。做对比实验时,两种设置必须统一,否则你和别人的曲线没法放一起看。

衰落模型的参数也要说清楚:/sqrt(2)是为了让复高斯的实部虚部方差各为 1/2,模平方的期望才是 1。如果忘了归一化,功率增益的均值会变成 2,等于凭空给所有链路加了 3 dB 增益,这种错误在只看相对趋势的图里根本看不出来。

3.3 SINR 与链路速率的映射

SINR 的计算逻辑只有一句话:同 RB 上的其它链路构成干扰,不同 RB 上的链路互不影响。这句话翻译成代码,就是一个按 RB 分组的求和。

function sinr_dB = compute_sinr(cfg, topo, G, alloc, pwr) %COMPUTE_SINR 按资源分配结果计算每条链路的 SINR N = cfg.numCU + cfg.numD2D; sinr_lin = zeros(N,1); for i = 1:N rb = alloc.rbOfLink(i); % 同一 RB 上的其它链路才是干扰源 interferer = find(alloc.rbOfLink == rb); interferer(interferer == i) = []; % 关键:剔除自己 % 干扰功率求和,pwr 为线性毫瓦 I = sum(pwr(interferer) .* G(i, interferer).'); S = pwr(i) * G(i, i); sinr_lin(i) = S / (I + cfg.noisePower_mW); end sinr_dB = 10*log10(sinr_lin); end

interferer(interferer == i) = []这一行是全篇最容易被漏掉的一句。漏了它,链路自己会被算进干扰源里,SINR 被自己压低,而且 CU 数量越少、功率越大,压低越明显——表现出来就是「加功率反而性能变差」这种反直觉现象。我在避坑章节里还会展开讲。

速率映射用香农公式,注意带宽单位:

function rate = eval_throughput(sinr_dB, cfg) %EVAL_THROUGHPUT SINR(dB) 映射为单链路速率(Mbps) sinr_lin = 10.^(sinr_dB / 10); rate = cfg.rbBandwidth * log2(1 + sinr_lin) / 1e6; % bps -> Mbps end

log2(1+SINR)里的 SINR 必须是线性值,传 dB 进去会得到一个荒谬的常数。这个坑之所以常见,是因为 dB 和线性在 MATLAB 里都是 double,类型系统完全帮不上忙,只能靠命名规范:我在工程里强制所有带_dB后缀的变量是 dB,不带的是线性,函数入参也照这个来,靠命名而不是注释来防错。

4. 资源分配算法实战:随机基线、图着色复用与功率控制

算法这一层是大多数人真正关心的部分,但我要先泼一盆冷水:如果基线没跑通,任何花哨算法的增益都不可信。这套工程里,随机分配不是陪衬,它是验证评估框架正确性的标尺——随机分配下 D2D 复用带来的干扰应该是最严重的,吞吐曲线应该最低、SINR 分布的左尾应该最长,如果你的图着色算法跑出来比随机还差,那基本可以确定是框架问题而不是算法问题。

4.1 随机分配基线:先证明你的评估框架没写错

随机基线的逻辑要足够笨,笨到没有出错的空间:CU 各自占一个互不相同的 RB,D2D 对在全部 RB 里随机挑一个复用。CU 之间不共享 RB,这样可以先把 CU 之间的干扰排除掉,让基线结果只反映 D2D 复用带来的干扰。

function alloc = alloc_random(cfg, topo) %ALLOC_RANDOM 基线:CU 独占 RB,D2D 随机复用 N = cfg.numCU + cfg.numD2D; rb = zeros(N,1); % CU 之间不共享 RB,用 randperm 保证不重复 rb(1:cfg.numCU) = randperm(cfg.numRB, cfg.numCU).'; % D2D 随机挑选 RB,允许与 CU 及其他 D2D 冲突 rb(cfg.numCU+1:end) = randi(cfg.numRB, cfg.numD2D, 1); alloc.rbOfLink = rb; end

randperm(n, k)返回 k 个不重复的整数,正好对应「CU 独占资源」这个约束,前提是numRB >= numCU。如果你的场景里 CU 比 RB 多,这句会直接报错,这其实是个好事——它逼你在配置阶段就想清楚「用户数不能超过资源数」这个物理约束。相比之下,用randi生成 CU 的 RB 再手动去重,写出来的代码既长又容易有边界问题。

基线跑完之后,先别急着看算法,先看三条 sanity check:所有 SINR 里最差的那条是不是接近 0 dB 甚至负值;D2D 的平均 SINR 是不是明显低于 CU(因为 D2D 有跨层干扰);系统总吞吐是不是随 D2D 对数增加先升后降。这三条符合直觉,才说明框架是对的。

4.2 冲突图构建与贪心着色复用

图着色复用的思路很直接:如果两条链路复用同一个 RB 会让任何一方的 SINR 掉到门限以下,就给它们在冲突图里连一条边,然后用贪心着色给相邻节点分配不同的颜色,颜色编号映射到 RB 编号。这样得到的分配天然满足「强干扰链路不共享资源」。

function alloc = alloc_graph_coloring(cfg, topo, G, pwr) %ALLOC_GRAPH_COLORING 冲突图 + 贪心着色分配 RB N = cfg.numCU + cfg.numD2D; adj = false(N); % 1. 建冲突图:假设 i、j 复用,检查双方 SINR 是否都达标 for i = 1:N for j = i+1:N s_i = pwr(i)*G(i,i) / (pwr(j)*G(i,j) + cfg.noisePower_mW); s_j = pwr(j)*G(j,j) / (pwr(i)*G(j,i) + cfg.noisePower_mW); if 10*log10(min(s_i, s_j)) < cfg.sinrThresh adj(i,j) = true; adj(j,i) = true; end end end % 2. 贪心着色:按度数从大到小,依次选最小可用颜色 deg = sum(adj, 2); [~, order] = sort(deg, 'descend'); color = zeros(N,1); for k = 1:N i = order(k); used = color(adj(i,:) & color > 0); % 邻居已用的颜色 c = 1; while any(used == c) c = c + 1; end color(i) = c; end % 3. 颜色映射到 RB,超出 RB 数时循环复用 alloc.rbOfLink = mod(color - 1, cfg.numRB) + 1; end

第一步建图是 O(N²) 的双重循环,N=60 时是 1800 次判断,单次 drop 可以接受;如果 D2D 对数扩到几百,这里就要改成只检查「距离小于干扰门限」的候选对,否则会明显拖慢。

第二步的排序很关键。贪心着色对节点处理顺序敏感,按度数降序排是经典的 Welsh-Powell 思路,先把冲突最多的节点安排掉,后续节点的可用颜色更宽松。如果不排序,直接按链路编号走,得到的颜色数可能多出一截,颜色数直接对应需要的 RB 数,颜色越多意味着复用度越低、频谱效率越差。

第三步的mod映射是这套实现里的一个妥协:如果着色出来的颜色数超过可用 RB 数,就只能循环复用,而循环复用意味着两个本该冲突的链路可能被分到同一个 RB。这是一个真实的边界问题,处理方式我放在避坑章节里讲。

4.3 功率控制与 SINR 保护

功率控制的目标很简单:在满足目标 SINR 的前提下,把发射功率压到最低,减少对邻链路的干扰。开环功控的实现是解一个一元方程——给定干扰加噪声,反解出达到目标 SINR 所需的发射功率。

function pwr = power_control(cfg, G, alloc) %POWER_CONTROL 目标 SINR 开环功控,返回线性功率(mW) N = cfg.numCU + cfg.numD2D; pwr = cfg.txPowerAll; % 从最大功率起算 for i = 1:N rb = alloc.rbOfLink(i); interferer = find(alloc.rbOfLink == rb); interferer(interferer == i) = []; % 用当前功率估计干扰水平 I = sum(pwr(interferer) .* G(i, interferer).'); target = 10^(cfg.sinrTarget/10); % 线性目标 SINR % 反解所需功率,并夹在 [pmin, pmax] 之间 need = target * (I + cfg.noisePower_mW) / G(i,i); pwr(i) = min(max(need, cfg.pwrMin), cfg.txPowerAll(i)); end end

这段代码有一个隐含的迭代问题:每个链路算干扰时用的是「当前」的功率向量,而功率向量本身在循环里被不断更新,所以先算的链路和后算的链路看到的干扰不一样。严格的做法是迭代到收敛(通常三到五轮就够),工程上为了省时间可以只跑一轮,但要在文档里注明这是「单轮近似」。我一般会在外面包一层三到五轮的循环,观察功率向量的变化幅度小于 0.1 dB 就停,这个习惯在写论文的对比实验时很重要,不然别人复现不出你的数。

还有一个细节:cfg.pwrMin不能设得太小。功率压到极低时,SINR 会掉到门限以下,链路直接中断,吞吐统计里就会出现一堆接近零的点,把 CDF 的左尾拖得很长。这个下界通常取最大功率往下 20 dB 左右,具体看场景。

4.4 蒙特卡洛统计与 CDF 出图

统计这一层要做的事只有两件:把所有 drop 的 SINR 拼起来画 CDF,以及按不同算法分开画对比曲线。CDF 的写法不用调ecdf,自己排序插值更可控,也方便加图例。

function plot_cdf(sinrAll, cfg) %PLOT_CDF 多算法 SINR 的累积分布对比 figure; hold on; grid on; names = fieldnames(sinrAll); for k = 1:numel(names) x = sort(sinrAll.(names{k})); y = (1:numel(x)).' / numel(x); plot(x, y, 'LineWidth', 1.5, 'DisplayName', names{k}); end xlabel('SINR (dB)'); ylabel('CDF'); legend('Location','southeast'); end

y = (1:n)/n这个经验分布的定义决定了横轴到纵轴的映射,注意不要写成(0:n-1)/n,两者在样本量大的时候差异可以忽略,但在只有几百个样本的小规模实验里会让曲线整体偏移一格,看图的人会以为是算法差异。出图的时候我习惯把横轴截到 -10 到 30 dB,超出范围的尾部对结论没帮助,反而压缩了中间段的区分度。

5. 避坑与排查:仿真结果对不上时先看这几个地方

前面几章讲的是正常路径,这一章讲的是我实际踩过、也帮别人定位过的坑。D2D 仿真的特点是错误不报异常,只体现在曲线上,所以排查思路要从「代码能不能跑」转成「结果合不合理」。下面五条按出现频率排序,每条都给出现象、原因和解决方式。

5.1 阴影衰落每次 drop 重新生成,曲线整体偏低

现象:不同算法之间的相对关系正常,但所有曲线的绝对 SINR 都比预期低 5 到 8 dB,换参数也调不回来。

原因:阴影衰落本来应该在一次 drop 内固定,但代码里把它写进了按 RB 或者按 TTI 的循环里,导致同一链路在一次快照内被反复叠加随机扰动。对数正态变量的叠加不是简单的加减,反复抽样会让等效的标准差变大,深衰落样本变多,左尾被拉长。

解决:把阴影矩阵的生成提到 drop 循环内、RB 循环外,也就是build_channel里一次性算好 N×N 的阴影,后面所有传输块复用。判断方法很简单,在build_channel里打一行fprintf输出阴影矩阵的均值,跑两次 drop 看是不是同一个值,如果每次都变,说明位置放错了。

5.2 干扰求和把自身链路算进去,加功率反而变差

现象:把 D2D 发射功率从 23 dBm 提到 26 dBm,系统吞吐不升反降,SINR 分布的左尾更长了。

原因:find(alloc.rbOfLink == rb)找到的是同 RB 上的全部链路,包含了自己。求和的时候没有剔除,自身的信号被当成干扰加进了分母,于是功率越大、自干扰越大,SINR 反而下降。这个错误在只有一两条链路时几乎看不出来,因为自干扰项相对噪声不算大,链路一多就暴露了。

解决:在find之后紧跟一句剔除,就是 3.3 节里那个interferer(interferer == i) = []。排查技巧是把噪声设成极小值(比如 -200 dBm),然后看单链路独占 RB 时的 SINR,理论上应该接近无穷大,如果算出来是个有限值,那分母里一定混进了不该有的东西。

5.3 距离为 0 导致路径损耗出现 Inf,后面全是 NaN

现象:跑完 1000 次 drop,统计结果里有一堆 NaN,plot_cdf画出来是一条断掉的线,或者横轴范围异常。

原因:log10(0)返回-Inf,路径损耗变成-Inf之后,线性增益10^(-PL/10)变成Inf,再参与后续的乘除就污染成 NaN。触发条件是某个接收端和发射端坐标完全重合,或者距离小到浮点精度以下,D2D 对随机撒点时如果接收端半径设为 0 就会必然出现。

解决:在pathloss_dB入口加距离下限保护d = max(d, 1),并且给自身链路增益加一个上限。更彻底的做法是在gen_topology里给 D2D 对的最小间距设一个下限(比如 3 米),撒点后检查距离,不满足就重撒。这条我建议当成硬性规范,因为 NaN 会沿着矩阵传染,最后可能几百行代码之外才报错,排查成本极高。

5.4 贪心着色颜色数超过 RB 数,复用度被悄悄拉低

现象:图着色算法的性能比随机基线好,但提升幅度远小于文献里的数,而且随着 D2D 对数增加,性能迅速恶化。

原因:冲突图建得太密,贪心着色出来的颜色数超过numRB,mod映射之后,本来不相邻的链路被强行塞进同一个 RB,冲突重新出现。更隐蔽的是,这个过程的性能下降是渐进的,不会报错,只会让你觉得「算法就这样」。

解决:在alloc_graph_coloring末尾加一句断言,颜色数大于 RB 数时给警告,然后看是门限设得太严还是 D2D 对数太多。门限从 5 dB 放宽到 0 dB,颜色数通常能降三分之一。如果确实需要高密度复用,就得回到算法层面,用分数规划或者基于 SINR 的迭代分配替代纯图着色,而不是靠mod硬塞。

5.5 随机种子和并行循环,复现性被破坏

现象:同一份代码、同一套参数,今天跑出来的曲线和昨天不一样,差异不大但肉眼可见。

原因:一是没固定rng,每次启动 MATLAB 的初始种子都不同;二是在用parfor加速 drop 循环时,每个 worker 的随机流是独立的,且默认不保证可复现。单机串行跑的时候,固定一次种子就能复现,一旦并行就失效。

解决:串行时在run_main开头固定种子;并行时用parfor配合按 drop 编号派生的子种子,比如rng(cfg.seed + drop),让每个 drop 的随机流只依赖 drop 编号,与执行顺序无关。这样即使中途中断重跑,结果也是确定的。我一般还会把每次实验的cfg结构体存成mat文件,跟图一起归档,半年后回头查某个曲线是怎么来的,不至于抓瞎。

6. 二次开发:换算法、加场景与提速的几条实操路径

这套工程的改造成本主要看你动哪一层。如果只是换分配算法,最省事的方式是遵守统一接口:输入(cfg, topo, G, pwr),输出一个带rbOfLink字段的结构体,然后把文件放进alloc目录,在run_main里换一行函数名就行。接口统一之后,你甚至可以写个循环把三种算法跑一遍,把结果塞进同一个sinrAll结构体交给plot_cdf,一次出对比图。我建议新算法先用随机基线跑同一份topo和G,把两边的分配结果 diff 一下,确认差异只出现在 RB 编号上,而不是链路的某个字段,这样可以避免因为结构体字段名不一致导致的静默错位。

场景扩展方面,单小区扩多小区时最需要注意的是小区间干扰的引入。现在的 G 矩阵只算了本小区内所有链路的两两增益,扩成多小区后要把邻小区基站的发射也加进干扰源,同时 CU 的接收端也要区分归属基站。具体做法是在build_channel里把发射端矩阵从「本小区 N 条」扩成「小区数 × N 条」,然后让compute_sinr里的find从「同 RB」改成「同 RB 且跨小区全部计入」。这一步会显著增加矩阵维度,60 个用户扩到 7 小区就是 420 条链路,G 从 60×60 变成 420×420,内存没问题但循环会变慢。

提速这一块,血泪经验是先把双重循环干掉。build_channel里的距离矩阵已经向量化了,但compute_sinr里还有一层按链路的循环,可以改成用逻辑索引加累加的方式:先把alloc.rbOfLink展开成一个 N×N 的掩码,掩码为真的位置才计入干扰,然后用矩阵乘法一次算完所有链路的干扰功率。我在一个 200 用户的场景上做过对比,改成矩阵运算后单次 drop 从 0.4 秒降到 0.05 秒,1000 次 drop 从接近 7 分钟压到 1 分钟以内。另一个提速点是parfor替代for,但要注意前面说的种子问题,以及parfor里不能有跨迭代的变量累加,得改成在外层收集。

最后提一个验证习惯。每次改完算法,我都会先跑一个「闭眼测试」:把 D2D 对数设为 0,此时系统应该退化成纯蜂窝场景,吞吐量等于 CU 总速率,没有任何 D2D 干扰。这一步能一次性排除掉 G 矩阵对称性、RB 索引越界、干扰剔除这类基础错误。等这个退化场景通过了,再逐步把 D2D 对数加到 10、20、40,观察曲线是不是平滑过渡,中间如果出现非单调的跳变,基本就是某个约束在特定规模下失效了。从那以后我每次改信道或分配代码,都强制先跑一遍零 D2D 的退化测试再往下走,比对着论文调参数省事得多。希望这套骨架和这些坑,能帮你少花几个晚上在对不上的曲线上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 15:05:52

cpr 1.3.0实战:C++ HTTP请求封装、编译与排错指南

简介&#xff1a;cpr 1.3.0 预编译库面向需要在 C 项目中快速接入 HTTP 请求能力的开发者&#xff0c;尤其适合不想自行处理 curl 依赖与 VS 编译链、希望直接用于调试或发布构建的人群。压缩包共 27 个文件&#xff0c;包含 21 个头文件、4 个 lib 库和 2 个 dll 动态库&#…

作者头像 李华
网站建设 2026/10/29 15:05:05

快递e栈JavaWeb实战:Servlet+Filter+MVC取件闭环

简介&#xff1a;面向 JavaWeb 入门与进阶学习者的综合实战项目源码包&#xff0c;以「快递e栈」快递代收业务为背景&#xff0c;把 jQuery、Bootstrap、Layui、Ajax 等前端技术与 MySQL、Servlet、Filter、Listener、JSP 及 MVC 分层组织成完整链路&#xff0c;适合课程设计、…

作者头像 李华
网站建设 2026/10/29 15:04:36

superpowers不是工具,而是开发者效率能力组合的实用指南

我见过太多人把“superpowers”当成一个可以下载安装的软件包来搜&#xff0c;甚至有人会问“这个工具到底在哪个商店里”。说实话&#xff0c;我第一次看到这个词也愣了一下&#xff0c;但折腾过几轮之后我彻底想明白了&#xff1a;superpowers不是一个软件&#xff0c;不是某…

作者头像 李华
网站建设 2026/10/29 15:04:19

Multi-Agent系统核心技术解析与商业实践

1. Multi-Agent平台的核心竞争格局分析过去一年里&#xff0c;我亲眼见证了超过20个团队从零开始搭建Multi-Agent系统。这个领域的竞争正在从单纯的技术比拼&#xff0c;转向更复杂的生态位争夺。目前市场上的玩家主要分为三类&#xff1a;第一类是像AutoGPT这样的开源项目&…

作者头像 李华
网站建设 2026/10/29 15:04:11

Java与C++成员变量与方法修饰符顺序规范详解

1. 成员变量与成员方法修饰符顺序的重要性在面向对象编程中&#xff0c;成员变量和成员方法的修饰符顺序看似是个小问题&#xff0c;但实际上它影响着代码的可读性、一致性和维护性。就像我们整理衣柜时会把常穿的衣服放在容易拿到的地方一样&#xff0c;合理的修饰符顺序能让代…

作者头像 李华
网站建设 2026/10/29 15:02:25

系统架构设计师知识点集锦:从PDF到可检索知识库的整理与检索实践

简介&#xff1a;这份《2021-系统架构设计师知识点集锦》面向备考软考系统架构设计师的考生&#xff0c;尤其适合以自学方式推进复习、需要系统梳理考纲要点的人群。内容围绕系统架构设计核心知识展开&#xff0c;可用于搭建知识框架、定位薄弱环节与考前集中回顾。资源为单个P…

作者头像 李华