☰
基于MATLAB的QD信道射线追踪与多径合成工程
2026/10/26 15:00:58 网站建设 项目流程

简介:面向毫米波(mmWave)射频研究与无线信道仿真的科研人员和工程技术人员,这份 MATLAB 射线追踪代码给出准确定性(QD)通道模型的完整实现,可用于室内传播特性分析与基于跟踪的移动性仿真。代码采用图像法光线追踪,针对毫米波频段定制,计入镜面反射而忽略衍射,内置基于实测的准确定性射线扩散模型与配套材料库,并支持 XML、AMF 格式的 CAD 模型导入。压缩包共 328 个文件,以 dat 测量数据、m 函数脚本、json 与 csv 配置、xml 场景描述、amf 建筑模型为主,另有说明文档与示例工程,整体约 33.82MB;代码已在 MATLAB R2019b 下测试并兼容 R2016b,无需工具箱即可运行。目前已有 1036 人学习。读者可获得一套开箱即用的毫米波信道仿真基线,借助默认示例场景理解场景配置与射线扩散参数设置,并在此基础上开展传播模型验证与对比实验。

1. 从一条“不该跳变”的射线说起:QD 通道到底在算什么

把接收机往旁边挪 5 厘米,功率延迟谱就整个换一副面孔——时延扩展从 20 ns 跳到 80 ns,主波束方向也对不上,这是纯随机信道模型的通病:位置连续变化,信道却跳变。准确定性(QD)通道针对的正是这个:把多径拆成几何可算的确定性射线 D-ray 和统计生成的随机分量 R-ray、F-ray,前者用射线追踪求镜像路径,后者按簇铺开,并让两者随位置平滑演化。这份 MATLAB 射线追踪代码输入房间尺寸、收发坐标和阵列参数,输出带离开角、到达角、时延和复幅度的信道矩阵,是一套系统开源、能直接拆开改的工程。做毫米波信道仿真、波束赋形验证、定位算法标定的开发者,拿它当底座比调黑盒工具箱更踏实。

2. 工程骨架:几何、射线、信道三层怎么分

拿到一份 MATLAB 信道工程,最怕的是所有逻辑塞进一个 .m 文件,改一个参数要翻三百行。这份代码的分层思路很朴素:几何层只回答“墙在哪”,射线层只回答“有哪些路径”,信道层只回答“这些路径怎么变成 H”。三层之间靠结构体传参,不共享全局变量,所以你可以只换掉几何层而不动后面两层的任何一行。

2.1 目录结构与职责边界

qd-realization/ ├── main.m % 唯一入口:读配置 → 建几何 → 追射线 → 合信道 → 出图 ├── config/ │ ├── scenario_params.m % 场景、阵列、随机分量参数 │ └── material_lib.m % 材质库:相对介电常数、电导率 ├── geometry/ │ ├── build_room.m % 长方体房间 → 面元列表(顶点 + 法向量 + 材质索引) │ ├── mirror_point.m % 点关于平面 n·x = d 的镜像 │ └── seg_plane_hit.m % 线段与面元求交 + 面内判定 ├── ray/ │ ├── dray_image_method.m % 迭代镜像法,产出 D-ray │ ├── rray_cluster.m % 簇生成:时延-角度联合分布 │ └── fray_birth_death.m % F-ray 生灭过程,带空间一致性 ├── channel/ │ ├── sv_upa.m % 均匀平面阵导向矢量 │ ├── synth_cir.m % 射线集合 → 频域信道矩阵 │ └── spread_metrics.m % 时延扩展 / 角度扩展统计 └── results/ % 图与 .mat 输出,不存在就 mkdir

这套划分的价值在替换成本上。ray/层只认walls结构体里的字段v(四个顶点)、n(单位法向量)、d(平面偏移)、mat(材质索引),不关心房间是长方体还是三角面元拼出来的;channel/层只认射线的tau / A / phi_t / theta_t / phi_r / theta_r六个字段,不关心射线是镜像法算的还是实测导入的。你把这四个接口定死,后面换场景、换阵列、换输出格式都不会牵一发动全身。

