Files
SDR-Rover/experiments/lab020_bpsk_frame_error_rate.py
LittleSam129 c486039053 Split experiments from tests
The tests/ directory held 50 laboratory programs and no tests. They model
channels, run hundreds of repetitions and write CSV, PNG and reports;
calling that a test suite blocked introducing a real one, because any
pytest run would have collected the labs and re-executed every
experiment.

- move all 50 lab programs to experiments/ with git mv, preserving history
- rewrite the 38 cross-imports between labs from tests.labNNN to
  experiments.labNNN
- leave tests/ empty for actual fast checks of protocol/
- point quick_gate and the hook at the new layout and add experiments/ to
  the syntax sweep
- update the paths quoted in the Lab042 specification and the verifier
  agent definition

This also defuses the import-time work finding without touching 41 files:
the labs still create directories and write files on import, but nothing
imports them now except the gate, which does so deliberately.

Gate passes: syntax clean, protocol imports, 15 lab modules import, 2
functional suites run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:34:58 +03:00

1494 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Lab020. Frame Error Rate полного BPSK-радиокадра.
Программа многократно передаёт IQ-радиокадр из Lab018
при разных значениях Eb/N0.
В каждой попытке случайным образом изменяются:
- задержка начала сигнала;
- постоянный фазовый поворот BPSK;
- реализация AWGN-шума.
Приёмник выполняет:
1. Согласованную RRC-фильтрацию.
2. Поиск PREAMBLE + RADIO SYNC.
3. Выбор фазы символьной дискретизации.
4. Оценку и компенсацию фазового поворота.
5. Демодуляцию BPSK.
6. Чтение радиозаголовка.
7. Извлечение внутреннего пакета.
8. Проверку структуры и CRC-32.
9. Восстановление исходного сообщения.
Результат:
FER — Frame Error Rate, доля радиокадров,
которые не удалось полностью восстановить.
"""
from csv import DictWriter
from math import erfc, expm1, log1p, sqrt
from pathlib import Path
import struct
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import fftconvolve
from protocol.packet import (
CRCError,
MESSAGE_TYPE_TEXT,
PacketError,
parse_packet,
)
# ============================================================
# Параметры радиокадра из Lab018
# ============================================================
RADIO_SYNC_WORD = 0xD391
PREAMBLE_BIT_COUNT = 64
SAMPLES_PER_SYMBOL = 32
RRC_ROLLOFF = 0.35
RRC_SPAN_SYMBOLS = 10
# Полный радиокадр:
#
# PREAMBLE 64 бита
# RADIO HEADER 32 бита
# INNER PACKET 224 бита
#
# Итого 320 бит
RADIO_FRAME_BIT_COUNT = 320
# ============================================================
# Контрольные данные
# ============================================================
EXPECTED_MESSAGE = "ПРИВЕТ SDR"
EXPECTED_SEQUENCE_NUMBER = 18
# ============================================================
# Настройки эксперимента
# ============================================================
EB_N0_VALUES_DB = [
4.0,
5.0,
6.0,
7.0,
8.0,
10.0,
12.0,
]
TRIALS_PER_EB_N0 = 100
RANDOM_SEED = 2026
# Максимальная случайная задержка начала кадра.
MAX_RANDOM_DELAY_SAMPLES = (
2 * SAMPLES_PER_SYMBOL
)
# Если нормированная корреляция ниже этого значения,
# считаем, что маркер радиокадра не обнаружен.
DETECTION_THRESHOLD = 0.55
# Защита от повреждённого поля LENGTH.
MAX_PROTOCOL_PACKET_SIZE = 4096
# ============================================================
# Пути
# ============================================================
INPUT_IQ_PATH = Path(
"data/processed/lab018/"
"lab018_bpsk_tx_iq.npy"
)
OUTPUT_DIRECTORY = Path(
"data/processed/lab020"
)
OUTPUT_DIRECTORY.mkdir(
parents=True,
exist_ok=True,
)
CSV_PATH = (
OUTPUT_DIRECTORY
/ "lab020_frame_error_results.csv"
)
GRAPH_PATH = (
OUTPUT_DIRECTORY
/ "lab020_frame_error_rate.png"
)
REPORT_PATH = (
OUTPUT_DIRECTORY
/ "lab020_frame_error_report.txt"
)
# ============================================================
# Статусы одной попытки
# ============================================================
STATUS_SUCCESS = "SUCCESS"
STATUS_DETECTION_FAIL = "DETECTION_FAIL"
STATUS_HEADER_ERROR = "HEADER_ERROR"
STATUS_CRC_ERROR = "CRC_ERROR"
STATUS_PACKET_ERROR = "PACKET_ERROR"
# ============================================================
# Преобразование bytes → bits
# ============================================================
def bytes_to_bits(
data: bytes,
) -> np.ndarray:
"""
Преобразовать bytes в одномерный массив битов.
"""
if not isinstance(data, bytes):
raise TypeError(
"data должен иметь тип bytes"
)
return np.unpackbits(
np.frombuffer(
data,
dtype=np.uint8,
)
)
# ============================================================
# Преобразование bits → bytes
# ============================================================
def bits_to_bytes(
bits: np.ndarray,
) -> bytes:
"""
Упаковать биты обратно в bytes.
"""
bits = np.asarray(
bits,
dtype=np.uint8,
)
if bits.ndim != 1:
raise ValueError(
"bits должен быть одномерным массивом"
)
if len(bits) % 8 != 0:
raise ValueError(
"Количество битов должно быть кратно восьми"
)
if not np.all(
(bits == 0) | (bits == 1)
):
raise ValueError(
"bits должен содержать только 0 и 1"
)
return np.packbits(
bits
).tobytes()
# ============================================================
# BPSK
# ============================================================
def bpsk_modulate(
bits: np.ndarray,
) -> np.ndarray:
"""
Преобразовать биты в опорные BPSK-символы.
0 → -1
1 → +1
"""
bits = np.asarray(
bits,
dtype=np.uint8,
)
symbols = (
2.0 * bits.astype(np.float64)
- 1.0
)
return symbols.astype(
np.complex128
)
def bpsk_demodulate(
symbols: np.ndarray,
) -> np.ndarray:
"""
Демодулировать BPSK по знаку компоненты I.
"""
return (
symbols.real >= 0.0
).astype(np.uint8)
# ============================================================
# Root Raised Cosine-фильтр
# ============================================================
def root_raised_cosine_taps(
rolloff: float,
samples_per_symbol: int,
span_symbols: int,
) -> np.ndarray:
"""
Рассчитать коэффициенты RRC-фильтра.
Параметры совпадают с Lab018 и Lab019.
"""
if not 0.0 < rolloff <= 1.0:
raise ValueError(
"rolloff должен находиться в диапазоне 0...1"
)
if samples_per_symbol <= 0:
raise ValueError(
"samples_per_symbol должен быть положительным"
)
if span_symbols <= 0:
raise ValueError(
"span_symbols должен быть положительным"
)
if span_symbols % 2 != 0:
raise ValueError(
"span_symbols должен быть чётным"
)
half_sample_count = (
span_symbols
* samples_per_symbol
// 2
)
sample_indexes = np.arange(
-half_sample_count,
half_sample_count + 1,
dtype=np.float64,
)
time_values = (
sample_indexes
/ samples_per_symbol
)
taps = np.zeros_like(
time_values
)
beta = rolloff
for index, time_value in enumerate(
time_values
):
if np.isclose(
time_value,
0.0,
):
taps[index] = (
1.0
- beta
+ 4.0 * beta / np.pi
)
continue
if np.isclose(
abs(time_value),
1.0 / (4.0 * beta),
):
taps[index] = (
beta
/ np.sqrt(2.0)
* (
(
1.0
+ 2.0 / np.pi
)
* np.sin(
np.pi
/ (4.0 * beta)
)
+ (
1.0
- 2.0 / np.pi
)
* np.cos(
np.pi
/ (4.0 * beta)
)
)
)
continue
numerator = (
np.sin(
np.pi
* time_value
* (1.0 - beta)
)
+ (
4.0
* beta
* time_value
* np.cos(
np.pi
* time_value
* (1.0 + beta)
)
)
)
denominator = (
np.pi
* time_value
* (
1.0
- (
4.0
* beta
* time_value
) ** 2
)
)
taps[index] = (
numerator
/ denominator
)
taps /= np.sqrt(
np.sum(
taps ** 2
)
)
return taps
# ============================================================
# Маркер PREAMBLE + RADIO SYNC
# ============================================================
def build_frame_marker(
) -> tuple[np.ndarray, np.ndarray]:
"""
Сформировать биты и BPSK-символы маркера.
"""
preamble_bits = np.tile(
np.array(
[1, 0],
dtype=np.uint8,
),
PREAMBLE_BIT_COUNT // 2,
)
sync_bits = bytes_to_bits(
struct.pack(
">H",
RADIO_SYNC_WORD,
)
)
marker_bits = np.concatenate(
[
preamble_bits,
sync_bits,
]
)
marker_symbols = bpsk_modulate(
marker_bits
)
return marker_bits, marker_symbols
# ============================================================
# Теоретический BER и идеальная FER
# ============================================================
def theoretical_bpsk_ber(
eb_n0_db: float,
) -> float:
"""
Теоретический BER когерентной BPSK в AWGN.
"""
eb_n0_linear = 10.0 ** (
eb_n0_db / 10.0
)
return 0.5 * erfc(
sqrt(eb_n0_linear)
)
def ber_to_frame_error_rate(
ber: float,
frame_bit_count: int,
) -> float:
"""
Оценить FER при независимых битовых ошибках.
FER = 1 - (1 - BER) ** N
"""
if ber == 0.0:
return 0.0
return -expm1(
frame_bit_count
* log1p(-ber)
)
# ============================================================
# Добавление AWGN по заданному Eb/N0
# ============================================================
def add_awgn_for_eb_n0(
clean_iq: np.ndarray,
signal_energy_per_bit: float,
eb_n0_db: float,
random_generator: np.random.Generator,
) -> np.ndarray:
"""
Добавить комплексный AWGN.
signal_energy_per_bit рассчитывается по исходному
IQ-радиокадру как:
сумма |IQ|² / количество передаваемых битов
Полная комплексная дисперсия шума:
noise_variance = Eb / (Eb/N0)
"""
eb_n0_linear = 10.0 ** (
eb_n0_db / 10.0
)
complex_noise_variance = (
signal_energy_per_bit
/ eb_n0_linear
)
component_sigma = sqrt(
complex_noise_variance / 2.0
)
noise = component_sigma * (
random_generator.standard_normal(
len(clean_iq)
)
+ 1j
* random_generator.standard_normal(
len(clean_iq)
)
)
return clean_iq + noise
# ============================================================
# Корреляционный поиск радиокадра
# ============================================================
def find_radio_frame(
matched_iq: np.ndarray,
marker_symbols: np.ndarray,
samples_per_symbol: int,
) -> dict:
"""
Перебрать все возможные фазы символьной дискретизации
и найти максимальную нормированную корреляцию.
"""
marker_energy = float(
np.sum(
np.abs(marker_symbols) ** 2
)
)
best_result = None
for sample_phase in range(
samples_per_symbol
):
symbol_samples = matched_iq[
sample_phase::samples_per_symbol
]
if len(symbol_samples) < len(
marker_symbols
):
continue
correlation = np.correlate(
symbol_samples,
marker_symbols,
mode="valid",
)
window_energy = np.convolve(
np.abs(symbol_samples) ** 2,
np.ones(
len(marker_symbols)
),
mode="valid",
)
normalized_correlation = (
np.abs(correlation)
/ (
np.sqrt(
window_energy
* marker_energy
)
+ 1e-12
)
)
start_symbol_index = int(
np.argmax(
normalized_correlation
)
)
score = float(
normalized_correlation[
start_symbol_index
]
)
if (
best_result is None
or score > best_result["score"]
):
best_result = {
"score": score,
"sample_phase": sample_phase,
"start_symbol_index": (
start_symbol_index
),
"symbol_samples": (
symbol_samples
),
"complex_correlation": (
correlation[
start_symbol_index
]
),
}
if best_result is None:
raise RuntimeError(
"Корреляционный поиск не дал результата"
)
return best_result
# ============================================================
# Приём одной реализации радиокадра
# ============================================================
def receive_one_frame(
received_iq: np.ndarray,
rrc_taps: np.ndarray,
marker_bits: np.ndarray,
marker_symbols: np.ndarray,
) -> dict:
"""
Выполнить полный приём одного радиокадра.
Возвращает словарь со статусом и диагностикой.
"""
matched_iq = fftconvolve(
received_iq,
rrc_taps,
mode="full",
)
search_result = find_radio_frame(
matched_iq=matched_iq,
marker_symbols=marker_symbols,
samples_per_symbol=SAMPLES_PER_SYMBOL,
)
correlation_score = search_result[
"score"
]
if correlation_score < DETECTION_THRESHOLD:
return {
"status": STATUS_DETECTION_FAIL,
"correlation_score": correlation_score,
"marker_bit_errors": None,
}
symbol_samples = search_result[
"symbol_samples"
]
frame_start_symbol = search_result[
"start_symbol_index"
]
estimated_phase = np.angle(
search_result[
"complex_correlation"
]
)
corrected_symbols = (
symbol_samples
* np.exp(
-1j * estimated_phase
)
)
received_bits = bpsk_demodulate(
corrected_symbols
)
available_bits = received_bits[
frame_start_symbol:
]
marker_bit_count = len(
marker_bits
)
if len(available_bits) < marker_bit_count:
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": None,
}
received_marker_bits = available_bits[
:marker_bit_count
]
marker_bit_errors = int(
np.count_nonzero(
received_marker_bits
!= marker_bits
)
)
radio_header_start = (
PREAMBLE_BIT_COUNT
)
radio_header_end = (
radio_header_start + 32
)
if len(available_bits) < radio_header_end:
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
try:
radio_header = bits_to_bytes(
available_bits[
radio_header_start:
radio_header_end
]
)
(
received_radio_sync,
protocol_packet_length,
) = struct.unpack(
">HH",
radio_header,
)
except (ValueError, struct.error):
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
if received_radio_sync != RADIO_SYNC_WORD:
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
if not (
1
<= protocol_packet_length
<= MAX_PROTOCOL_PACKET_SIZE
):
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
protocol_packet_start = (
radio_header_end
)
protocol_packet_end = (
protocol_packet_start
+ protocol_packet_length * 8
)
if len(available_bits) < protocol_packet_end:
return {
"status": STATUS_HEADER_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
try:
protocol_packet = bits_to_bytes(
available_bits[
protocol_packet_start:
protocol_packet_end
]
)
except ValueError:
return {
"status": STATUS_PACKET_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
try:
parsed_packet = parse_packet(
protocol_packet
)
except CRCError:
return {
"status": STATUS_CRC_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
except PacketError:
return {
"status": STATUS_PACKET_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
try:
restored_message = (
parsed_packet.payload.decode(
"utf-8"
)
)
except UnicodeDecodeError:
return {
"status": STATUS_PACKET_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
if (
parsed_packet.message_type
!= MESSAGE_TYPE_TEXT
or parsed_packet.sequence_number
!= EXPECTED_SEQUENCE_NUMBER
or restored_message
!= EXPECTED_MESSAGE
):
return {
"status": STATUS_PACKET_ERROR,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
return {
"status": STATUS_SUCCESS,
"correlation_score": correlation_score,
"marker_bit_errors": marker_bit_errors,
}
# ============================================================
# Загрузка IQ из Lab018
# ============================================================
if not INPUT_IQ_PATH.exists():
raise FileNotFoundError(
f"Не найден файл: {INPUT_IQ_PATH}. "
"Сначала необходимо выполнить Lab018."
)
transmitted_iq = np.load(
INPUT_IQ_PATH
)
if transmitted_iq.ndim != 1:
raise ValueError(
"IQ-массив должен быть одномерным"
)
if not np.iscomplexobj(
transmitted_iq
):
raise ValueError(
"Входной массив должен быть комплексным"
)
transmitted_iq = transmitted_iq.astype(
np.complex128
)
# ============================================================
# Подготовка приёмника
# ============================================================
rrc_taps = root_raised_cosine_taps(
rolloff=RRC_ROLLOFF,
samples_per_symbol=SAMPLES_PER_SYMBOL,
span_symbols=RRC_SPAN_SYMBOLS,
)
marker_bits, marker_symbols = (
build_frame_marker()
)
signal_energy_per_bit = (
np.sum(
np.abs(transmitted_iq) ** 2
)
/ RADIO_FRAME_BIT_COUNT
)
# ============================================================
# Основной эксперимент
# ============================================================
results = []
for eb_n0_index, eb_n0_db in enumerate(
EB_N0_VALUES_DB
):
counters = {
STATUS_SUCCESS: 0,
STATUS_DETECTION_FAIL: 0,
STATUS_HEADER_ERROR: 0,
STATUS_CRC_ERROR: 0,
STATUS_PACKET_ERROR: 0,
}
correlation_scores = []
marker_error_counts = []
random_generator = np.random.default_rng(
RANDOM_SEED
+ 1000 * eb_n0_index
)
for trial_index in range(
TRIALS_PER_EB_N0
):
random_delay = int(
random_generator.integers(
0,
MAX_RANDOM_DELAY_SAMPLES + 1,
)
)
random_phase = (
random_generator.uniform(
-np.pi,
np.pi,
)
)
phase_rotated_iq = (
transmitted_iq
* np.exp(
1j * random_phase
)
)
# Добавляем нули до и после кадра.
# После внесения AWGN эти участки превращаются
# в обычный шумовой фон.
clean_received_iq = np.concatenate(
[
np.zeros(
random_delay,
dtype=np.complex128,
),
phase_rotated_iq,
np.zeros(
2 * SAMPLES_PER_SYMBOL,
dtype=np.complex128,
),
]
)
noisy_received_iq = add_awgn_for_eb_n0(
clean_iq=clean_received_iq,
signal_energy_per_bit=(
signal_energy_per_bit
),
eb_n0_db=eb_n0_db,
random_generator=random_generator,
)
attempt_result = receive_one_frame(
received_iq=noisy_received_iq,
rrc_taps=rrc_taps,
marker_bits=marker_bits,
marker_symbols=marker_symbols,
)
status = attempt_result[
"status"
]
counters[status] += 1
correlation_scores.append(
attempt_result[
"correlation_score"
]
)
marker_bit_errors = attempt_result[
"marker_bit_errors"
]
if marker_bit_errors is not None:
marker_error_counts.append(
marker_bit_errors
)
success_count = counters[
STATUS_SUCCESS
]
frame_error_count = (
TRIALS_PER_EB_N0
- success_count
)
experimental_fer = (
frame_error_count
/ TRIALS_PER_EB_N0
)
theoretical_ber = theoretical_bpsk_ber(
eb_n0_db
)
ideal_fer = ber_to_frame_error_rate(
ber=theoretical_ber,
frame_bit_count=(
RADIO_FRAME_BIT_COUNT
),
)
mean_correlation = float(
np.mean(
correlation_scores
)
)
if marker_error_counts:
mean_marker_bit_errors = float(
np.mean(
marker_error_counts
)
)
else:
mean_marker_bit_errors = float(
"nan"
)
results.append(
{
"eb_n0_db": eb_n0_db,
"trials": TRIALS_PER_EB_N0,
"success_count": success_count,
"detection_fail_count": counters[
STATUS_DETECTION_FAIL
],
"header_error_count": counters[
STATUS_HEADER_ERROR
],
"crc_error_count": counters[
STATUS_CRC_ERROR
],
"packet_error_count": counters[
STATUS_PACKET_ERROR
],
"experimental_fer": experimental_fer,
"theoretical_ber": theoretical_ber,
"ideal_fer": ideal_fer,
"mean_correlation": mean_correlation,
"mean_marker_bit_errors": (
mean_marker_bit_errors
),
}
)
# ============================================================
# Вывод таблицы
# ============================================================
print(
"=== Lab020. FER полного BPSK-радиокадра ==="
)
print("\nКоличество попыток на точку:")
print(TRIALS_PER_EB_N0)
print("\nРезультаты:")
print(
f"{'Eb/N0':>9}"
f"{'Успех':>9}"
f"{'Нет марк.':>11}"
f"{'Загол.':>9}"
f"{'CRC':>7}"
f"{'Пакет':>8}"
f"{'FER':>10}"
f"{'Идеал FER':>13}"
f"{'Коррел.':>11}"
)
print("-" * 87)
for result in results:
print(
f"{result['eb_n0_db']:>6.1f} дБ"
f"{result['success_count']:>9}"
f"{result['detection_fail_count']:>11}"
f"{result['header_error_count']:>9}"
f"{result['crc_error_count']:>7}"
f"{result['packet_error_count']:>8}"
f"{result['experimental_fer']:>10.3f}"
f"{result['ideal_fer']:>13.3f}"
f"{result['mean_correlation']:>11.3f}"
)
# ============================================================
# Сохранение CSV
# ============================================================
with CSV_PATH.open(
"w",
newline="",
encoding="utf-8-sig",
) as csv_file:
writer = DictWriter(
csv_file,
fieldnames=list(
results[0].keys()
),
)
writer.writeheader()
writer.writerows(results)
# ============================================================
# Подготовка данных графика
# ============================================================
eb_n0_values = np.array(
[
result["eb_n0_db"]
for result in results
],
dtype=np.float64,
)
experimental_fer_values = np.array(
[
result["experimental_fer"]
for result in results
],
dtype=np.float64,
)
ideal_fer_values = np.array(
[
result["ideal_fer"]
for result in results
],
dtype=np.float64,
)
measurement_floor = (
0.5 / TRIALS_PER_EB_N0
)
experimental_fer_for_plot = np.maximum(
experimental_fer_values,
measurement_floor,
)
# ============================================================
# Графики
# ============================================================
figure, axes = plt.subplots(
2,
1,
figsize=(11, 10),
)
# ------------------------------------------------------------
# 1. FER
# ------------------------------------------------------------
axes[0].semilogy(
eb_n0_values,
ideal_fer_values,
marker="o",
label=(
"Идеальная FER по BER "
"при идеальной синхронизации"
),
)
axes[0].semilogy(
eb_n0_values,
experimental_fer_for_plot,
marker="s",
linestyle="--",
label="Экспериментальная FER приёмника",
)
axes[0].axhline(
measurement_floor,
linestyle=":",
label=(
"Предел измерения "
f"{measurement_floor:.3f}"
),
)
axes[0].set_xlabel(
"Eb/N0, дБ"
)
axes[0].set_ylabel(
"Frame Error Rate"
)
axes[0].set_title(
"Устойчивость полного BPSK-радиокадра"
)
axes[0].grid(
True,
which="both",
)
axes[0].legend()
# ------------------------------------------------------------
# 2. Классификация результатов
# ------------------------------------------------------------
success_rates = [
result["success_count"]
/ TRIALS_PER_EB_N0
for result in results
]
detection_fail_rates = [
result["detection_fail_count"]
/ TRIALS_PER_EB_N0
for result in results
]
header_error_rates = [
result["header_error_count"]
/ TRIALS_PER_EB_N0
for result in results
]
crc_error_rates = [
result["crc_error_count"]
/ TRIALS_PER_EB_N0
for result in results
]
packet_error_rates = [
result["packet_error_count"]
/ TRIALS_PER_EB_N0
for result in results
]
axes[1].plot(
eb_n0_values,
success_rates,
marker="o",
label="Успешно",
)
axes[1].plot(
eb_n0_values,
detection_fail_rates,
marker="o",
label="Маркер не обнаружен",
)
axes[1].plot(
eb_n0_values,
header_error_rates,
marker="o",
label="Ошибка радиозаголовка",
)
axes[1].plot(
eb_n0_values,
crc_error_rates,
marker="o",
label="Ошибка CRC",
)
axes[1].plot(
eb_n0_values,
packet_error_rates,
marker="o",
label="Ошибка структуры пакета",
)
axes[1].set_xlabel(
"Eb/N0, дБ"
)
axes[1].set_ylabel(
"Доля попыток"
)
axes[1].set_ylim(
-0.03,
1.03,
)
axes[1].set_title(
"Причины потери радиокадров"
)
axes[1].grid(
True
)
axes[1].legend()
figure.tight_layout()
figure.savefig(
GRAPH_PATH,
dpi=160,
)
plt.close(
figure
)
# ============================================================
# Текстовый отчёт
# ============================================================
report_lines = [
"Lab020. Full BPSK frame error rate",
"",
(
"Trials per Eb/N0: "
f"{TRIALS_PER_EB_N0}"
),
(
"Detection threshold: "
f"{DETECTION_THRESHOLD:.3f}"
),
(
"Frame bit count: "
f"{RADIO_FRAME_BIT_COUNT}"
),
"",
]
for result in results:
report_lines.extend(
[
f"Eb/N0: {result['eb_n0_db']:.1f} dB",
(
" Success: "
f"{result['success_count']}"
),
(
" Detection fail: "
f"{result['detection_fail_count']}"
),
(
" Header error: "
f"{result['header_error_count']}"
),
(
" CRC error: "
f"{result['crc_error_count']}"
),
(
" Packet error: "
f"{result['packet_error_count']}"
),
(
" Experimental FER: "
f"{result['experimental_fer']:.6f}"
),
(
" Ideal FER: "
f"{result['ideal_fer']:.6f}"
),
"",
]
)
REPORT_PATH.write_text(
"\n".join(
report_lines
),
encoding="utf-8",
)
# ============================================================
# Автоматические проверки
# ============================================================
result_at_4_db = next(
result
for result in results
if result["eb_n0_db"] == 4.0
)
result_at_12_db = next(
result
for result in results
if result["eb_n0_db"] == 12.0
)
assert sum(
[
result[
"success_count"
],
result[
"detection_fail_count"
],
result[
"header_error_count"
],
result[
"crc_error_count"
],
result[
"packet_error_count"
],
]
) == TRIALS_PER_EB_N0
assert (
result_at_12_db["success_count"]
> result_at_4_db["success_count"]
)
assert (
result_at_12_db["experimental_fer"]
< result_at_4_db["experimental_fer"]
)
assert CSV_PATH.exists()
assert GRAPH_PATH.exists()
assert REPORT_PATH.exists()
print("\nCSV:")
print(CSV_PATH)
print("\nГрафик:")
print(GRAPH_PATH)
print("\nОтчёт:")
print(REPORT_PATH)
print(
"\nПроверка пройдена: "
"измерена FER полного BPSK-радиокадра."
)