从数学公式到代码实现:用Python仿真大规模MIMO预编码的完整流程

从数学公式到代码实现:用Python仿真大规模MIMO预编码的完整流程

如果你是一位通信算法工程师或者相关领域的研究者,大概率已经对大规模MIMO(Massive MIMO)的理论框架耳熟能详。教科书和论文里充斥着各种优美的矩阵公式,从迫零(ZF)到块对角化(BD),原理推导看起来清晰明了。然而,当我真正尝试将纸面上的 W = H^H (H H^H)^{-1} 转化为可运行、可验证的代码时,才发现中间隔着一道巨大的鸿沟。奇异值分解(SVD)中一个维度的疏忽,功率归一化时一个范数选择的差异,都可能导致仿真结果与理论预期大相径庭,更不用说和MATLAB这类传统仿真工具的结果进行交叉验证了。

这篇文章正是为了弥合这道鸿沟而写。我不会重复教科书上的基础定义,而是假设你已经理解了预编码的核心目标是利用信道状态信息(CSIT)在发送端预处理信号,以消除多用户干扰。我们将直接切入实战,聚焦于如何用Python和NumPy,一步步、稳健地将那些线性代数运算落地为代码。我会分享在实现过程中踩过的坑,比如信道矩阵 H 的维度究竟应该是 用户天线数 × 基站天线数 还是反过来,SVD分解后到底该取 V 的哪些列,以及如何实现工程上至关重要的功率约束。最终,你将获得一套模块化、可复现的代码,不仅能算出预编码矩阵,还能计算和可视化频谱效率等关键性能指标,并与经典结果进行比对。

1. 仿真环境搭建与信道建模

在动手写任何算法之前,一个稳定、可复现的仿真环境是基石。我强烈推荐使用 Jupyter NotebookJupyter Lab 进行这类探索性编程和算法验证。它的交互式特性允许你逐个单元格执行代码,实时检查中间变量的形状和数值,这对于调试复杂的矩阵运算至关重要。

首先,我们需要导入核心的科学计算库。别小看这个步骤,版本的一致性有时会带来意想不到的结果。

import numpy as np
import matplotlib.pyplot as plt
from scipy import linalg
import warnings
warnings.filterwarnings('ignore')  # 暂时忽略一些不影响运行的警告

print(f"NumPy版本: {np.__version__}")

接下来是信道建模。大规模MIMO的核心特征之一是基站天线数 M 远大于单用户天线数 N_k,并且用户数 K 也较多。我们假设一个典型的单小区下行链路场景:

def generate_mu_mimo_channel(M, K, N_k, correlation='uncorrelated', seed=None):
    """
    生成多用户MIMO下行链路信道矩阵。

    参数:
    M: 基站发射天线数
    K: 用户数
    N_k: 每个用户的接收天线数(假设所有用户相同)
    correlation: 信道相关性模型,'uncorrelated' 或 'rayleigh'
    seed: 随机种子,用于结果复现

    返回:
    H_list: 长度为K的列表,每个元素是第k个用户的信道矩阵 H_k,形状为 (N_k, M)
    H: 聚合信道矩阵,形状为 (sum(N_k), M),即 (K*N_k, M)
    """
    if seed is not None:
        np.random.seed(seed)

    H_list = []
    for k in range(K):
        # 每个用户的信道矩阵:瑞利衰落,复高斯随机变量
        # 注意维度:H_k 是 N_k x M,表示从M根发射天线到用户k的N_k根接收天线的信道
        if correlation == 'uncorrelated':
            # 独立同分布瑞利衰落
            H_k = (np.random.randn(N_k, M) + 1j * np.random.randn(N_k, M)) / np.sqrt(2)
        else:
            # 此处可扩展为更复杂的相关信道模型,如Kronecker模型
            H_k = (np.random.randn(N_k, M) + 1j * np.random.randn(N_k, M)) / np.sqrt(2)
        H_list.append(H_k)

    # 垂直堆叠所有用户的信道矩阵,形成聚合信道矩阵 H
    H = np.vstack(H_list)  # 形状: (K*N_k, M)
    return H_list, H