2.2 配置先行:一次跑通要盯住的几个参数

所有可调项集中在scenario_params.m,主流程只读不写。这么做的好处是复现实验时你只需要保存一个结构体,而不是回忆“上次那个 0.3 到底是间距还是损耗”。

function cfg = scenario_params() %SCENARIO_PARAMS 场景与阵列参数集中管理,下游模块只读 cfg.fc = 60e9; % 载频 60 GHz cfg.c = 3e8; cfg.lambda = cfg.c / cfg.fc; % 房间尺寸 [Lx Ly Lz],单位 m cfg.room = [10 8 3.2]; cfg.tx_pos = [1.0 1.0 1.5]; % 发射端坐标 cfg.rx_pos = [8.5 6.5 1.4]; % 接收端坐标 cfg.max_order = 2; % 反射阶数,2 阶路径数按 6^2 增长 cfg.wall_mat = [1 1 1 1 2 3]; % 六面材质:1 混凝土 2 玻璃 3 木板 cfg.tx_array.num = [4 4]; % 4x4 均匀平面阵 cfg.rx_array.num = [4 4]; cfg.array_d = 0.5; % 归一化阵元间距 d/lambda cfg.rray.cluster_num = 12; % 簇个数 cfg.rray.delay_spread = 15e-9; % 时延扩展,s cfg.rray.as_spread = deg2rad(12); % 角度扩展,rad cfg.seed = 20240; % 固定种子,报告里的图才追得回来 end
参数典型取值改大了会怎样
fc28e9 / 60e9 / 140e9波长变短,同样的阵元间距对应更大电尺寸,波束更窄;反射系数随频率也变
max_order1 / 2 / 3路径数按 6^k 爆炸,2 阶够用,3 阶基本只用来验证收敛
array_d0.5大于 0.5 会出现栅瓣,信道矩阵的条件数立刻变差
delay_spread10e-9 ~ 40e-9直接决定 PDP 的尾巴长度和 RMS 时延扩展
seed任意整数只影响随机分量,D-ray 是确定的,不受影响

max_order是最容易拍脑袋定的一个。一阶反射覆盖了绝大多数室内强径,二阶能把角落里的二次反射补上,三阶往上单条路径的功率已经掉到噪声量级,却要多算 216 条路径。我一般先用 1 阶把链路跑通、指标对平,再开到 2 阶看统计量变不变,变了才考虑往上加。

2.3 main.m 的调用顺序与数据流

function main() cfg = scenario_params(); rng(cfg.seed, 'twister'); % 固定种子 walls = build_room(cfg.room, cfg.wall_mat); % 面元列表 dray = dray_image_method(cfg, walls); % 确定性射线 clust = rray_cluster(cfg); % 随机簇 fray = fray_birth_death(cfg, numel(clust)); % 生灭状态 rays = [dray(:); clust(:); fray(:)]; % 统一为同一种结构体 H = synth_cir(cfg, rays, cfg.fc); % Nr x Nt 复矩阵 [ds, as] = spread_metrics(rays); % 时延扩展 / 角度扩展 if ~exist('results','dir'), mkdir('results'); end save(fullfile('results','H.mat'), 'H', 'rays', 'cfg'); plot_pdp(rays, cfg); % 功率延迟谱 end

关键在那行[dray(:); clust(:); fray(:)]。三种射线的来源完全不同——D-ray 来自几何求交,R-ray 来自统计抽样,F-ray 是状态机——但它们进合成函数之前必须归一成同一套字段。这一步不做,后面每加一种射线就要改一次synth_cir,是最容易积累技术债的地方。字段统一之后,synth_cir只写一次循环,加新分量只是往rays里多塞几个元素。

顺序也有讲究:build_room必须在射线之前,因为镜像法要用法向量;rng必须在随机分量之前,且只调用一次,多次 reset 会让不同模块的随机流互相覆盖。我见过把rng(seed)写进每个子函数的版本,结果改一下簇的个数,D-ray 明明没变但整张 PDP 全变了,查了半天才发现是随机流被重排。

