Four WCDMA Carriers — PSD, band_power, AccTrace¶
A multi-carrier monitoring scene: four WCDMA-like downlink carriers — QPSK at the 3.84 Mcps chip rate, one per 5 MHz channel — at deliberately different power levels (0, -3, -6, -10 dBFS), analysed end-to-end with the spectral-measurement suite. It answers the everyday spectrum-monitor question: is every carrier at the right power, and how clean is the channel?
What you're seeing¶
Top-left — the averaged PSD. PSD (Kaiser window, mean trace, 96 frames)
resolves four flat-topped ~5 MHz channels with the sharp root-raised-cosine
skirts of a real WCDMA signal. Each channel is shaded; the measured per-channel
power and the noise_floor() line are annotated.
Top-right — trace averaging with AccTrace. The same power frames folded
three ways: one raw periodogram (grey, ±10 dB of variance), the AccTrace mean
(the variance collapses), and the AccTrace max-hold envelope (green). PSD
is this pipeline — window → FFT → power → AccTrace — so the panel is a peek
under its hood.
Bottom-left — per-channel power. PSD.band_power(edges) recovers the
programmed 0 / -3 / -6 / -10 dB spacing exactly (shown relative to the strongest
carrier); total_band_power gives the whole occupied span.
Bottom-right — the measurements. Per-channel in-band SNR (snr), the global
occupied bandwidth (occupied_bw, 99 %), the noise floor, the adjacent-channel
leakage ratio (ACLR, the strongest carrier vs. the empty guard channel beside
it), and the PSD resolution bandwidth.
Building it¶
The carriers come from doppler's own waveform generator. WCDMA downlink is
noise-like QPSK with root-raised-cosine pulse shaping (β = 0.22, the WCDMA
roll-off), and the generator does exactly that — qpsk(pulse="rrc")
(doppler#115) band-limits
the chips in the C engine at unit transmit power, so each carrier is a single
generator call; freq= places it at the channel centre:
import numpy as np
from doppler.spectral import PSD
from doppler.wfm import Composer, Segment, noise, qpsk
FS, SPS = 30.72e6, 8 # 8 samples / 3.84 Mcps chip
def rrc_carrier(fc, level, seed, n):
"""One RRC-shaped WCDMA carrier at offset ``fc``, scaled to ``level`` dBFS.
Straight from doppler's waveform generator: ``qpsk(pulse="rrc")``
band-limits the QPSK chips with a root-raised-cosine pulse (0.22 is
the WCDMA roll-off) — the shaping the default sample-and-hold QPSK
does not do — and ``freq=fc`` mixes the carrier to its channel
centre, all in the C engine. The RRC taps are unit-transmit-power
scaled, so the carrier is already unit power and the ``level``
factor lets ``band_power`` read the level directly.
"""
sig = (
qpsk(
sps=SPS,
pulse="rrc",
rrc_beta=0.22,
rrc_span=8,
freq=fc,
fs=FS,
seed=seed,
)
.steps(n)
.astype(np.complex64)
)
return sig * 10.0 ** (level / 20.0) # unit power -> level (dBFS)
Sum four of them at 5 MHz spacing over a composed AWGN floor, then measure with
PSD:
NFFT, N_FRAMES = 4096, 96 # PSD frame size x frames to average
CARRIERS = [ # (channel centre Hz, level dBFS)
(-7.5e6, 0.0),
(-2.5e6, -3.0),
(2.5e6, -6.0),
(7.5e6, -10.0),
]
# Sum the four carriers into one scene, over a composed AWGN floor at
# -70 dBFS (well below the weakest carrier) so noise_floor()/snr() have
# a real floor to measure. The noise synth shares the generator's
# 0 dBFS = unit-power reference.
n = NFFT * N_FRAMES
scene = np.zeros(n, dtype=np.complex64)
for i, (fc, lvl) in enumerate(CARRIERS):
scene += rrc_carrier(fc, lvl, 10 + i, n)
floor = Composer(
Segment.sum(noise(level=-70.0), num_samples=n, fs=FS)
).compose()
scene = (scene + floor[:n]).astype(np.complex64)
w = PSD(n=NFFT, fs=FS, window="kaiser", beta=12.0, mode="mean")
w.accumulate(scene) # folds all 96 frames into the average
edges = np.array( # [lo0, hi0, lo1, hi1, ...] channel edges, Hz
[-10e6, -5e6, -5e6, 0, 0, 5e6, 5e6, 10e6], dtype=np.float64
)
band_db = np.array(w.band_power(edges)) # per-channel power, dB
total_db = w.total_band_power(edges) # whole occupied span, dB
nf = w.noise_floor() # median dB level
snr0 = w.snr(-10e6, -5e6) # in-channel SNR of carrier 0
Snapshot zero-copy results
psd_db() and band_power() return zero-copy views into PSD's
internal buffers (the library's variable-output idiom). Wrap a result in
np.array(...) if you need it to survive a later call to the same method —
e.g. the ACLR band_power(guard) call would otherwise overwrite an earlier
band_power(edges) view.
See also¶
- PSD / AccTrace API — the measurement methods.
- Accumulator API —
AccTracemodes. - Composing a Scene — the waveform generator.
src/doppler/examples/wcdma_carriers_demo.py— the script behind this figure.
