简介:该资源面向通信工程领域的研究人员、高校教师与研究生,聚焦基于粒子群优化(PSO)的STAR-RIS辅助NOMA无线通信系统优化问题。STAR-RIS可同时反射与传输信号,结合NOMA能提升覆盖范围、服务用户数与频谱效率;资源在不依赖完整CSI的前提下,联合优化功率分配、基站波束成形及STAR-RIS传输与反射波束成形向量,以最大化总可实现速率并保障用户最低速率。包内为1个PDF文件,约830KB,完整呈现论文方案与可运行Python代码,涵盖系统参数设置、信道模型、速率计算、目标函数、约束函数、主优化函数及结果可视化等模块,便于读者理解与复现实验。资源还涉及与DDPG等方法的对比,以及模式切换、时间切换和能量分配等不同工作协议的性能差异分析。目前已有281人学习,适合研究智能反射面、非正交多址接入与智能优化算法的读者参考。
1. 从一次仿真翻车说起:这套 STAR-RIS 辅助 NOMA 的 PSO 代码到底能干什么
如果你做过 RIS 相关的仿真,大概率遇到过这种局面:论文里写得头头是道,公式推导也跟下来了,但真到写代码复现的时候,卡在“联合优化”这一步——功率分配和智能表面相位耦合在一起,目标函数非凸,约束还带模约束,用 CVX 硬解要么跑不动,要么解出来的东西根本对不上论文曲线。我最早复现 STAR-RIS 辅助 NOMA 的时候就翻过这个车,当时用交替优化(AO)拆子问题,结果相位那一块因为模约束非凸,每次收敛到的局部解都不一样,跑十次出十条曲线,玄学得很。
这份资源给的思路是用粒子群优化(PSO)来绕开非凸性——不追求解析最优,而是用群体搜索在可行域里找近似全局最优。它把功率分配向量和 STAR-RIS 每个元素的透射/反射系数(实部虚部分离)拼成一个高维粒子,用惩罚函数处理最低速率约束和模约束,直接最小化负的总和速率。代码是完整可运行的 Python 实现,依赖numpy、matplotlib和pyswarm,包含信道建模、NOMA 速率计算、SIC 解码顺序、PSO 目标函数与约束函数,以及 OMA/NOMA 对比可视化。适合通信方向的研究生、做智能表面波束成形验证的工程师,以及想拿一个能跑通的 PSO 联合优化模板去改自己场景的人。下面我按“先跑通、再拆解、最后避坑”的顺序把它过一遍。
2. 环境搭起来、代码跑起来:从零到第一条总和速率曲线
2.1 依赖安装与最小运行环境
这套代码对环境的胃口很小,核心就三个包。pyswarm是 PSO 的封装库,接口比手写粒子群干净,支持等式/不等式约束传入。我一般会在虚拟环境里装,避免和已有的科学计算栈打架:
python -m venv star_ris_env source star_ris_env/bin/activate # Windows 用 star_ris_env\Scripts\activate pip install numpy matplotlib pyswarm装完之后直接python main.py就能跑。但要注意,pyswarm底层用的是scipy.optimize的封装,它对约束的处理是罚函数式的,不是严格可行域投影,所以约束函数写得不合理时,粒子会“飘”到不可行区域然后被罚成一个巨大的值,表现为fopt一直卡在1e6附近不下降。这不是代码 bug,是约束设计问题,后面避坑章节会细说。
2.2 系统参数与信道模型:先搞清楚每个数字的物理含义
代码里SystemParameters类定义了整套仿真的骨架。N=16 是 STAR-RIS 的元素数,K=4 是用户数(2 个透射区 + 2 个反射区),noise_power=1e-9对应的是归一化噪声功率,P_max=1.0是基站最大发射功率(归一化后的),R_min=1.0bps/Hz 是每个用户的最低速率门限。这些数字不是随便填的,它们决定了优化问题的可行域大小——P_max 越小、R_min 越高,可行域越窄,PSO 越难找到满足所有约束的解。
信道模型这块,原始代码用的是纯随机复高斯:
h_direct = (np.random.randn(params.K, 1) + 1j*np.random.randn(params.K, 1)) / np.sqrt(2) H_ris = (np.random.randn(params.N, 1) + 1j*np.random.randn(params.N, 1)) / np.sqrt(2) G = (np.random.randn(params.K, params.N) + 1j*np.random.randn(params.K, params.N)) / np.sqrt(2)这里h_direct是基站到用户的直接链路(K×1),H_ris是基站到 STAR-RIS 的链路(N×1),G是 STAR-RIS 到用户的链路(K×N)。复合信道按h_total = h_direct + G @ diag(H_ris) @ theta算,其中theta是 STAR-RIS 的系数向量。注意原始代码里np.diag(H_ris)对 N×1 的H_ris会生成 N×N 对角阵,这一步在数学上等价于逐元素相乘,但内存开销大,N 大了会明显拖慢 PSO 的每次迭代——因为目标函数每评估一次就要算一遍。
扩展版代码里补了路径损耗模型,path_loss_exp=2.7、L0=-30dB,用户位置用极坐标生成,透射区和反射区按角度划分。这个改动让仿真更接近真实场景,但也引入了一个新问题:路径损耗会让不同用户的信道增益差出好几个数量级,NOMA 的 SIC 解码顺序对增益差很敏感,增益差太大时弱用户速率会被压到 R_min 以下,PSO 直接判不可行。我一般会把L0调到 -20dB 左右,或者把用户距离范围从 50~100m 缩到 30~80m,先保证有可行解,再逐步加严。
2.3 目标函数与约束:PSO 到底在优化什么
目标函数objective_function把粒子x拆成三段:前 K 个是功率分配p,中间 N 个是 STAR-RIS 系数实部,后 N 个是虚部。拼成复数theta后算复合信道、算速率、检查约束,违反就返回1e6,否则返回负的总和速率。这里有个细节值得说:PSO 是最小化算法,所以最大化总和速率要取负号,pyswarm返回的fopt再取负才是真实总和速率。
约束函数constraints返回两个值:功率约束sum(p) - P_max和模约束sum(|theta_n|^2) - N。注意模约束写的是平方和减 N,这等价于要求所有元素的模等于 1(因为每个 |theta_n|^2 理想情况下为 1,N 个加起来就是 N)。但 PSO 的粒子在实部虚部各自 [-1,1] 的盒子里飞,|theta_n|^2不一定等于 1,约束函数只能“软”地惩罚偏离,不能强制投影到单位圆上。这是这套代码最容易被诟病的地方,也是复现时曲线对不上的主要原因之一。
lb = np.zeros(params.K + 2*params.N) ub = np.ones(params.K + 2*params.N) lb[params.K:] = -1 ub[params.K:] = 1边界设置里,功率下界是 0、上界是 1(但实际受 P_max 约束),STAR-RIS 实虚部下界 -1、上界 1。这个盒子比单位圆大,PSO 有可能找到实部虚部都在 0.9 左右的粒子,模平方和约 1.62,远超 1,约束函数会惩罚它,但惩罚力度取决于pyswarm内部的罚因子,调不好就会在可行域边缘反复横跳。
3. 把 PSO 联合优化拆开看:粒子编码、惩罚函数与解码顺序的耦合
3.1 粒子维度与变量耦合:为什么不能分开优化
这套代码的粒子维度是 K + 2N = 4 + 32 = 36 维。功率分配 4 维,STAR-RIS 实部 16 维,虚部 16 维。把功率和相位拼在一个粒子里,意味着 PSO 在搜索时同时调整这两类变量,而不是像交替优化那样固定一个优化另一个。好处是能捕捉功率和相位之间的耦合——比如某个用户信道增益低,PSO 可以同时给它多分功率、让 STAR-RIS 相位朝它对齐;坏处是搜索空间维度高,粒子群规模 50、迭代 100 次,在 36 维空间里其实相当稀疏,容易早熟收敛。
我实测过,把swarm_size从 50 提到 100、maxiter从 100 提到 200,总和速率能再涨 5%~8%,但耗时翻四倍。如果只是验证算法框架,原始参数够用;如果要出论文级曲线,建议至少swarm_size=80, maxiter=150,并且固定随机种子多跑几次取平均,否则单次结果波动很大。
3.2 惩罚函数设计:1e6 这个数不是随便选的
目标函数里违反约束返回1e6,这个值必须远大于任何可行解的目标函数值。总和速率在 K=4、R_min=1 的场景下大概在 4~8 bps/Hz 量级,取负之后是 -4~-8,1e6 足够把不可行解“顶”到搜索空间边缘。但问题在于,如果初始粒子群大部分都不可行,PSO 的前若干代基本在瞎飞,因为所有粒子的适应度都是 1e6,没有梯度信息引导。我一般会先用随机搜索撒 500 个点,统计可行解比例,如果低于 10%,就说明参数设得太紧,得放宽 R_min 或增大 P_max。
另一个坑是约束函数的返回值。pyswarm要求f_ieqcons返回的值 ≤ 0 表示满足约束,> 0 表示违反。原始代码里power_constraint = sum(p) - P_max,当 sum(p) ≤ P_max 时返回负值或零,满足;theta_constraint = sum(|theta|^2) - N,当模平方和 ≤ N 时满足。但注意,模约束理想情况是等于 N,不是小于等于 N。如果 PSO 找到一组 theta 模平方和远小于 N(比如所有元素都接近 0),约束函数返回负值,PSO 认为满足约束,但实际上这组解对应的 STAR-RIS 几乎不反射/透射能量,总和速率会很低。这是“软约束”的典型漏洞——它只惩罚超限,不惩罚“不足”。
3.3 SIC 解码顺序:增益排序与速率计算的联动
calculate_rates函数按decoding_order依次计算每个用户的 SINR,先解码的用户受到的后继干扰被逐步消除。解码顺序由np.argsort(channel_gains)决定,增益低的先解码(因为 NOMA 里弱用户先解,强用户后解,强用户能容忍弱用户的干扰)。但这里有个隐藏问题:channel_gains是在优化前用初始theta(全 1 向量)算的,PSO 迭代过程中theta在变,等效信道增益排序可能变,但decoding_order没跟着更新。这意味着 PSO 优化的速率计算用的是固定解码顺序,而实际最优解码顺序可能不同。
扩展版代码里get_decoding_order(theta)每次根据当前 theta 重新排序,更合理,但也更耗时。我的做法是:在目标函数里每评估一次就重算解码顺序,但把np.argsort换成更快的np.argpartition,K=4 时差别不大,K 大了能省不少时间。另外,如果两个用户增益非常接近,排序会在两者之间反复切换,导致目标函数不连续,PSO 容易震荡。可以在排序前给增益加一个极小扰动(如+1e-8*np.random.randn(K))来稳定。
4. 避坑与排查:复现时最容易翻车的五个地方
4.1 现象:fopt 一直停在 1e6,总和速率打印出来是负数
原因:初始粒子群全部违反约束,PSO 找不到可行解。最常见的是 R_min 设得太高(比如 2.0 bps/Hz)而 P_max 太小(0.5),或者路径损耗参数让所有用户信道增益都极低。
解决:先把 R_min 降到 0.5,P_max 提到 2.0,跑通看到 fopt 降到 -3 以下,再逐步收紧参数。同时打印初始粒子群的可行解比例,低于 5% 就别指望 PSO 能救回来。
4.2 现象:总和速率每次运行都不一样,波动超过 20%
原因:PSO 是随机算法,np.random.seed(42)只固定了信道生成,没固定 PSO 内部的粒子初始化。pyswarm默认用np.random.rand初始化粒子位置,种子没设的话每次都不一样。
解决:在调用pso之前加np.random.seed(42),并且跑 10 次取平均和标准差。如果标准差大于均值的 10%,说明swarm_size不够,加到 100 以上。
4.3 现象:STAR-RIS 系数模不接近 1,优化出来的 theta 很多接近 0
原因:模约束是软约束,只惩罚超限不惩罚不足,PSO 发现把 theta 设小能降低“违反程度”(因为平方和更小),于是倾向于收缩 theta。
解决:把模约束改成等式约束的惩罚形式,在目标函数里加一项lambda * sum((|theta_n|^2 - 1)^2),lambda 取 10~100。或者更直接:在目标函数里把 theta 归一化,theta = theta / np.abs(theta),强制模为 1,代价是相位信息保留但幅度信息丢失。我一般用前者,后者会改变优化问题的性质。
4.4 现象:OMA 对比曲线是硬编码的示例数据,不是真实计算
原因:原始代码里oma_rates = np.array([1.2, 1.3, 1.1, 1.4])是写死的,noma_rates也是写死的,跟 PSO 优化结果无关。这是复现代码里最容易被忽略的“假对比”。
解决:OMA 速率要自己算——每个用户独占带宽,速率是log2(1 + p_k * |h_k|^2 / noise_power),功率平均分配或按信道增益注水分配。把 OMA 速率算出来再和 PSO 优化后的 NOMA 速率画在一起,才是真实对比。
4.5 现象:扩展版代码里 DDPG 部分跑不起来,报维度不匹配
原因:DDPG_Agent类引用了ActorNetwork、CriticNetwork、ReplayBuffer,但这些类在给出的代码里没有定义,只有调用。这是典型的“代码片段不完整”。
解决:要么补全这三个类的实现(Actor 用两层全连接 + tanh 输出,Critic 用两层全连接 + 线性输出,ReplayBuffer 用 deque 存 transition),要么先跳过 DDPG 对比,专注 PSO 部分。如果只是想验证 PSO 的有效性,DDPG 不是必需的。
5. 进阶技巧:用参数扫描和热启动把 PSO 的复现质量拉上来
5.1 参数扫描:找到 R_min 和 P_max 的可行边界
与其盲目调 PSO 参数,不如先做一轮参数扫描,搞清楚在给定信道下,R_min 和 P_max 的可行域边界在哪。我一般固定 P_max,从 0.5 到 2.0 步进 0.25,每个点跑一次 PSO,记录总和速率和可行解比例。画出来之后能明显看到:P_max 低于某个阈值时,可行解比例骤降,总和速率也塌下去。这个阈值就是你这套系统参数的“底线”,低于它任何优化算法都救不了。
import numpy as np from pyswarm import pso def scan_pmax(params, pmax_values): results = [] for pmax in pmax_values: params.P_max = pmax xopt, fopt = optimize_STAR_RIS_NOMA(params) results.append((pmax, -fopt)) return results pmax_values = np.arange(0.5, 2.1, 0.25) results = scan_pmax(params, pmax_values) for pmax, rate in results: print(f"P_max={pmax:.2f}, sum_rate={rate:.4f}")这段代码的逻辑是:外层循环改 P_max,内层调 PSO,每次记录最优总和速率。注意每次调用前要重新生成信道还是复用同一组信道?如果复用,对比的是同一信道下不同功率预算的影响;如果重新生成,对比的是平均性能。我一般复用同一组信道,这样曲线更平滑,能清楚看到功率预算的边际效益。
5.2 热启动:用上一次的最优解初始化粒子群
PSO 冷启动时粒子随机撒在 36 维空间里,前几十代基本在探索。如果做参数扫描,相邻 P_max 点的最优解很接近,可以把上一个点的xopt作为当前点的粒子群中心,周围加小扰动生成初始粒子。pyswarm不直接支持自定义初始粒子,但可以改pso的initial参数(部分版本支持),或者自己包一层:先用pso跑少量迭代,把结果作为下一次的x0。
我实测过热启动能把收敛所需迭代数从 100 降到 40 左右,总和速率还能略高一点,因为粒子群集中在有希望的区域。代价是如果上一个点的最优解不是当前点的好起点(比如 P_max 跳变太大),热启动反而会早熟。所以热启动适合参数连续变化的扫描,不适合独立场景。
5.3 验证方法:用穷举法在小规模场景下对拍
PSO 是启发式算法,你怎么知道它找到的解靠谱?最直接的办法是把 N 和 K 调小(比如 N=2, K=2),然后暴力穷举功率分配和相位(相位离散成 8 个角度),算出真实最优,再和 PSO 结果对比。如果 PSO 能找到穷举最优的 95% 以上,说明参数设置合理;如果差很远,要么是 PSO 参数不够,要么是惩罚函数有问题。
import itertools def brute_force_2x2(params): best_rate = 0 best_sol = None power_grid = np.linspace(0, params.P_max, 20) phase_grid = np.linspace(0, 2*np.pi, 8, endpoint=False) for p1 in power_grid: for p2 in power_grid: if p1 + p2 > params.P_max: continue for ph1 in phase_grid: for ph2 in phase_grid: theta = np.array([np.exp(1j*ph1), np.exp(1j*ph2)]) # 算速率、更新 best_rate return best_rate, best_sol这段穷举的逻辑是:功率在 [0, P_max] 上离散 20 个点,相位在 [0, 2π) 上离散 8 个点,两层循环遍历所有组合,算总和速率取最大。N=2, K=2 时组合数是 20×20×8×8=25600,秒级跑完。拿这个结果和 PSO 对比,心里就有底了。
5.4 一个我踩过的坑:别在目标函数里做可视化
早期我为了调试,在objective_function里加了一行plt.plot(...),结果 PSO 每评估一次就画一张图,100 代 × 50 粒子 = 5000 张图,内存直接爆掉,进程被系统 kill。血泪经验:目标函数里只做数值计算,任何 I/O、打印、绘图都放到外面,用全局变量或回调在迭代结束后统一处理。如果非要看迭代过程,用pyswarm的debug=True打印到控制台,别碰 matplotlib。
从那以后我每次写 PSO 目标函数都强制走一遍“纯计算检查”——函数体内不允许出现print、plt、open、save,只允许numpy运算和返回标量。这个习惯帮我省了至少三次调试到半夜的崩溃。希望帮到你。
本文还有配套的精品资源,点击获取