3. 确定性射线:镜像法把 D-ray 从几何里抠出来

QD 模型里 D-ray 承担了绝大部分能量,直射径加上几次强反射,往往能解释 80% 以上的接收功率。这部分必须算准,后面随机分量再怎么调都是锦上添花。这一章从面元建模讲到反射系数,中间那步“回溯求交”是整个流程最容易翻车的地方。

3.1 房间面元化:法向量朝内是硬约束

function walls = build_room(L, mat_idx) % 长方体房间 → 6 个矩形面元,法向量一律指向房间内部 x = L(1); y = L(2); z = L(3); V = [0 0 0; x 0 0; x y 0; 0 y 0; 0 0 z; x 0 z; x y z; 0 y z]; F = [1 2 3 4; 5 6 7 8; 1 2 6 5; 2 3 7 6; 3 4 8 7; 4 1 5 8]; V0 = V(F(:,1),:); V1 = V(F(:,2),:); V2 = V(F(:,3),:); V3 = V(F(:,4),:); c = (V0 + V1 + V2 + V3) / 4; % 面元中心 n = cross(V1 - V0, V2 - V0, 2); n = n ./ vecnorm(n, 2, 2); % 单位法向量 room_c = [x y z] / 2; for k = 1:6 if dot(n(k,:), c(k,:) - room_c) < 0 % 统一翻向房间内部 n(k,:) = -n(k,:); end walls(k).v = [V0(k,:); V1(k,:); V2(k,:); V3(k,:)]; walls(k).n = n(k,:); walls(k).d = dot(n(k,:), c(k,:)); % 平面方程 n·x = d walls(k).mat = mat_idx(k); end end

平面用n·x = d存,比存三个点省事得多:镜像和求交都只需要n和d,顶点只在“交点是否落在面内”那一步用。法向量方向必须统一朝内,否则mirror_point会把发射端镜像到房间外面去,一阶反射一条都留不下。我调试时习惯先画一次:把六个面元中心和它们的法向量用quiver3打出来,箭头朝外就直接改,别等到路径数为零再回头找。

顶点顺序也不能随便写。F里每个面的四个顶点必须绕行一圈(不能是 1-2-3-4 里的对角线顺序),cross(V1-V0, V2-V0)才算得出正确的法向;顺序写错,叉积出来是零向量,vecnorm一做除就出 NaN,后面全线崩。这种错误 MATLAB 不会报,只会让你在 PDP 图上看到一条平线。

3.2 迭代镜像法:正向镜像、反向回溯、有效性判定

function rays = dray_image_method(cfg, walls) P = cfg.tx_pos; Q = cfg.rx_pos; rays = struct('tau',{},'A',{},'phi_t',{},'theta_t',{},'phi_r',{},'theta_r',{}); for order = 1:cfg.max_order seq_list = enum_wall_seqs(numel(walls), order); % 6^order 个墙面序列 for s = 1:size(seq_list,1) seq = seq_list(s,:); % 1) 正向镜像:Tx 依次关于 seq 中每个面镜像 Pimg = P; imgs = zeros(order,3); for i = 1:order Pimg = mirror_point(Pimg, walls(seq(i))); imgs(i,:) = Pimg; end % 2) 反向回溯:从 Rx 连到最后一个镜像点,逐面求交 pts = zeros(order,3); cur = Q; ok = true; for i = order:-1:1 [hit, inside] = seg_plane_hit(cur, imgs(i,:), walls(seq(i))); if ~inside, ok = false; break; end pts(i,:) = hit; cur = hit; end if ~ok, continue; end % 交点跑出面外 → 这条路不存在 % 3) 折线路径 → 时延、反射系数、复幅度 path = [P; pts; Q]; seg = diff(path, 1, 1); d = vecnorm(seg, 2, 2); tau = sum(d) / cfg.c; G = 1; for i = 1:order nn = walls(seq(i)).n; cos_i = abs(dot(-seg(i,:)/d(i), nn)); G = G * reflection_coeff(acos(min(cos_i,1)), ... walls(seq(i)).mat, cfg.fc); end A = (cfg.lambda/(4*pi)) * G / sum(d); % 镜像源球面波扩散 + 反射损耗 [phi_t, theta_t] = vec2angle( seg(1,:)); [phi_r, theta_r] = vec2angle(-seg(end,:)); rays(end+1) = struct('tau',tau,'A',A,'phi_t',phi_t, ... 'theta_t',theta_t,'phi_r',phi_r,'theta_r',theta_r); %#ok<AGROW> end end end