注意:信道矩阵 H_k 的维度是 (N_k, M)。这是一个关键点,它决定了后续所有矩阵运算的维度一致性。许多初学者容易在这里弄反,导致后续的伪逆或SVD无法计算。

为了验证我们的信道生成是否正确,可以快速检查一下矩阵的秩和条件数:

M = 64  # 基站天线数
K = 8   # 用户数
N_k = 2 # 每个用户天线数
H_list, H = generate_mu_mimo_channel(M, K, N_k, seed=42)

print(f"聚合信道矩阵 H 的形状: {H.shape}")
print(f"矩阵 H 的秩: {np.linalg.matrix_rank(H)}")
print(f"矩阵 H 的条件数: {np.linalg.cond(H):.2e}")

在理想独立衰落下,当 M > K*N_k 时,矩阵 H 通常是列满秩或行满秩的,这取决于天线数量的对比。这个属性直接影响ZF等基于求逆的预编码算法的稳定性。

2. 线性预编码算法的核心实现

理论部分我们快速回顾一下目标:对于聚合信道 H(维度 (K*N_k, M))和待发送的数据符号向量 s(维度 (K*N_k, 1)),我们要找到一个预编码矩阵 W(维度 (M, K*N_k)),使得发送信号 x = W s 满足功率约束 E[||x||^2] = P,并且能有效压制用户间干扰。

2.1 迫零(ZF)预编码:陷阱与实现

ZF预编码的数学公式简洁优美:W_zf = H^H (H H^H)^{-1}。但在代码实现时,有以下几个工程细节必须处理:

  1. 伪逆的计算:当 H 不是方阵时,我们使用Moore-Penrose伪逆。NumPy的 np.linalg.pinv 函数基于SVD,数值上更稳定,但理解其与直接公式计算的关系很重要。
  2. 功率归一化:这是保证仿真符合实际功率约束的关键一步,却常常在示例代码中被忽略。归一化必须在预编码矩阵计算完成后进行。
  3. 维度验证:每一步运算后,都应用 assert 语句检查矩阵维度,这是避免隐蔽错误的最佳实践。

下面是一个加入了详细检查和注释的ZF预编码实现:

def zero_forcing_precoding(H, P_tx=1.0, method='pinv'):
    """
    计算迫零预编码矩阵。

    参数:
    H: 聚合信道矩阵,形状 (N_total, M), N_total = sum(N_k)
    P_tx: 基站总发射功率
    method: 求逆方法,'pinv' 使用伪逆(更稳定),'direct' 使用直接公式计算

    返回:
    W_zf: 迫零预编码矩阵,形状 (M, N_total)
    """
    N_total, M = H.shape
    # 确保基站天线数不少于总数据流数,否则信道求逆会严重放大噪声
    if M < N_total:
        print(f"警告: 基站天线数 M({M}) < 总数据流数 N_total({N_total}),ZF性能将严重恶化。")

    if method == 'pinv':
        # 使用伪逆:W = H^H * (H H^H)^(-1) 等价于 H 的右伪逆
        # numpy.linalg.pinv 计算的是 Moore-Penrose 伪逆,对于行满秩矩阵,pinv(H) = H^H (H H^H)^{-1}
        W = np.linalg.pinv(H)  # 形状 (M, N_total)
    elif method == 'direct':
        # 直接公式计算,适用于 H 行满秩的情况
        H_H = H.conj().T  # 共轭转置
        H_HH = H @ H_H    # 形状 (N_total, N_total)
        # 添加一个极小正则项防止矩阵奇异,提升数值稳定性
        epsilon = 1e-10
        W = H_H @ np.linalg.inv(H_HH + epsilon * np.eye(N_total))
    else:
        raise ValueError("method 参数必须是 'pinv' 或 'direct'")

    # 功率归一化:确保发送信号满足总功率约束 E[||x||^2] = P_tx
    # 假设数据符号 s 是单位功率的,即 E[s^H s] = N_total
    # 那么发送功率 = trace(W^H W) * (每个数据流的功率)
    # 更常见的工程做法是进行缩放,使得 ||W||_F^2 = P_tx
    scale_factor = np.sqrt(P_tx / np.trace(W.conj().T @ W))
    W_zf = scale_factor * W

    # 维度验证
    assert W_zf.shape == (M, N_total), f"预编码矩阵形状错误: {W_zf.shape}"
    return W_zf

