import numpy as np
import pytest
import orion_sdr as sdr
FS = 240_000.0
def _mean_band_power_db(iq: np.ndarray, lo_norm: float, hi_norm: float) -> float:
n = len(iq)
x = 2.0 * np.pi * np.arange(n) / n
w = (
0.35875
- 0.48829 * np.cos(x)
+ 0.14128 * np.cos(2.0 * x)
- 0.01168 * np.cos(3.0 * x)
)
spec = np.fft.fft(np.asarray(iq, dtype=np.complex128) * w)
freq = np.abs(np.fft.fftfreq(n)) sel = (freq >= lo_norm) & (freq <= hi_norm)
assert sel.any(), f"empty measurement band [{lo_norm}, {hi_norm}]"
return float(10.0 * np.log10((np.abs(spec[sel]) ** 2).mean() + 1e-30))
def _sample_payload(n: int) -> np.ndarray:
return np.frombuffer(bytes((i * 37 + 11) & 0xFF for i in range(n)), dtype=np.uint8)
def _cofdm_config(n_fft=256, cp_len=64, edge_guard=31, constellation="qpsk"):
return sdr.OfdmConfig(
n_fft,
cp_len,
np.zeros(0, dtype=np.int32),
np.zeros(0, dtype=np.int32),
np.zeros(0, dtype=np.complex64),
FS,
0.0,
1.0,
constellation,
edge_guard=edge_guard,
)
class TestCofdmSizing:
def test_edge_guard_narrows_the_occupied_band(self):
assert _cofdm_config(edge_guard=0).occupied_half_carriers == 127
assert _cofdm_config(edge_guard=31).occupied_half_carriers == 96
def test_edge_guard_rejects_an_explicit_carrier_list(self):
with pytest.raises(ValueError):
sdr.OfdmConfig(
64,
8,
np.array([1, 2, 3], dtype=np.int32),
np.zeros(0, dtype=np.int32),
np.zeros(0, dtype=np.complex64),
FS,
0.0,
1.0,
"qpsk",
edge_guard=4,
)
def test_suggested_taps_track_the_room_available(self):
narrow = _cofdm_config(edge_guard=8).tx_lowpass_suggested_taps(60.0)
wide = _cofdm_config(edge_guard=48).tx_lowpass_suggested_taps(60.0)
assert wide < narrow
shallow = _cofdm_config(edge_guard=31).tx_lowpass_suggested_taps(40.0)
deep = _cofdm_config(edge_guard=31).tx_lowpass_suggested_taps(80.0)
assert shallow < deep
def test_group_delay_is_the_odd_clamped_half_length(self):
cfg = _cofdm_config()
assert cfg.tx_lowpass_group_delay(65) == 32
assert cfg.tx_lowpass_group_delay(64) == cfg.tx_lowpass_group_delay(65)
def test_fits_guard_is_the_shared_budget(self):
cfg = _cofdm_config(cp_len=64)
assert cfg.tx_lowpass_fits_guard(65) assert not cfg.tx_lowpass_fits_guard(65, 32) assert cfg.tx_lowpass_fits_guard(33, 16)
def test_backoff_zero_leaves_no_slack_at_all(self):
cfg = _cofdm_config(cp_len=64)
assert not cfg.tx_lowpass_fits_guard(3, 0, backoff=0)
def _frame_iq(cfg, payload, *, repeat_len=32):
table = sdr.McsTable.default_ladder()
mod = sdr.OfdmFrameMod(cfg, table, num_repeats=4, repeat_len=repeat_len)
return mod.modulate_frame(sdr.FramePacket(payload, sequence_num=0xC1, mcs_index=1))
def _cofdm_frame_config(constellation="bpsk"):
return _cofdm_config(
n_fft=128, cp_len=32, edge_guard=20, constellation=constellation
)
class TestCofdmShapingDefaults:
def test_all_three_levers_default_off(self):
cfg = _cofdm_frame_config()
payload = _sample_payload(48)
assert np.array_equal(_frame_iq(cfg, payload), _frame_iq(cfg, payload))
def test_each_lever_changes_the_waveform(self):
cfg = _cofdm_frame_config()
payload = _sample_payload(48)
plain = _frame_iq(cfg, payload)
windowed = _frame_iq(cfg.with_symbol_window(6), payload)
masked = _frame_iq(cfg.with_tx_lowpass(15, 40.0), payload)
assert len(windowed) == len(plain), "the taper is a same-length post-pass"
assert len(masked) == len(plain), "the mask is a same-length post-pass"
assert not np.array_equal(windowed, plain)
assert not np.array_equal(masked, plain)
assert not np.array_equal(windowed, masked)
class TestCofdmShapingRoundtrip:
@pytest.mark.parametrize(
"roll_off,taps",
[
(0, 21), (6, 15), (16, 0), ],
)
def test_shaped_frame_still_decodes(self, roll_off, taps):
cp_len = 32
backoff = cp_len // 2
cfg = (
_cofdm_frame_config()
.with_inner_fec("ldpc", "n512r12")
.with_outer_fec("bch", 8)
.with_payload_crc("crc32")
.with_header_crc("crc16")
.with_rx_window_backoff(backoff)
)
if roll_off:
cfg = cfg.with_symbol_window(roll_off)
if taps:
assert cfg.tx_lowpass_fits_guard(taps, roll_off, backoff)
cfg = cfg.with_tx_lowpass(taps, 40.0)
payload = _sample_payload(48)
iq = _frame_iq(cfg, payload)
buf = np.concatenate(
[
np.zeros(40, dtype=np.complex64),
iq,
np.zeros(128, dtype=np.complex64),
]
)
rx = sdr.OfdmFrameStreamDemod(
cfg, sdr.McsTable.default_ladder(), num_repeats=4, repeat_len=32
)
frames = rx.feed(buf)
assert len(frames) == 1, "the shaped frame must still acquire and decode"
assert np.array_equal(frames[0].payload, payload)
_SPEC_TAPS, _SPEC_ROLL_OFF = 45, 8
_STOP_BAND = (0.47, 0.5)
def _spectrum_config(edge_guard=31):
return _cofdm_config(n_fft=256, cp_len=64, edge_guard=edge_guard)
def _stop_band_db(cfg, payload):
sps = cfg.samples_per_ofdm_symbol
body = _frame_iq(cfg, payload)[4 * sps : 4 * sps + 4096]
assert len(body) == 4096, "need a full analysis block"
return _mean_band_power_db(body, *_STOP_BAND)
class TestCofdmSpectrum:
def test_mask_drops_the_stop_band_below_the_taper_floor(self):
cfg = _spectrum_config()
assert cfg.occupied_half_carriers == 96 assert cfg.tx_lowpass_fits_guard(_SPEC_TAPS, _SPEC_ROLL_OFF, 32)
payload = _sample_payload(256)
power = {
name: _stop_band_db(c, payload)
for name, c in {
"baseline": cfg,
"taper": cfg.with_symbol_window(_SPEC_ROLL_OFF),
"mask": cfg.with_tx_lowpass(_SPEC_TAPS, 60.0),
"both": cfg.with_symbol_window(_SPEC_ROLL_OFF).with_tx_lowpass(
_SPEC_TAPS, 60.0
),
}.items()
}
assert power["taper"] < power["baseline"] - 5.0, power
assert power["mask"] < power["taper"] - 25.0, power
assert power["both"] < power["mask"] - 4.0, power
def test_edge_guard_alone_lowers_the_skirt(self):
payload = _sample_payload(256)
guarded = _stop_band_db(_spectrum_config(edge_guard=31), payload)
unguarded = _stop_band_db(_spectrum_config(edge_guard=0), payload)
assert guarded < unguarded - 15.0, (guarded, unguarded)
def test_shaping_leaves_in_band_power_alone(self):
cfg = _spectrum_config()
payload = _sample_payload(256)
sps = cfg.samples_per_ofdm_symbol
def in_band(c):
body = _frame_iq(c, payload)[4 * sps : 4 * sps + 4096]
return _mean_band_power_db(body, 0.0, 0.36)
base = in_band(cfg)
shaped = in_band(
cfg.with_symbol_window(_SPEC_ROLL_OFF).with_tx_lowpass(_SPEC_TAPS, 60.0)
)
assert abs(base - shaped) < 0.5, (base, shaped)
class TestDvbTSizing:
@pytest.mark.parametrize(
"guard,cp_len", [("1/32", 64), ("1/16", 128), ("1/8", 256), ("1/4", 512)]
)
def test_cp_len_per_guard(self, guard, cp_len):
assert sdr.dvb_t_cp_len(guard) == cp_len
def test_cp_len_rejects_unknown_guard(self):
with pytest.raises(ValueError):
sdr.dvb_t_cp_len("1/64")
def test_backoff_ceiling_is_the_pilot_grid(self):
assert sdr.dvb_t_max_rx_window_backoff() == 85
def test_shaping_slack_saturates_at_g1_8(self):
cap = sdr.dvb_t_max_rx_window_backoff()
slack = {}
for guard in ("1/32", "1/16", "1/8", "1/4"):
cp_len = sdr.dvb_t_cp_len(guard)
b = min(cp_len // 2, cap)
slack[guard] = min(cp_len - b, b)
assert slack == {"1/32": 32, "1/16": 64, "1/8": 85, "1/4": 85}
def test_suggested_taps_and_group_delay(self):
taps = sdr.dvb_t_tx_lowpass_suggested_taps(60.0)
assert taps % 2 == 1, "a linear-phase FIR is forced odd"
assert sdr.dvb_t_tx_lowpass_group_delay(taps) == (taps - 1) // 2
assert sdr.dvb_t_tx_lowpass_suggested_taps(80.0) > taps
def test_fits_guard_matches_the_budget(self):
assert sdr.dvb_t_tx_lowpass_group_delay(89) == 44
assert sdr.dvb_t_tx_lowpass_fits_guard("1/8", 89, 16, 64)
assert not sdr.dvb_t_tx_lowpass_fits_guard("1/32", 89, 16, 32)
def _place(iq: np.ndarray, sps: int, lead: int = 200) -> np.ndarray:
return np.concatenate(
[
np.zeros(lead, dtype=np.complex64),
iq,
np.zeros(sps, dtype=np.complex64),
]
)
class TestDvbTShaping:
def test_both_levers_default_off(self):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
a = sdr.DvbTFrameMod(p).modulate(payload).iq
b = sdr.DvbTFrameMod(p).modulate(payload).iq
np.testing.assert_array_equal(a, b)
taper = sdr.DvbTFrameMod(p).with_symbol_window(16).modulate(payload).iq
mask = sdr.DvbTFrameMod(p).with_tx_lowpass(89, 60.0).modulate(payload).iq
assert len(taper) == len(a) and len(mask) == len(a)
assert not np.array_equal(taper, a)
assert not np.array_equal(mask, a)
def test_backoff_defaults_to_zero_and_reads_back(self):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
assert sdr.DvbTFrameDemod(p).rx_window_backoff == 0
assert sdr.DvbTFrameDemod(p).with_rx_window_backoff(64).rx_window_backoff == 64
@pytest.mark.parametrize(
"guard,backoff,roll_off,taps,lead",
[
("1/32", 32, 0, 45, 200), ("1/8", 32, 8, 45, 200), ("1/8", 32, 24, 0, 200), ("1/8", 32, 24, 0, 0),
("1/8", 32, 8, 45, 0),
],
)
def test_shaped_frame_round_trips(self, guard, backoff, roll_off, taps, lead):
p = sdr.DvbTFrameParams(guard, "qpsk", "1/2", frame_number=1, cell_id=0x33)
assert backoff <= sdr.dvb_t_max_rx_window_backoff()
mod = sdr.DvbTFrameMod(p)
if roll_off:
mod = mod.with_symbol_window(roll_off)
if taps:
assert sdr.dvb_t_tx_lowpass_fits_guard(guard, taps, roll_off, backoff)
mod = mod.with_tx_lowpass(taps, 60.0)
payload = _sample_payload(184)
frame = mod.modulate(payload)
buf = _place(frame.iq, frame.samples_per_symbol, lead=lead)
rx = (
sdr.DvbTFrameDemod(p)
.with_rx_window_backoff(backoff)
.decode(buf, frame.n_symbols, len(payload))
)
assert rx.payload == payload.tobytes()
assert rx.tps.guard == guard
assert rx.tps.frame_number == 1
assert rx.tps.cell_id == 0x33
def test_backoff_past_the_pilot_ceiling_fails(self):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
frame = sdr.DvbTFrameMod(p).modulate(payload)
buf = _place(frame.iq, frame.samples_per_symbol)
ok = (
sdr.DvbTFrameDemod(p)
.with_rx_window_backoff(64)
.decode(buf, frame.n_symbols, len(payload))
)
assert ok.payload == payload.tobytes()
with pytest.raises(ValueError):
sdr.DvbTFrameDemod(p).with_rx_window_backoff(128).decode(
buf, frame.n_symbols, len(payload)
)
def test_mask_attenuates_the_null_band(self):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
plain = sdr.DvbTFrameMod(p).modulate(payload)
masked = sdr.DvbTFrameMod(p).with_tx_lowpass(89, 60.0).modulate(payload)
sps = plain.samples_per_symbol
a = plain.iq[2 * sps : 2 * sps + 8192]
b = masked.iq[2 * sps : 2 * sps + 8192]
null_a = _mean_band_power_db(a, 0.47, 0.5)
null_b = _mean_band_power_db(b, 0.47, 0.5)
assert null_b < null_a - 30.0, (null_a, null_b)
in_a = _mean_band_power_db(a, 0.0, 0.40)
in_b = _mean_band_power_db(b, 0.0, 0.40)
assert abs(in_a - in_b) < 0.5, "in-band power must be untouched"
class TestDvbTSuperFrameShaping:
@pytest.mark.parametrize(
"roll_off,taps,stopband_db",
[
(0, 45, 60.0),
(0, 89, 40.0), (0, 89, 80.0),
(16, 0, 60.0), (8, 45, 60.0), ],
)
def test_shaped_super_frame_round_trips(self, roll_off, taps, stopband_db):
p = sdr.DvbTSuperFrameParams("1/8", "qpsk", "1/2", cell_id=0xBEEF)
payload = _sample_payload(700)
mod = sdr.DvbTSuperFrameMod(p)
if roll_off:
mod = mod.with_symbol_window(roll_off)
if taps:
mod = mod.with_tx_lowpass(taps, stopband_db)
sf = mod.modulate(payload)
assert len(sf.iq) == sf.n_symbols * sf.samples_per_symbol
rx = (
sdr.DvbTSuperFrameDemod(p)
.with_rx_window_backoff(32)
.decode(sf.iq, sf.symbols_per_frame, sf.frame_payload_lens)
)
assert rx.payload == payload.tobytes()
assert rx.cell_id == 0xBEEF
def test_mask_runs_across_the_frame_seams(self):
p = sdr.DvbTSuperFrameParams("1/8", "qpsk", "1/2", cell_id=0x0102)
payload = _sample_payload(700)
taps = 45
whole = sdr.DvbTSuperFrameMod(p).with_tx_lowpass(taps, 60.0).modulate(payload).iq
plain = sdr.DvbTSuperFrameMod(p).modulate(payload).iq
seam = len(whole) // 4
d = sdr.dvb_t_tx_lowpass_group_delay(taps)
assert (
np.abs(whole[seam - d : seam + d] - plain[seam - d : seam + d]).max() > 1e-6
)
class TestDvbTStreamShaping:
@pytest.mark.parametrize("roll_off,taps", [(0, 45), (16, 0), (8, 45)])
def test_streaming_receiver_takes_a_back_off(self, roll_off, taps):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
mod = sdr.DvbTFrameMod(p)
if roll_off:
mod = mod.with_symbol_window(roll_off)
if taps:
mod = mod.with_tx_lowpass(taps, 60.0)
frame = mod.modulate(payload)
buf = _place(frame.iq, frame.samples_per_symbol, lead=101)
rx = sdr.DvbTFrameStreamDemod(
p, frame.n_symbols, len(payload), rx_window_backoff=32
)
assert rx.rx_window_backoff == 32
frames = rx.feed(buf)
assert len(frames) == 1
assert frames[0].payload == payload.tobytes()
def test_stream_default_back_off_is_zero(self):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
rx = sdr.DvbTFrameStreamDemod(p, 68, 184)
assert rx.rx_window_backoff == 0
class TestDvbTBackOffSensitivity:
GUARD, SNR_DB, TRIALS = "1/8", 6.0, 12
def _decode_rate(self, backoff, snr_db=None, seed=0xBEEF):
p = sdr.DvbTFrameParams(self.GUARD, "qpsk", "1/2")
payload = _sample_payload(184)
frame = sdr.DvbTFrameMod(p).modulate(payload)
demod = sdr.DvbTFrameDemod(p).with_rx_window_backoff(backoff)
power = float(np.mean(np.abs(frame.iq) ** 2))
n0 = power / (10.0 ** ((snr_db if snr_db is not None else self.SNR_DB) / 10.0))
rng = np.random.default_rng(seed)
ok = 0
for _ in range(self.TRIALS):
noise = rng.normal(0, np.sqrt(n0 / 2), len(frame.iq)) + 1j * rng.normal(
0, np.sqrt(n0 / 2), len(frame.iq)
)
buf = _place(
(frame.iq + noise).astype(np.complex64), frame.samples_per_symbol
)
try:
rx = demod.decode(buf, frame.n_symbols, len(payload))
ok += rx.payload == payload.tobytes()
except ValueError:
pass
return ok / self.TRIALS
def test_a_free_back_off_decodes(self):
assert self._decode_rate(32) == 1.0
def test_the_aliasing_cap_is_unusable(self):
cap = sdr.dvb_t_max_rx_window_backoff()
assert self._decode_rate(cap, snr_db=15.0) == 0.0
def test_the_cost_is_monotonic_in_the_interpolation_error(self):
assert self._decode_rate(32) >= self._decode_rate(48) >= self._decode_rate(64)
def test_budget_legal_shaping_costs_nothing_extra(self):
p = sdr.DvbTFrameParams(self.GUARD, "qpsk", "1/2")
payload = _sample_payload(184)
shaped = (
sdr.DvbTFrameMod(p)
.with_symbol_window(8)
.with_tx_lowpass(45, 60.0)
.modulate(payload)
)
assert sdr.dvb_t_tx_lowpass_fits_guard(self.GUARD, 45, 8, 32)
demod = sdr.DvbTFrameDemod(p).with_rx_window_backoff(32)
power = float(np.mean(np.abs(shaped.iq) ** 2))
n0 = power / (10.0 ** (self.SNR_DB / 10.0))
rng = np.random.default_rng(0xBEEF)
ok = 0
for _ in range(self.TRIALS):
noise = rng.normal(0, np.sqrt(n0 / 2), len(shaped.iq)) + 1j * rng.normal(
0, np.sqrt(n0 / 2), len(shaped.iq)
)
buf = _place(
(shaped.iq + noise).astype(np.complex64), shaped.samples_per_symbol
)
rx = demod.decode(buf, shaped.n_symbols, len(payload))
ok += rx.payload == payload.tobytes()
assert ok == self.TRIALS
class TestDvbTAcquisitionUnderShaping:
@pytest.mark.parametrize("roll_off,taps", [(0, 0), (8, 0), (32, 0), (0, 45), (8, 45)])
def test_any_shaping_acquires_from_sample_zero(self, roll_off, taps):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
mod = sdr.DvbTFrameMod(p)
if roll_off:
mod = mod.with_symbol_window(roll_off)
if taps:
mod = mod.with_tx_lowpass(taps, 60.0)
frame = mod.modulate(payload)
rx = (
sdr.DvbTFrameDemod(p)
.with_rx_window_backoff(32)
.decode(frame.iq, frame.n_symbols, len(payload))
)
assert rx.payload == payload.tobytes()
@pytest.mark.parametrize("lead", [7, 40, 200, 1000])
def test_a_genuine_lead_in_is_not_collapsed_to_the_origin(self, lead):
p = sdr.DvbTFrameParams("1/8", "qpsk", "1/2")
payload = _sample_payload(184)
frame = sdr.DvbTFrameMod(p).with_symbol_window(16).modulate(payload)
buf = _place(frame.iq, frame.samples_per_symbol, lead=lead)
rx = (
sdr.DvbTFrameDemod(p)
.with_rx_window_backoff(32)
.decode(buf, frame.n_symbols, len(payload))
)
assert rx.payload == payload.tobytes()