三步走的分工很明确。正向镜像只负责“假如这条路径存在,反射点应该在哪”,它不判断可见性;反向回溯从接收端出发,把镜像点当虚拟源,逐面求交并检查交点是否落在面元矩形内,落不进去就说明这条镜像路径在现实里被墙挡住了,直接丢弃;最后一步才把折线转成时延、角度和复幅度。

幅度那一行A = (cfg.lambda/(4*pi)) * G / sum(d)用的是镜像源近似:把整个反射链等价成位于最后一个镜像点上的一个球面波源,自由空间衰减按总路程sum(d)算,G是各次反射系数的连乘。直射径可以走同一条公式(order = 0、G = 1),这样 LOS 和 NLOS 的幅度量纲天然一致,后面算功率比不会差一个常数因子。严格说起来做近场或大面元时得按分段扩散因子算,但室内几十米尺度上这个近似够用。

enum_wall_seqs(6, 2)就是 36 个两两组合,用ndgrid一行能展开,不值得为它写递归。真正要小心的是去重:序列[1 2]和[2 1]是两条不同的路径(先打左墙再打前墙,和先打前墙再打左墙),不能合;但[1 1]这种同一面墙连续反射两次在长方体里也是合法的(对着同一面墙来回弹),别顺手给过滤掉。

3.3 反射系数与材质:比几何更容易出错的地方

function [G, eps_c] = reflection_coeff(theta_i, mat, fc) % 单层均匀介质菲涅尔反射;theta_i 为入射角(与法线夹角,rad) eps0 = 8.8541878128e-12; omega = 2*pi*fc; eps_c = mat.eps_r - 1j*mat.sigma/(omega*eps0); % 复相对介电常数 k = sqrt(eps_c - sin(theta_i).^2); G_te = (cos(theta_i) - k) ./ (cos(theta_i) + k); % 垂直极化 G_tm = (eps_c*cos(theta_i) - k) ./ (eps_c*cos(theta_i) + k); % 平行极化 G = 0.5*(G_te + G_tm); % 未指定极化时的折中 end

mat.sigma的单位是 S/m,omega里fc必须是 Hz。这两处是最高频的翻车点:见过把 60 GHz 写成60e6的,也见过电导率按 mS/m 填的,算出来的eps_c虚部差三个数量级,|G|直接大于 1,能量凭空多出来。

几个自检点记住就够:正入射(theta_i = 0)时反射最弱,掠入射(theta_i → π/2)时|G| → 1,任何角度下|G| ≤ 1。写完后画一条theta_i从 0 到 π/2 的曲线,形状不对就是公式或单位错了,比在整条链路里查快得多。

材质库我一般单独放一个文件,把混凝土、玻璃、木板在几个常用频点的eps_r和sigma列成表:

材质eps_rsigma (S/m) @60 GHz说明
混凝土5.240.0462强反射,室内主反射面
玻璃6.300.0100镜面感强,穿透也强
木板2.900.0200反射弱,常用于隔断

这些数值不同文献之间能差一倍,取值来源一定要写进注释和报告里。做对比实验时材质一改,PDP 的尾巴形状就变,别把材质差异当成模型误差。

4. 随机分量与信道合成:R-ray、F-ray 怎么和 D-ray 拼成 H

D-ray 只能覆盖镜面反射,真实的室内还有大量漫散射、家具边缘绕射,以及人走动造成的遮挡。这些没法用几何精确

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询