提示:在实际仿真中,我通常优先使用 method='pinv'np.linalg.pinv 内部使用SVD并自动处理秩亏情况,数值鲁棒性远高于直接求逆。虽然计算量稍大,但对于天线数在百位量级的大规模MIMO仿真,仍在可接受范围内。

让我们测试一下,并观察等效信道 H_eq = H @ W_zf 是否接近单位阵(即实现了“迫零”):

# 生成信道
H_list, H = generate_mu_mimo_channel(M=32, K=4, N_k=2, seed=123)
N_total = H.shape[0]

# 计算ZF预编码矩阵
W_zf = zero_forcing_precoding(H, P_tx=1.0, method='pinv')

# 计算等效信道
H_eq = H @ W_zf  # 理想情况下应为近似单位阵

# 检查等效信道的对角优势和非对角元素(干扰)大小
print("等效信道矩阵 H_eq 的形状:", H_eq.shape)
print("H_eq 对角线元素的平均幅度:", np.mean(np.abs(np.diag(H_eq))))
print("H_eq 非对角线元素的平均幅度:", np.mean(np.abs(H_eq - np.diag(np.diag(H_eq)))))

如果实现正确,你会看到对角线元素的幅度接近一个常数(由功率归一化决定),而非对角线元素的幅度远小于对角线元素,这表明用户间干扰被有效压制。

2.2 正则化迫零(RZF)或MMSE预编码

ZF预编码在低信噪比或信道条件数大时,会过度放大噪声。正则化迫零通过引入一个正则化因子 alpha(或 beta)来平衡干扰消除和噪声增强。其公式为 W_rzf = H^H (H H^H + alpha * I)^{-1}

这里的 alpha 选择是一门艺术,通常与噪声功率 sigma_n^2 和发射功率 P_tx 有关。一个常见的启发式设置是 alpha = N_total * sigma_n^2 / P_tx

def regularized_ze_precoding(H, P_tx=1.0, snr_db=20):
    """
    计算正则化迫零(RZF)预编码矩阵,也称为MMSE预编码。

    参数:
    H: 聚合信道矩阵
    P_tx: 发射总功率
    snr_db: 信噪比(dB),用于计算噪声功率,从而确定正则化因子

    返回:
    W_rzf: RZF预编码矩阵
    """
    N_total, M = H.shape
    H_H = H.conj().T

    # 将SNR转换为线性值,并计算噪声功率sigma_n^2
    # 假设接收信号功率归一化为1,则噪声功率 sigma_n^2 = 10^(-snr_db/10)
    sigma_n_2 = 10 ** (-snr_db / 10.0)

    # 计算正则化因子 alpha
    # 一种常见选择:alpha = N_total * sigma_n^2 / P_tx
    alpha = N_total * sigma_n_2 / P_tx

    # 计算预编码矩阵
    H_HH = H @ H_H
    # 添加正则化项
    G = H_HH + alpha * np.eye(N_total)
    W = H_H @ np.linalg.inv(G)

    # 功率归一化
    scale_factor = np.sqrt(P_tx / np.trace(W.conj().T @ W))
    W_rzf = scale_factor * W

    return W_rzf

为了直观对比ZF和RZF的性能差异,我们可以计算在不同SNR下的系统和速率(Sum Rate)。这需要模拟发送随机数据符号,并通过带有噪声的信道。

