07-21-2026, 12:54 PM
Here is a complete, self-contained Python script using numpy and matplotlib
This script builds a frequency-domain G-OFDM matrix (G) using a Root-Raised Cosine (RRC) pulse-shaping profile. It matrices a raw IQ payload across adjacent subcarriers to control out-of-band spectral leakage while spreading signal energy across the subcarrier grid.
Key Elements of the Script
Matrix Precoding (G): Rather than passing the raw IQ array d straight into the IFFT, it gets transformed via matrix multiplication with G
Controlled Subcarrier Overlap: The generate G matrix function uses a Root-Raised Cosine roll-off curve to distribute portions of each symbol's energy into neighboring subcarriers.
Out-of-Band (OOB) Suppression: If you run the code and look at the Matplotlib PSD output, you will see the side lobes drop off much faster on the G-OFDM trace than on a standard rectangular-windowed OFDM signal, preventing spectral bleed.
This script builds a frequency-domain G-OFDM matrix (G) using a Root-Raised Cosine (RRC) pulse-shaping profile. It matrices a raw IQ payload across adjacent subcarriers to control out-of-band spectral leakage while spreading signal energy across the subcarrier grid.
Code:
import numpy as np
import matplotlib.pyplot as plt
def generate_g_matrix(num_subcarriers, roll_off=0.35, span=3):
"""
Generates a Generalized OFDM (G-OFDM) frequency-domain pulse-shaping
precoding matrix G.
Parameters:
num_subcarriers (int): Number of subcarriers (N)
roll_off (float): Roll-off factor alpha (0.0 to 1.0)
span (int): Subcarrier overlap span for pulse shaping
Returns:
numpy.ndarray: N x N complex transformation matrix G
"""
N = num_subcarriers
G = np.zeros((N, N), dtype=complex)
# Generate Root-Raised Cosine (RRC) window profile across subcarriers
for i in range(N):
for j in range(N):
# Calculate distance/offset between subcarriers
diff = np.abs(i - j)
# Wrap around for cyclic subcarrier continuity
if diff > N / 2:
diff = N - diff
if diff == 0:
G[i, j] = 1.0
elif diff <= span:
# Apply frequency-domain RRC roll-off factor
weight = 0.5 * (1 + np.cos(np.pi * diff / (span + 1))) ** roll_off
# Apply phase distribution shift across matrix elements
phase_shift = np.exp(-1j * np.pi * (i - j) / N)
G[i, j] = weight * phase_shift
# Normalize matrix energy to maintain constant TX power
G = G / np.linalg.norm(G, ord=2)
return G
def process_g_ofdm_payload(iq_symbols, G_matrix, cp_len=16):
"""
Processes an IQ payload vector through G-OFDM precoding and IFFT.
Parameters:
iq_symbols (ndarray): Vector of complex IQ payload symbols
G_matrix (ndarray): Precoding matrix G
cp_len (int): Cyclic Prefix length in samples
Returns:
ndarray: Time-domain G-OFDM transmit waveform
"""
N = G_matrix.shape[0]
# 1. Apply Frequency-Domain G-Matrix Precoding
# x_freq = G * d (Spreads payload energy across subcarrier matrix)
precoded_subcarriers = np.dot(G_matrix, iq_symbols)
# 2. IFFT transform to discrete time-domain waveform
time_domain_signal = np.fft.ifft(precoded_subcarriers, n=N)
# 3. Add Cyclic Prefix (CP) for multipath resilience
cyclic_prefix = time_domain_signal[-cp_len:]
g_ofdm_frame = np.concatenate([cyclic_prefix, time_domain_signal])
return g_ofdm_frame, precoded_subcarriers
# =====================================================================
# BENCH TEST RUNNER
# =====================================================================
if __name__ == "__main__":
# System Parameters
N_SUBCARRIERS = 64 # Number of subcarriers
CP_LENGTH = 16 # Cyclic prefix samples
# 1. Generate random QPSK IQ payload (d)
np.random.seed(42) # Fixed seed for repeatable test
raw_bits = np.random.randint(0, 2, N_SUBCARRIERS * 2)
i_bits = 2 * raw_bits[0::2] - 1
q_bits = 2 * raw_bits[1::2] - 1
iq_payload = (i_bits + 1j * q_bits) / np.sqrt(2) # QPSK normalized
# 2. Construct the G-OFDM Matrix (G)
G = generate_g_matrix(N_SUBCARRIERS, roll_off=0.35, span=2)
# 3. Process signal through standard OFDM vs G-OFDM pipelines
# Standard OFDM (G = Identity Matrix)
standard_ofdm_signal, _ = process_g_ofdm_payload(iq_payload, np.eye(N_SUBCARRIERS), CP_LENGTH)
# G-OFDM Matrix Pipeline
g_ofdm_signal, precoded_iq = process_g_ofdm_payload(iq_payload, G, CP_LENGTH)
# 4. Compute Power Spectral Densities (PSD) for comparison
fft_size = 1024
psd_standard = np.abs(np.fft.fftshift(np.fft.fft(standard_ofdm_signal, n=fft_size))) ** 2
psd_g_ofdm = np.abs(np.fft.fftshift(np.fft.fft(g_ofdm_signal, n=fft_size))) ** 2
# Convert to dB
psd_standard_db = 10 * np.log10(psd_standard / np.max(psd_standard))
psd_g_ofdm_db = 10 * np.log10(psd_g_ofdm / np.max(psd_g_ofdm))
freq_axis = np.linspace(-0.5, 0.5, fft_size)
# 5. Output Verification
print(f"=== G-OFDM Matrix Verification ===")
print(f"Matrix Dimension: {G.shape[0]}x{G.shape[1]}")
print(f"Input IQ Payload Shape: {iq_payload.shape}")
print(f"Precoded Subcarrier Payload Shape: {precoded_iq.shape}")
print(f"Final Time-Domain Frame Length (with CP): {len(g_ofdm_signal)} samples")
# Plot Spectral Comparison
plt.figure(figsize=(10, 5))
plt.plot(freq_axis, psd_standard_db, label="Standard OFDM (Rectangular)", alpha=0.6, color="red")
plt.plot(freq_axis, psd_g_ofdm_db, label="G-OFDM (G Matrix Shaped)", linewidth=2, color="blue")
plt.title("Spectral Comparison: Standard OFDM vs. G-OFDM Precoding")
plt.xlabel("Normalized Frequency (f / Fs)")
plt.ylabel("Power Spectral Density (dB)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.ylim(-60, 5)
plt.legend()
plt.tight_layout()
plt.show()Key Elements of the Script
Matrix Precoding (G): Rather than passing the raw IQ array d straight into the IFFT, it gets transformed via matrix multiplication with G
Controlled Subcarrier Overlap: The generate G matrix function uses a Root-Raised Cosine roll-off curve to distribute portions of each symbol's energy into neighboring subcarriers.
Out-of-Band (OOB) Suppression: If you run the code and look at the Matplotlib PSD output, you will see the side lobes drop off much faster on the G-OFDM trace than on a standard rectangular-windowed OFDM signal, preventing spectral bleed.

