Lab045: demonstrate real RF BPSK error correction
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
86
protocol/bit_interleaver.py
Normal file
86
protocol/bit_interleaver.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Детерминированный блочный перемежитель (запись строками, чтение столбцами).
|
||||
|
||||
Перемежитель декоррелирует пачки ошибок канала: соседние по входу элементы
|
||||
после перестановки оказываются разнесены, а пачка подряд идущих ошибок
|
||||
канала после обратной перестановки распадается на разрозненные одиночные
|
||||
ошибки, которые свёрточный код исправляет лучше всего.
|
||||
|
||||
Схема. Для длины ``length`` и числа столбцов ``columns`` элементы условно
|
||||
записываются построчно в матрицу ``rows × columns`` (``rows =
|
||||
ceil(length/columns)``) и считываются по столбцам. Если длина не кратна
|
||||
размеру матрицы, недостающие ячейки последнего ряда просто пропускаются —
|
||||
ни один элемент не теряется и не выдумывается. Результат выражен как
|
||||
перестановка индексов ``permutation`` длиной ``length``: перемежённый
|
||||
поток ``output[k] = input[permutation[k]]``. Обратная операция применяет
|
||||
обратную перестановку и работает как с битами, так и напрямую с soft-
|
||||
значениями BPSK на приёме. Перестановка зависит только от ``length`` и
|
||||
``columns`` и не зависит от передаваемых данных.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def _validated_permutation(permutation: np.ndarray, length: int) -> np.ndarray:
|
||||
"""Проверить, что перестановка содержит каждый допустимый индекс один раз."""
|
||||
|
||||
raw_permutation = np.asarray(permutation)
|
||||
if raw_permutation.ndim != 1:
|
||||
raise ValueError("permutation должен быть одномерным")
|
||||
if len(raw_permutation) != length:
|
||||
raise ValueError("длина values не совпадает с длиной перестановки")
|
||||
if not np.issubdtype(raw_permutation.dtype, np.integer):
|
||||
raise ValueError("permutation должен содержать целые индексы")
|
||||
checked = raw_permutation.astype(np.int64, copy=False)
|
||||
if not np.array_equal(np.sort(checked), np.arange(length, dtype=np.int64)):
|
||||
raise ValueError("permutation не является полной перестановкой индексов")
|
||||
return checked
|
||||
|
||||
|
||||
def build_permutation(length: int, columns: int) -> np.ndarray:
|
||||
"""Построить перестановку чтения по столбцам для блока длиной ``length``.
|
||||
|
||||
Возвращает одномерный массив индексов длиной ``length``: ``output[k] =
|
||||
input[permutation[k]]``. Гарантируется, что каждый индекс 0..length-1
|
||||
встречается ровно один раз.
|
||||
"""
|
||||
|
||||
if not isinstance(length, (int, np.integer)) or length < 1:
|
||||
raise ValueError("length должен быть положительным целым")
|
||||
if not isinstance(columns, (int, np.integer)) or columns < 1:
|
||||
raise ValueError("columns должен быть положительным целым")
|
||||
columns = int(columns)
|
||||
length = int(length)
|
||||
rows = (length + columns - 1) // columns
|
||||
permutation = [
|
||||
row * columns + column
|
||||
for column in range(columns)
|
||||
for row in range(rows)
|
||||
if row * columns + column < length
|
||||
]
|
||||
result = np.asarray(permutation, dtype=np.int64)
|
||||
assert len(result) == length
|
||||
return result
|
||||
|
||||
|
||||
def interleave(values: np.ndarray, permutation: np.ndarray) -> np.ndarray:
|
||||
"""Применить перестановку: ``output[k] = values[permutation[k]]``."""
|
||||
|
||||
values = np.asarray(values)
|
||||
if values.ndim != 1:
|
||||
raise ValueError("values должен быть одномерным")
|
||||
checked_permutation = _validated_permutation(permutation, len(values))
|
||||
return values[checked_permutation]
|
||||
|
||||
|
||||
def deinterleave(values: np.ndarray, permutation: np.ndarray) -> np.ndarray:
|
||||
"""Обратить перестановку без потери типа (работает и с soft-значениями)."""
|
||||
|
||||
values = np.asarray(values)
|
||||
if values.ndim != 1:
|
||||
raise ValueError("values должен быть одномерным")
|
||||
checked_permutation = _validated_permutation(permutation, len(values))
|
||||
restored = np.empty_like(values)
|
||||
restored[checked_permutation] = values
|
||||
return restored
|
||||
216
protocol/convolutional_fec.py
Normal file
216
protocol/convolutional_fec.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""Свёрточный код R=1/2, K=7 и soft-decision декодер Витерби.
|
||||
|
||||
Настоящий помехоустойчивый код уровня бит для исправления неизвестных
|
||||
битовых ошибок реального BPSK-канала (Lab044 показал BER ≈0.5–0.6%, при
|
||||
котором незащищённый пакет практически не проходит CRC). В отличие от
|
||||
``protocol/packet_erasure_fec`` (GF(256), только стирания), этот код
|
||||
исправляет случайные битовые ошибки без знания их позиций.
|
||||
|
||||
Параметры зафиксированы и не передаются в каждом кадре:
|
||||
|
||||
- constraint length ``K = 7``; память ``m = 6``; число состояний ``2**6 = 64``;
|
||||
- скорость ``R = 1/2`` (два выходных бита на один входной);
|
||||
- порождающие многочлены ``G0 = 0o171``, ``G1 = 0o133`` (стандарт NASA/Voyager);
|
||||
- начальное состояние регистра ``0``;
|
||||
- терминирование: к информационным битам добавляются ``m = 6`` нулевых
|
||||
хвостовых бита, так что финальное состояние тоже ``0``.
|
||||
|
||||
Соглашение сдвигового регистра. Полный 7-битный регистр для вычисления
|
||||
выхода собирается как ``full = (input_bit << 6) | state``, где ``state`` —
|
||||
6 предыдущих входных битов, самый свежий в бите 5, самый старый в бите 0.
|
||||
Порождающий многочлен применяется как битовая маска этого 7-битного
|
||||
регистра, выходной бит = чётность числа единиц. Новое состояние =
|
||||
``full >> 1`` (самый старый бит вытесняется). Такое соглашение проверяется
|
||||
независимо посчитанными эталонными векторами в
|
||||
``tests/test_lab045_hackrf_bpsk_fec.py``.
|
||||
|
||||
Соответствие BPSK (совпадает с ``protocol/bpsk_radio.bpsk_modulate``):
|
||||
бит ``0`` → символ ``-1``, бит ``1`` → символ ``+1``. Декодер работает с
|
||||
soft-значениями BPSK (проекция принятого символа на решающую ось,
|
||||
``normalized.real`` в Lab044) и использует евклидову ветвевую метрику к
|
||||
ожидаемым символам ``{-1, +1}``. Hard-decision — частный случай: знаки
|
||||
soft-значений.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
CONSTRAINT_LENGTH = 7
|
||||
MEMORY = CONSTRAINT_LENGTH - 1 # 6
|
||||
STATE_COUNT = 1 << MEMORY # 64
|
||||
GENERATOR_POLYNOMIALS_OCTAL = (0o171, 0o133) # (171, 133) octal
|
||||
OUTPUTS_PER_INPUT = len(GENERATOR_POLYNOMIALS_OCTAL) # 2
|
||||
TAIL_BIT_COUNT = MEMORY # 6
|
||||
CODE_RATE = (1, OUTPUTS_PER_INPUT) # 1/2
|
||||
|
||||
_STATE_MASK = STATE_COUNT - 1 # 0x3F
|
||||
_INPUT_BIT_POSITION = MEMORY # входной бит занимает бит 6 полного регистра
|
||||
_LOWER_STATE_MASK = (1 << (MEMORY - 1)) - 1 # 0x1F, младшие 5 бит состояния
|
||||
|
||||
|
||||
def _parity(value: int) -> int:
|
||||
"""Чётность числа установленных битов (0 или 1)."""
|
||||
|
||||
return bin(value).count("1") & 1
|
||||
|
||||
|
||||
def _next_state(state: int, input_bit: int) -> int:
|
||||
full = (input_bit << _INPUT_BIT_POSITION) | state
|
||||
return full >> 1
|
||||
|
||||
|
||||
def _output_bits(state: int, input_bit: int) -> tuple[int, ...]:
|
||||
full = (input_bit << _INPUT_BIT_POSITION) | state
|
||||
return tuple(_parity(full & generator) for generator in GENERATOR_POLYNOMIALS_OCTAL)
|
||||
|
||||
|
||||
def _build_decode_tables() -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Предвычислить предшественников и ожидаемые символы для решётки.
|
||||
|
||||
Для каждого следующего состояния ``next_state`` вход, приведший в него,
|
||||
однозначен и равен старшему биту состояния (бит 5). У ``next_state``
|
||||
ровно два предшественника, различающиеся вытесненным младшим битом.
|
||||
Возвращает ``predecessors[next_state, branch]`` и
|
||||
``symbols[next_state, branch, output_index]`` со значениями в {-1, +1}.
|
||||
"""
|
||||
|
||||
predecessors = np.zeros((STATE_COUNT, 2), dtype=np.int64)
|
||||
symbols = np.zeros((STATE_COUNT, 2, OUTPUTS_PER_INPUT), dtype=np.float64)
|
||||
for next_state in range(STATE_COUNT):
|
||||
input_bit = next_state >> (MEMORY - 1) # старший бит состояния
|
||||
base = (next_state & _LOWER_STATE_MASK) << 1
|
||||
for branch in range(2):
|
||||
previous_state = base | branch
|
||||
assert _next_state(previous_state, input_bit) == next_state
|
||||
predecessors[next_state, branch] = previous_state
|
||||
output = _output_bits(previous_state, input_bit)
|
||||
for output_index, bit in enumerate(output):
|
||||
symbols[next_state, branch, output_index] = 2.0 * bit - 1.0
|
||||
return predecessors, symbols
|
||||
|
||||
|
||||
_PREDECESSORS, _BRANCH_SYMBOLS = _build_decode_tables()
|
||||
_BRANCH_SYMBOL_0 = _BRANCH_SYMBOLS[:, :, 0]
|
||||
_BRANCH_SYMBOL_1 = _BRANCH_SYMBOLS[:, :, 1]
|
||||
|
||||
|
||||
def _validate_information_bits(information_bits: np.ndarray) -> np.ndarray:
|
||||
raw_bits = np.asarray(information_bits)
|
||||
if raw_bits.ndim != 1:
|
||||
raise ValueError("information_bits должен быть одномерным")
|
||||
if raw_bits.size == 0:
|
||||
raise ValueError("information_bits не должен быть пустым")
|
||||
if not np.all((raw_bits == 0) | (raw_bits == 1)):
|
||||
raise ValueError("information_bits должен содержать только 0 и 1")
|
||||
return raw_bits.astype(np.uint8, copy=False)
|
||||
|
||||
|
||||
def encoded_bit_count(information_bit_count: int) -> int:
|
||||
"""Число кодовых битов для заданного числа информационных битов."""
|
||||
|
||||
if not isinstance(information_bit_count, (int, np.integer)):
|
||||
raise TypeError("information_bit_count должен быть целым")
|
||||
if information_bit_count < 0:
|
||||
raise ValueError("information_bit_count не может быть отрицательным")
|
||||
return OUTPUTS_PER_INPUT * (information_bit_count + TAIL_BIT_COUNT)
|
||||
|
||||
|
||||
def encode(information_bits: np.ndarray) -> np.ndarray:
|
||||
"""Свёрточно закодировать биты, добавив внутреннее терминирование.
|
||||
|
||||
Вход — одномерные биты {0,1}. К ним добавляются ``TAIL_BIT_COUNT``
|
||||
нулевых хвостовых бита, регистр стартует из нуля и заканчивает в нуле.
|
||||
Выход — одномерные кодовые биты {0,1} длиной ``2*(len+6)``.
|
||||
"""
|
||||
|
||||
bits = _validate_information_bits(information_bits)
|
||||
terminated = np.concatenate((bits, np.zeros(TAIL_BIT_COUNT, dtype=np.uint8)))
|
||||
output = np.empty(OUTPUTS_PER_INPUT * len(terminated), dtype=np.uint8)
|
||||
state = 0
|
||||
for index, input_bit in enumerate(int(bit) for bit in terminated):
|
||||
full = (input_bit << _INPUT_BIT_POSITION) | state
|
||||
for output_index, generator in enumerate(GENERATOR_POLYNOMIALS_OCTAL):
|
||||
output[OUTPUTS_PER_INPUT * index + output_index] = _parity(full & generator)
|
||||
state = full >> 1
|
||||
assert state == 0
|
||||
return output
|
||||
|
||||
|
||||
def viterbi_decode_soft(
|
||||
soft_values: np.ndarray,
|
||||
information_bit_count: int,
|
||||
) -> np.ndarray:
|
||||
"""Декодировать soft-значения BPSK терминированным Витерби.
|
||||
|
||||
``soft_values`` — одномерный массив вещественных решающих значений
|
||||
(в идеале ±1, реально ``normalized.real``), длиной ровно
|
||||
``encoded_bit_count(information_bit_count)``. Метрика ветви — евклидово
|
||||
расстояние до ожидаемых символов {-1, +1}. Решётка терминирована:
|
||||
старт и финиш в состоянии 0, хвостовые биты отбрасываются. Возвращает
|
||||
``information_bit_count`` информационных битов. TX-данные не используются.
|
||||
"""
|
||||
|
||||
if not isinstance(information_bit_count, (int, np.integer)):
|
||||
raise TypeError("information_bit_count должен быть целым")
|
||||
if information_bit_count <= 0:
|
||||
raise ValueError("information_bit_count должен быть положительным")
|
||||
information_bit_count = int(information_bit_count)
|
||||
values = np.asarray(soft_values, dtype=np.float64)
|
||||
if values.ndim != 1:
|
||||
raise ValueError("soft_values должен быть одномерным")
|
||||
if not np.all(np.isfinite(values)):
|
||||
raise ValueError("soft_values должен содержать только конечные значения")
|
||||
expected_length = encoded_bit_count(information_bit_count)
|
||||
if len(values) != expected_length:
|
||||
raise ValueError(
|
||||
f"soft_values имеет длину {len(values)}, ожидалось {expected_length}"
|
||||
)
|
||||
|
||||
step_count = information_bit_count + TAIL_BIT_COUNT
|
||||
received = values.reshape(step_count, OUTPUTS_PER_INPUT)
|
||||
|
||||
infinity = np.float64(1e18)
|
||||
path_metric = np.full(STATE_COUNT, infinity, dtype=np.float64)
|
||||
path_metric[0] = 0.0
|
||||
branch_choice = np.empty((step_count, STATE_COUNT), dtype=np.uint8)
|
||||
|
||||
for step in range(step_count):
|
||||
received_0 = received[step, 0]
|
||||
received_1 = received[step, 1]
|
||||
branch_metric = (
|
||||
(received_0 - _BRANCH_SYMBOL_0) ** 2
|
||||
+ (received_1 - _BRANCH_SYMBOL_1) ** 2
|
||||
) # (STATE_COUNT, 2)
|
||||
candidate = path_metric[_PREDECESSORS] + branch_metric # (STATE_COUNT, 2)
|
||||
chosen = np.argmin(candidate, axis=1).astype(np.uint8)
|
||||
path_metric = candidate[np.arange(STATE_COUNT), chosen]
|
||||
branch_choice[step] = chosen
|
||||
|
||||
# Терминированный трекбэк из состояния 0.
|
||||
decoded = np.empty(step_count, dtype=np.uint8)
|
||||
current_state = 0
|
||||
for step in range(step_count - 1, -1, -1):
|
||||
branch = branch_choice[step, current_state]
|
||||
decoded[step] = current_state >> (MEMORY - 1)
|
||||
current_state = int(_PREDECESSORS[current_state, branch])
|
||||
assert current_state == 0
|
||||
return decoded[:information_bit_count]
|
||||
|
||||
|
||||
def viterbi_decode_hard(
|
||||
coded_bits: np.ndarray,
|
||||
information_bit_count: int,
|
||||
) -> np.ndarray:
|
||||
"""Hard-decision вариант: биты {0,1} отображаются в ±1 и декодируются.
|
||||
|
||||
Служит базовой линией для честного сравнения soft и hard в тестах.
|
||||
"""
|
||||
|
||||
raw_bits = np.asarray(coded_bits)
|
||||
if raw_bits.ndim != 1:
|
||||
raise ValueError("coded_bits должен быть одномерным")
|
||||
if not np.all((raw_bits == 0) | (raw_bits == 1)):
|
||||
raise ValueError("coded_bits должен содержать только 0 и 1")
|
||||
bits = raw_bits.astype(np.uint8, copy=False)
|
||||
return viterbi_decode_soft(2.0 * bits.astype(np.float64) - 1.0, information_bit_count)
|
||||
Reference in New Issue
Block a user