def calculate_sum_rate(H, W, snr_db):
    """
    计算给定预编码矩阵下的香农和速率。

    参数:
    H: 信道矩阵
    W: 预编码矩阵
    snr_db: 信噪比(dB)

    返回:
    sum_rate: 和速率(bps/Hz)
    """
    N_total, M = H.shape
    # 计算等效信道和噪声增强后的等效噪声协方差
    H_eq = H @ W  # 形状 (N_total, N_total)

    # 假设每个数据流独立,功率为1,总发射功率已包含在W的归一化中
    # 接收端每个流上的干扰加噪声功率
    sigma_n_2 = 10 ** (-snr_db / 10.0)

    sum_rate = 0
    for k in range(N_total):
        # 第k个流的期望信号功率
        signal_power = np.abs(H_eq[k, k]) ** 2
        # 第k个流受到的来自其他流的干扰功率
        interference_power = np.sum(np.abs(H_eq[k, :]) ** 2) - signal_power
        # 计算该流的SINR
        sinr_k = signal_power / (interference_power + sigma_n_2)
        # 累加该流的速率
        sum_rate += np.log2(1 + sinr_k)

    return sum_rate

2.3 块对角化(BD)预编码:多用户干扰的零空间方法

BD预编码适用于每个用户有多根天线(N_k > 1)的场景,其目标不仅消除用户间干扰(Inter-User Interference, IUI),还要消除用户内各数据流间的干扰。核心思想是为每个用户计算其他所有用户信道张成空间的零空间。

实现BD预编码分为两步:

  1. 计算干扰消除矩阵:对于用户k,找到其信道 H_k 在其他用户联合信道 H_tilde_k 的零空间中的投影矩阵 W_k_a。这通过计算 H_tilde_k 的SVD并取其右奇异向量矩阵中对应零奇异值的部分(即零空间基)来完成。
  2. 计算用户内预编码:在消除了用户间干扰的等效信道 H_k_eff = H_k @ W_k_a 上,再进行一次单用户MIMO的预编码(如SVD预编码),得到 W_k_b。最终预编码矩阵为 W_k = W_k_a @ W_k_b

实现细节颇多,下面是关键部分的代码:

def block_diagonalization_precoding(H_list, P_tx=1.0):
    """
    计算块对角化预编码矩阵。

    参数:
    H_list: 列表,包含K个用户的信道矩阵 H_k,每个形状为 (N_k, M)
    P_tx: 总发射功率

    返回:
    W_bd: 块对角化预编码矩阵,形状 (M, N_total)
    """
    K = len(H_list)
    M = H_list[0].shape[1]  # 基站天线数
    N_k_list = [H_k.shape[0] for H_k in H_list]
    N_total = sum(N_k_list)

    W_list = []  # 存储每个用户的预编码矩阵 W_k

    for k in range(K):
        # 步骤1: 构造其他所有用户的联合信道矩阵 H_tilde_k
        H_tilde_k_list = [H_list[j] for j in range(K) if j != k]
        if H_tilde_k_list:
            H_tilde_k = np.vstack(H_tilde_k_list)  # 形状 (N_total - N_k, M)
        else:
            # 如果只有一个用户,则不存在用户间干扰,直接进行单用户预编码
            H_tilde_k = np.array([]).reshape(0, M)

        # 计算 H_tilde_k 的零空间
        if H_tilde_k.size > 0:
            # 对 H_tilde_k 进行SVD: H_tilde_k = U_t S_t V_t^H
            U_t, S_t, Vh_t = np.linalg.svd(H_tilde_k, full_matrices=True)
            # V_t 是 Vh_t 的共轭转置
            V_t = Vh_t.conj().T
            # 零空间由 V_t 中对应奇异值为0的列向量张成
            # 由于数值计算,奇异值可能非常小但不精确为0,我们设置一个阈值
            rank_t = np.sum(S_t > 1e-10)
            if rank_t < V_t.shape[1]:
                V_t_null = V_t[:, rank_t:]  # 零空间基,形状 (M, M - rank_t)
            else:
                # 如果没有零空间(理论上不应发生,因为M > N_total - N_k是BD可行的条件)
                raise ValueError(f"用户 {k} 的干扰信道矩阵满秩,无法找到零空间。")
        else:
            # 没有其他用户,零空间是整个 C^M 空间
            V_t_null = np.eye(M)

        # 干扰消除矩阵 W_k_a 就是零空间基
        W_k_a = V_t_null  # 形状 (M, M - rank_t)

        # 步骤2: 计算用户k在零空间上的等效信道
        H_k_eff = H_list[k] @ W_k_a  # 形状 (N_k, M - rank_t)

        # 对等效信道进行SVD预编码(单用户)
        U_k, S_k, Vh_k = np.linalg.svd(H_k_eff, full_matrices=False)
        V_k = Vh_k.conj().T
        # 取前 N_k 个右奇异向量(对应主要信道方向)
        # 注意:这里假设 N_k <= M - rank_t,即等效信道有足够自由度支持所有数据流
        W_k_b = V_k[:, :N_k_list[k]]  # 形状 (M - rank_t, N_k)

        # 用户k的最终预编码矩阵
        W_k = W_k_a @ W_k_b  # 形状 (M, N_k)
        W_list.append(W_k)

    # 将所有用户的预编码矩阵水平拼接
    W_bd = np.hstack(W_list)  # 形状 (M, N_total)

    # 功率归一化
    scale_factor = np.sqrt(P_tx / np.trace(W_bd.conj().T @ W_bd))
    W_bd = scale_factor * W_bd

    return W_bd

