简介:围绕多小区下行链路NOMA系统的最优功率分配问题,这套MATLAB源代码给出完整仿真实现,适合通信工程、电子信息与数学等专业学生完成课程设计、期末大作业或毕业设计。代码以加权最小均方误差迭代算法为主线,包含信道生成、串行干扰消除解码条件检验、二分法求最优解、时分多址速率对比及收敛曲线绘制等模块;参数化编程便于调整系统参数,注释清晰,附带的案例数据可一键运行。整个压缩包共15个文件,其中14个为.m源文件、1个为说明文档,总大小约20KB,结构简明、易于迁移。已有159人参与学习,可作为理解NOMA功率分配与多小区干扰管理的实用参考。
1. 多小区 NOMA 的功率分配,先别急着跑仿真
刚接触 NOMA(非正交多址接入)的工程师,第一反应往往是去搜“最佳功率分配”的 matlab 源代码;可把代码下载下来直接跑的人,十个有七个对着“SIC 顺序不满足”“收敛曲线震荡”发愣。问题不在代码,而在功率分配和用户配对、小区间干扰本来就是耦合的:同一子信道上叠加多个用户,各用户分配到的功率必须差出一个量级,串行干扰消除才谈得上成立;而多小区同频组网又让每个用户的信干噪比里多出一项邻区干扰,推高本小区功率的同时也在污染隔壁小区。这篇博文以“多小区下行链路 NOMA 系统最佳功率分配”为主线,把系统模型、优化目标、可落地的求解方法以及 matlab 代码骨架串起来讲,适合正在做通信仿真、开题或者接手 NOMA 预研的工程师直接对照着改。
2. 多小区下行 NOMA 系统:从 SIC 序列到小区间干扰建模
2.1 功率域叠加编码与 SIC 解码顺序
NOMA 和 OMA 的本质区别在于,同一个子载波(子信道)上同时传输多个用户信号,靠功率差而不是频带/时隙来区分用户。下行场景里,发送端把多个用户的符号线性叠加为x = sqrt(p1)*s1 + sqrt(p2)*s2,其中s1、s2是归一化符号,p1、p2是两个用户分到的发射功率。信道增益弱的远端用户需要拿到更大的功率,它在解码时把自己的符号直接解出来,把另一个用户当作噪声;信道增益好的近端用户要先解出远端用户的符号、重构并减去它,再解自己的符号,这就是 SIC(串行干扰消除)。
接收端做 SIC 的顺序必须和功率分配同步决定,否则错误传播会把两个用户的解调全部带偏。对两用户子信道,假设|h1|^2 > |h2|^2,解调顺序是“用户 2 先解自己 → 用户 1 解用户 2 再解自己”。此时用户 2 的接收 SINR 是:
SINR2 = p2*|h2|^2 / ( p1*|h2|^2 + sigma^2 )
用户 1 的接收 SINR 是:
SINR1 = p1*|h1|^2 / sigma^2
注意用户 1 的干扰里没有用户 2,因为已经被 SIC 消掉了。设计功率分配最先要过的坎,就是保证这个 SIC 成立:给远端用户的功率必须让它解得出自己的信号,否则近端用户重构出来的干扰信号是错的,减去一个错误信号等于引入了更大的噪声。
2.2 多小区同频组网下的接收信号与 ICI 项
上了多小区,接收信号里就多出一整块邻区干扰。假设第 m 个基站在子信道 k 上同时服务 K 个用户,其中第 u 个用户的接收信号为:
y_m,u = h_m,u * sqrt(p_m,u) * s_m,u + 小区内其他用户叠加项 + Σ_{n≠m} h_n,u * sqrt(p_n,w) * s_n,w + n
最后那个和式就是相邻基站 n 在同一个子信道上发射的功率对用户 u 造成的干扰,即小区间干扰 ICI。对下行 NOMA 来说,ICI 的存在改变了功率分配的取值逻辑:本小区的总功率高,对边缘用户是好事,可对邻区用户就是实打实的干扰抬升。因此“最佳”并不能在单个小区内独立定义,而是整个多小区网络在某一个共同性能指标下的折中。
工程上简化模型时会做两个常见假设:一是把邻区信号全部建模为高斯噪声,不做 SIC 跨小区处理;二是只考虑相邻第一圈基站的干扰,更远小区的干扰并进噪声功率里。另一个务实的选择是给每个小区分配不同的子信道子集,让 ICI 只出现在部分频带上。这样可以把问题切成“小区内 NOMA + 小区间正交”的退化情形,也是很多论文里先做子信道分配、再做功率分配的原因。
2.3 用户配对与功率分配的耦合关系
NOMA 的大多数实用方案并不会让所有用户挤在同一子信道上,而是把每个子信道分给 2~4 个用户,这就引入了用户配对。常规做法是按等效信道增益|h|^2 / (ICI + sigma^2)排序,然后“强弱搭配”:信道最好的用户和信道最差的用户配对,次好的和次差的配对,让两用户的信道差异尽量大,这样 SIC 的功率差更容易满足,频谱效率也更好看。
但配对与功率分配不是两件独立的事。功率分配的结果反过来会影响每个用户的 SINR,进而改变排序依据;同一个用户,跟不同的用户配对时,最优的功率值也不同。于是多小区 NOMA 的完整优化是一个混合整数非线性规划:配对是整数变量,功率是连续变量。直接求全局最优解在 5G 以上的用户规模下基本不现实,所以后文所有“最佳功率分配”的讨论,都默认是给定配对方案之后的功率协同优化,再靠“配对→功率→重配对”的外环迭代逼近一个较好的局部解。
3. 最佳功率分配问题建模与两类可解路径
3.1 固定配对后的优化问题形式
固定用户配对和子信道分配后,多小区 NOMA 的功率分配问题变成一个连续优化问题。以最大化多小区加权和速率为例,目标函数写为:
maximize Σ_m Σ_u w_u * log2(1 + SINR_m,u)
约束条件有三个。第一个是每个基站的总发射功率约束Σ_u p_m,u ≤ P_max;第二个是用户最低速率约束log2(1 + SINR_m,u) ≥ R_min,这是 QoS 门限;第三个是 SIC 解码约束,即分配给远端用户的功率必须满足近端用户能先正确解出远端符号。对两个用户配对的子信道,SIC 约束可以写成近端用户解远端用户时的接收 SINR 要高于某个解调门限。
这个问题的目标函数是log2(1 + p*|h|^2 / (p_other*|h|^2 + ICI + sigma^2)),分母里含对合变量,且通过 ICI 项把各小区的功率变量耦合在一起,所以整体上是非凸的。直接丢给求解器,初始点选不好就收敛到很差的点,这也是很多现成代码“能跑但结果不合理”的根源。
3.2 SCA 序列凸近似与分数规划两条主流路线
处理非凸功率分配,最常用的两类迭代算法是 SCA(序列凸近似)和分数规划(FP)。SCA 的思路很直接:把目标函数里凸的那部分保留,非凸的那部分在当前迭代点做一阶泰勒展开,构造一个原目标的下界函数,这个下界函数是凸的。每次迭代求解这个凸近似问题,得到新的功率点,再重新做泰勒展开,重复直到功率变化小于容差。SCA 的收敛性有保障,能把解拉到一个满足 KKT 条件的驻点附近,对初始点不敏感,工程上最常用。
分数规划则针对 SINR 是分式这个痛点。把SINR = 分子 / 分母先看成一个整体变量 gamma,原问题等价于在额外约束gamma ≤ 分子/分母下最大化某个凹函数。通过引入辅助变量(矩阵加权或比例因子),把分式约束转成分子与分母解耦的线性/凸约束,再做交替迭代。多小区场景下,WMMSE(加权均方误差最小化)是最经典的变体,它证明了对多小区下行广播信道的和速率最大化等价于一个加权 MMSE 最小化问题,然后通过交替更新功率、接收机和权重稳定收敛。我在实际仿真里更常用 SCA,因为它对 matlab 代码的侵入最小——换一下展开点即可,不需要改整体框架。
3.3 两用户子信道的 KKT 闭式更新
在只有一个基站、每个子信道两个用户的简化设定下,功率分配问题小到可以用 KKT 条件直接推闭式解。对固定信道增益|h1|^2 > |h2|^2,最优功率一定让总功率约束取等号,因为速率是功率的增函数。把总功率约束代进目标函数,求导为零,可以得到功率差应当满足的一个方程:
p1 - p2 = (|h2|^2 - |h1|^2) / (|h1|^2 * |h2|^2) * sigma^2的某种权衡形式
直观结论是:信道差异越大,两个用户的最优功率差越大;总功率越低,功率差越要向强用户倾斜。这个闭式解不用迭代,非常适合作为算法验证的基线,也可以直接用在一个子信道的快速用户配对内循环里。我的习惯是单小区调试阶段先用这个闭式解核对 SIC 约束是否满足,再扩展到多小区。
3.4 求解器选型:CVX、matlab 优化工具箱还是手写投影梯度
具体到实现层面,有三条常见路线:CVX 加 sedumi/mosek 求解器适合搭原型,把 SCA 凸近似问题直接交给 CVX,代码最干净;matlab 优化工具箱自带的 fmincon 不需要额外许可,适合写独立的仿真脚本;手写投影梯度适合想彻底搞懂迭代细节、或者要部署到嵌入式环境的场景。三者的取舍参考下表。
| 路线 | 上手成本 | 数值稳定性 | 典型应用场景 |
|---|---|---|---|
| CVX + SCA | 低,模型直译 | 中,依赖求解器选型 | 论文验证、算法对比 |
| fmincon(interior-point) | 中,需设好边界和初值 | 高,内置多种回退 | 多小区链路级仿真 |
| 手写投影梯度 | 高,需自己调步长 | 中,步长敏感 | 在线快速实现、硬件加速 |
我的建议是:先搭 CVX 或 fmincon 的 SCA 框架得到正确结果,再决定要不要换成手写梯度。直接从一开始就手写,排错时根本分不清是算法错了还是信道模型错了。
4. matlab 代码骨架:从信道生成到 SCA 迭代
4.1 主程序:参数初始化与信道矩阵生成
不管功率分配算法多复杂,仿真第一步都是把多小区信道生成出来。下面的代码生成 M 个基站、每小区 K 个用户的信道系数,包含路径损耗、阴影衰落和小尺度瑞利衰落。
rng(42); M = 3; % 小区数 K = 4; % 每小区用户数 B = 8; % 子信道数 Pmax = 40; % 基站总功率(W) sigma2 = 1e-11; % 噪声功率(W) cellR = 500; % 小区半径(m) alpha = 3.5; % 路径损耗指数 shadowStd = 8; % 阴影衰落标准差(dB) posBS = [0, 0; 866, 500; 0, 1000]; posUE = zeros(M*K, 2); for m = 1:M % 用户以基站为中心、按半径均匀撒点 r = cellR * sqrt(rand(K, 1)); theta = 2 * pi * rand(K, 1); posUE((m-1)*K+1 : m*K, :) = posBS(m, :) + ... [r .* cos(theta), r .* sin(theta)]; end H = zeros(M, M*K, B); for m = 1:M for u = 1:M*K d = norm(posBS(m,:) - posUE(u,:)); if d < 1, d = 1; end % 防止除以零 PL = d^(-alpha); % 路径损耗 shadow = 10^(shadowStd * randn / 10); small = (randn(1, B) + 1j*randn(1, B)) / sqrt(2); H(m, u, :) = sqrt(PL * shadow) * small; end end这段代码的关键是信道矩阵的维度设计:H(m, u, :)里 m 是服务基站,u 是用户全局编号,最后的维度是子信道。这样邻区信道H(n, u, :)可以直接取出,不必额外维护一张干扰拓扑表。路径损耗用d^(-alpha),注意距离小于 1 米时必须钳位,否则功率会超过物理上限,出一堆 NaN。
4.2 用户分组与 SIC 顺序编排
对每个小区,先按等效信道增益对用户排序,再两两配对。这里的等效信道要把所有子信道上的平均增益算出来,排序依据用增益差的绝对值,倾向于把差距大的用户配在一起。
pairList = cell(M, 1); for m = 1:M idx = (m-1)*K+1 : m*K; Hmean = squeeze(mean(abs(H(m, idx, :)).^2, 3)); [~, order] = sort(Hmean, 'descend'); % 强弱配对:最好的和最后的配,次好的和次差的配 pairs = [order(1: K/2), order(K:-1: K/2+1)]; pairList{m} = pairs; end配对完成后,SIC 解码顺序直接由信道增益排序确定:每对里信道增益大的用户后解码,另一个先解码。注意做全程仿真时,这个解码顺序会在 SIC 模块里复用,所以最好在功率分配前就把顺序写进一个结构体数组,而不是在算法里临时再排一次。
4.3 SCA 主循环与功率更新函数
下面是 SCA 迭代的核心框架,以单小区子信道两用户为例展示原理,多小区只需在外层再枚举基站。
function [p, history] = scaPowerAlloc(Hpair, Pmax, sigma2, maxIter) % Hpair: 2 x 1 信道增益,用户1为强用户(后解码) % p: 最优功率向量 [p1, p2] p = [Pmax * 0.7, Pmax * 0.3]; % 初始解:远用户多一点 history = zeros(maxIter, 1); for it = 1:maxIter % 把目标函数和 SIC 约束在当前 p 处线性化 fun = @(x) -(log2(1 + x(1)*Hpair(1)/sigma2) + ... log2(1 + x(2)*Hpair(2)/(x(1)*Hpair(2)+sigma2))); nonlcon = @(x) deal([], [ ... log2(1 + x(2)*Hpair(2)/(x(1)*Hpair(2)+sigma2)) ... - log2(1 + 3) ]); % SIC所需最小速率 options = optimoptions('fmincon', 'Algorithm', 'interior-point', ... 'Display', 'off', 'OptimalityTolerance', 1e-8); [pNew, fval] = fmincon(fun, p, [], [], [], [], ... [0, 0], [Pmax, Pmax], nonlcon, options); history(it) = -fval; if norm(pNew - p) < 1e-4 % 收敛判断:功率变化小于阈值 p = pNew; break; end p = pNew; end end上面这个函数把问题直接交回 fmincon 求解。注意SINR2表达式里分母含x(1),让目标函数非凸;SCA 的做法是在每次迭代前把x(1)固定成上一次的值,把分母变成常数,再让 fmincon 只优化分子,这样就一步步把非凸问题拉回凸问题。实际工程里我不会真的在 every 次迭代都重新拉一个 fmincon,而是把这个内层求解换成闭式更新,速度能快一个量级。
4.4 参数表:直接影响结果正确性的仿真参数
仿真参数设置不合理的后果远比算法错误隐蔽。下表给出典型值和敏感性分析。
| 参数 | 典型值 | 调大后的影响 | 常见错误 |
|---|---|---|---|
| 路径损耗指数 | 3~3.5 | 边缘用户 SINR 快速恶化,分配更向远用户倾斜 | 用自由空间 2,低估干扰 |
| 阴影衰落标准差 | 8 dB | 用户间信道差异变大,SIC 更容易成立 | 取 0,场景过于理想 |
| 用户数 / 子信道 | 2~4 | 目标函数项增多,求解时间线性上涨 | 超过 4 后 SIC 要求极苛刻 |
| 总功率 Pmax | 30~46 dBm | 进入干扰受限区,再提功率收益变小 | 过小导致 SIC 门限不可达 |
| 收敛容差 | 1e-4~1e-6 | 过小会白跑几百轮,过大提前终止 | 用默认值不调,结果忽高忽低 |
如果你复现出来的和速率曲线在某个功率点突然掉下来,先查阴影衰落标准差,再看 SIC 约束是否被 fmincon 静默忽略了。内点法默认满足约束到一定容差,但 NOMA 的 SIC 约束是最容易在数值上触界的那一类。
4.5 排错:SIC 顺序断裂、不可行初始点与版本差异
所有复现失败,最后都能归到三类。第一类是 SIC 解码顺序断裂,即按当前功率算出来的 SINR 不满足“远端用户先解”的假设。排查时要把每个子信道的 SINR 和解码顺序打印出来,逐行核对。第二类是初始点不可行,常见于约束较强的场景。我一般先用等功率分配跑一次,把结果作为初值喂给 SCA,比随机初值稳定得多。第三类是 matlab 版本差异,个别 fmincon 选项在较新 release 里改了默认容差,建议显式指定OptimalityTolerance和ConstraintTolerance,不要依赖默认值。调试时如果遇到“当前不会命中断点”之类的提示,往往是你下载的源代码和当前 matlab 运行环境不匹配,先检查函数署名与工具箱是否齐全,再看断点是否落在了没被执行到的分支里。等功率初值跑通,再逐步换成 SCA,一层层验。
5. 把离线最优解做成在线可用的快速功率分配器
SCA 迭代在链路级仿真里跑得很稳,但到了系统级仿真或者需要实时响应的场景,几百次迭代就不太现实了。常见做法是离线训练、在线查表:先在典型信道分布下用 SCA 求出一批“信道状态 → 最优功率分配”的样本,存成数据表,在线运行时用最近邻或线性插值直接查表。下面是一个极简的查找表构建示例。
% 离线阶段:扫不同的信道差与总功率,记录最优功率比 ratioMap = zeros(20, 10); for i = 1:20 for j = 1:10 h1 = 0.1 * i; % 强用户信道增益 h2 = 0.01 * (j - 5); % 弱用户信道增益 if h2 <= 0, continue; end [p, ~] = scaPowerAlloc([h1, h2], Pmax, sigma2, 50); ratioMap(i, j) = p(2) / (p(1) + eps); end end在线阶段只需用当前信道的h1/h2索引到这张表,再乘上总功率即可得到两个用户的功率。整个查表过程在微秒级。更进一步的做法是用深度神经网络拟合这个映射,输入是信道向量,输出是功率向量,把 SCA 的结果当监督标签,训练一个 MLP;用 matlab 的深度学习工具箱就能完成。这样既保留了 SCA 的最优性,又能满足实时决策的算力预算。验证阶段有一个很实用的技巧:把 SCA 求出的功率回代到完整的链路仿真里,对比等功率分配的误比特率,若增益超过 2~3 dB 且 SIC 后两用户速率均高于门限,说明整套实现是自洽的;如果增益明显偏低,优先检查配对顺序是否在回代时被重排过。
本文还有配套的精品资源,点击获取