BD预编码的计算复杂度明显高于ZF,因为它需要对每个用户进行SVD分解。但其优势在于能更好地处理用户内流间干扰,尤其在用户天线数较多时。

3. 性能评估与MATLAB交叉验证

算法实现后,我们必须验证其正确性。一个强有力的方法是与业界公认的仿真工具(如MATLAB)的结果进行交叉验证。虽然我们不能直接运行MATLAB代码,但可以遵循一致的仿真假设,并对比关键指标。

验证策略:

  1. 固定随机种子:在Python和MATLAB中使用相同的随机种子生成信道矩阵 H,确保输入一致。
  2. 对比预编码矩阵本身:计算两种语言得到的预编码矩阵 W 的Frobenius范数差异:diff = norm(W_python - W_matlab, 'fro') / norm(W_matlab, 'fro')。这个相对误差应该非常小(例如 < 1e-10)。
  3. 对比系统性能指标:计算和速率、误码率(BER)等指标。由于这些指标是标量,更容易比对。

为了模拟这个过程,我们可以用Python生成一个信道,用我们的函数计算预编码矩阵,然后“假设”我们从MATLAB得到了一个结果文件,并进行比对。在实际工作中,你可能需要将Python生成的信道数据保存为 .mat 文件供MATLAB读取,或者反之。

def validate_with_matlab_simulation():
    """
    模拟与MATLAB结果的交叉验证流程。
    在实际操作中,需要将数据在Python和MATLAB间导入导出。
    """
    # 使用固定种子生成可复现的信道
    seed = 2024
    M, K, N_k = 32, 4, 2
    H_list, H = generate_mu_mimo_channel(M, K, N_k, seed=seed)

    # Python计算
    W_zf_py = zero_forcing_precoding(H, P_tx=1.0, method='pinv')
    W_bd_py = block_diagonalization_precoding(H_list, P_tx=1.0)

    # 假设我们从MATLAB加载了结果(这里用随机数模拟,实际应从文件加载)
    np.random.seed(seed+1)  # 使用不同的种子模拟“MATLAB计算”
    # 注意:这只是模拟!真实情况下应加载实际MATLAB计算结果
    W_zf_ml_simulated = zero_forcing_precoding(H, P_tx=1.0, method='direct')  # 模拟MATLAB可能用直接求逆
    W_bd_ml_simulated = block_diagonalization_precoding(H_list, P_tx=1.0)  # 模拟MATLAB的BD实现

    # 计算差异
    diff_zf = np.linalg.norm(W_zf_py - W_zf_ml_simulated, 'fro') / np.linalg.norm(W_zf_ml_simulated, 'fro')
    diff_bd = np.linalg.norm(W_bd_py - W_bd_ml_simulated, 'fro') / np.linalg.norm(W_bd_ml_simulated, 'fro')

    print(f"模拟验证结果(相对Frobenius范数误差):")
    print(f"  ZF预编码矩阵差异: {diff_zf:.2e}")
    print(f"  BD预编码矩阵差异: {diff_bd:.2e}")
    print("\n注意:以上是模拟流程。真实交叉验证需确保两端算法、归一化方式完全一致。")

    # 更实际的验证:比较性能指标,如不同SNR下的和速率曲线
    snr_range_db = np.arange(0, 31, 5)
    sum_rate_zf_py = []
    sum_rate_bd_py = []

    for snr_db in snr_range_db:
        # 为公平比较,每次重新计算预编码矩阵(因为RZF的alpha与SNR有关)
        W_zf = zero_forcing_precoding(H, P_tx=1.0)
        W_rzf = regularized_ze_precoding(H, P_tx=1.0, snr_db=snr_db)
        W_bd = block_diagonalization_precoding(H_list, P_tx=1.0)

        sum_rate_zf_py.append(calculate_sum_rate(H, W_zf, snr_db))
        # 使用RZF作为对比
        sum_rate_rzf_py.append(calculate_sum_rate(H, W_rzf, snr_db))
        sum_rate_bd_py.append(calculate_sum_rate(H, W_bd, snr_db))

    # 绘制性能曲线
    plt.figure(figsize=(10, 6))
    plt.plot(snr_range_db, sum_rate_zf_py, 'o-', label='ZF (Python)')
    plt.plot(snr_range_db, sum_rate_rzf_py, 's-', label='RZF/MMSE (Python)')
    plt.plot(snr_range_db, sum_rate_bd_py, '^-', label='BD (Python)')
    plt.xlabel('SNR (dB)')
    plt.ylabel('Sum Rate (bps/Hz)')
    plt.title('大规模MIMO下行链路预编码算法性能比较')
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.legend()
    plt.show()

# 运行验证
validate_with_matlab_simulation()

通过绘制和速率随SNR变化的曲线,我们可以直观地看到不同预编码算法的性能趋势:ZF在低SNR时性能较差,RZF在整个SNR范围内更稳健,而BD在用户天线数较多时能提供额外的性能增益。

4. 工程实践:模块化、可视化与性能分析

将代码模块化并封装成类,可以极大提升仿真项目的可维护性和复用性。下面展示一个预编码仿真器的简单类结构,并介绍如何添加关键的可视化功能。

class MassiveMIMOPrecodingSimulator:
    """
    大规模MIMO预编码仿真器类。
    """
    def __init__(self, M, K, N_k, P_tx=1.0, channel_seed=None):
        self.M = M  # 基站天线数
        self.K = K  # 用户数
        self.N_k = N_k  # 每用户天线数(假设相同)
        self.N_total = K * N_k
        self.P_tx = P_tx
        self.channel_seed = channel_seed

        # 生成信道
        self.H_list, self.H = generate_mu_mimo_channel(M, K, N_k, seed=channel_seed)
        self.W = None  # 当前预编码矩阵
        self.current_method = None

    def compute_precoding(self, method='zf', **kwargs):
        """计算指定方法的预编码矩阵。"""
        if method == 'zf':
            self.W = zero_forcing_precoding(self.H, self.P_tx, **kwargs)
            self.current_method = 'ZF'
        elif method == 'rzf' or method == 'mmse':
            snr_db = kwargs.get('snr_db', 20)
            self.W = regularized_ze_precoding(self.H, self.P_tx, snr_db)
            self.current_method = 'RZF/MMSE'
        elif method == 'bd':
            self.W = block_diagonalization_precoding(self.H_list, self.P_tx)
            self.current_method = 'BD'
        else:
            raise ValueError(f"不支持的预编码方法: {method}")
        return self.W

    def plot_equivalent_channel(self):
        """绘制等效信道矩阵 H_eq = H @ W 的幅度热图。"""
        if self.W is None:
            raise ValueError("请先计算预编码矩阵。")
        H_eq = self.H @ self.W
        fig, axes = plt.subplots(1, 2, figsize=(12, 5))

        im0 = axes[0].imshow(np.abs(H_eq), cmap='hot', interpolation='nearest')
        axes[0].set_title(f'等效信道幅度 |H_eq| ({self.current_method})')
        axes[0].set_xlabel('数据流索引')
        axes[0].set_ylabel('接收天线/用户索引')
        plt.colorbar(im0, ax=axes[0])

        # 突出显示对角线元素(期望信号)
        diag_mask = np.eye(H_eq.shape[0], dtype=bool)
        off_diag_mask = ~diag_mask
        axes[1].hist(np.abs(H_eq[off_diag_mask]).flatten(), bins=50, alpha=0.7, label='非对角 (干扰)')
        axes[1].hist(np.abs(H_eq[diag_mask]).flatten(), bins=50, alpha=0.7, label='对角 (信号)')
        axes[1].set_xlabel('幅度')
        axes[1].set_ylabel('频数')
        axes[1].set_title('等效信道元素幅度分布')
        axes[1].legend()
        axes[1].grid(True, linestyle='--', alpha=0.5)

        plt.tight_layout()
        plt.show()

    def evaluate_sum_rate_vs_snr(self, snr_range_db=np.arange(-5, 31, 2)):
        """评估和速率随SNR的变化。"""
        sum_rates = []
        for snr_db in snr_range_db:
            # 对于RZF,需要根据当前SNR重新计算预编码矩阵
            if self.current_method == 'RZF/MMSE':
                W_temp = regularized_ze_precoding(self.H, self.P_tx, snr_db)
            else:
                W_temp = self.W  # ZF和BD的W与SNR无关(理想功率分配下)
            rate = calculate_sum_rate(self.H, W_temp, snr_db)
            sum_rates.append(rate)

        plt.figure(figsize=(8, 5))
        plt.plot(snr_range_db, sum_rates, 'b-o', linewidth=2, markersize=6)
        plt.xlabel('SNR (dB)')
        plt.ylabel('Sum Rate (bps/Hz)')
        plt.title(f'和速率 vs SNR ({self.current_method}预编码, M={self.M}, K={self.K})')
        plt.grid(True, linestyle='--', alpha=0.7)
        plt.show()
        return snr_range_db, sum_rates

使用这个类,我们可以方便地进行一系列实验:

# 初始化仿真器
sim = MassiveMIMOPrecodingSimulator(M=64, K=8, N_k=2, channel_seed=42)

# 计算并可视化ZF预编码
sim.compute_precoding('zf')
sim.plot_equivalent_channel()
snr_range, rates_zf = sim.evaluate_sum_rate_vs_snr()

# 计算并可视化BD预编码
sim.compute_precoding('bd')
sim.plot_equivalent_channel()
snr_range, rates_bd = sim.evaluate_sum_rate_vs_snr()

从等效信道热图中,你可以清晰地看到BD预编码产生的块对角化结构——每个用户对应的子块内部有值,而用户间的非对角块元素幅度接近零,这直观地展示了用户间干扰的消除。

在实现和测试过程中,有几个坑值得特别注意。首先是数值稳定性,大规模MIMO中矩阵条件数可能很大,直接求逆 np.linalg.inv 容易产生数值误差,优先使用 np.linalg.pinv 或添加微小正则项。其次是功率归一化的具体形式,不同的文献可能采用不同的归一化准则(如每天线功率约束、总功率约束、每流功率约束),务必明确你的仿真假设并与对比对象保持一致。最后是信道模型的假设,本文使用的是最简单的i.i.d.瑞利衰落,实际中可能需要考虑空间相关性、路径损耗和阴影衰落,这些都会显著影响预编码算法的性能排序。

将这些代码片段整合到你的Jupyter Notebook中,你就拥有了一套从信道生成、算法实现、性能评估到结果可视化的完整大规模MIMO预编码仿真工具链。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值