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>
This commit is contained in:
617
experiments/lab014_bpsk_ber_curve.py
Normal file
617
experiments/lab014_bpsk_ber_curve.py
Normal file
@@ -0,0 +1,617 @@
|
||||
"""
|
||||
Lab014. Измерение BER для BPSK в канале AWGN.
|
||||
|
||||
Программа:
|
||||
|
||||
1. Генерирует 1 000 000 случайных битов.
|
||||
2. Преобразует их в BPSK-символы.
|
||||
3. Добавляет комплексный гауссов шум.
|
||||
4. Демодулирует принятый сигнал.
|
||||
5. Измеряет экспериментальный BER.
|
||||
6. Рассчитывает теоретический BER.
|
||||
7. Оценивает вероятность повреждения пакетов разной длины.
|
||||
8. Строит BER-кривую.
|
||||
9. Сохраняет результаты в CSV.
|
||||
|
||||
Упрощения модели:
|
||||
|
||||
- один бит передаётся одним BPSK-символом;
|
||||
- один символ представлен одним IQ-сэмплом;
|
||||
- частотная и фазовая синхронизация идеальны;
|
||||
- межсимвольные искажения отсутствуют;
|
||||
- канал содержит только AWGN.
|
||||
"""
|
||||
|
||||
from csv import DictWriter
|
||||
from math import (
|
||||
erfc,
|
||||
expm1,
|
||||
log1p,
|
||||
sqrt,
|
||||
)
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Настройки эксперимента
|
||||
# ============================================================
|
||||
|
||||
BIT_COUNT = 1_000_000
|
||||
|
||||
EB_N0_VALUES_DB = [
|
||||
-4.0,
|
||||
-2.0,
|
||||
0.0,
|
||||
2.0,
|
||||
4.0,
|
||||
6.0,
|
||||
8.0,
|
||||
10.0,
|
||||
12.0,
|
||||
]
|
||||
|
||||
RANDOM_SEED = 2026
|
||||
|
||||
OUTPUT_DIRECTORY = Path(
|
||||
"data/processed/lab014"
|
||||
)
|
||||
|
||||
OUTPUT_DIRECTORY.mkdir(
|
||||
parents=True,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
GRAPH_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab014_bpsk_ber_curve.png"
|
||||
)
|
||||
|
||||
CSV_PATH = (
|
||||
OUTPUT_DIRECTORY
|
||||
/ "lab014_bpsk_ber_results.csv"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Размеры пакетов для оценки PER
|
||||
# ============================================================
|
||||
|
||||
# Пакет из Lab013:
|
||||
#
|
||||
# заголовок 8 байт
|
||||
# PAYLOAD 16 байт
|
||||
# CRC 4 байта
|
||||
#
|
||||
# Итого 28 байт = 224 бита
|
||||
SHORT_PACKET_BITS = 224
|
||||
|
||||
# Типовой пакет изображения:
|
||||
#
|
||||
# заголовок протокола 8 байт
|
||||
# заголовок фрагмента 12 байт
|
||||
# данные JPEG 512 байт
|
||||
# CRC 4 байта
|
||||
#
|
||||
# Итого 536 байт
|
||||
IMAGE_PACKET_BITS = 536 * 8
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вспомогательные функции
|
||||
# ============================================================
|
||||
|
||||
def bpsk_modulate(
|
||||
bits: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Преобразовать биты в BPSK-символы.
|
||||
|
||||
Отображение:
|
||||
|
||||
0 → -1
|
||||
1 → +1
|
||||
"""
|
||||
|
||||
bits = np.asarray(
|
||||
bits,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
if bits.ndim != 1:
|
||||
raise ValueError(
|
||||
"bits должен быть одномерным массивом"
|
||||
)
|
||||
|
||||
if not np.all(
|
||||
(bits == 0) | (bits == 1)
|
||||
):
|
||||
raise ValueError(
|
||||
"bits должен содержать только 0 и 1"
|
||||
)
|
||||
|
||||
symbols = (
|
||||
2.0 * bits.astype(np.float64)
|
||||
- 1.0
|
||||
)
|
||||
|
||||
return symbols.astype(
|
||||
np.complex128
|
||||
)
|
||||
|
||||
|
||||
def add_awgn(
|
||||
iq_samples: np.ndarray,
|
||||
eb_n0_db: float,
|
||||
random_generator: np.random.Generator,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Добавить комплексный AWGN-шум.
|
||||
|
||||
Энергия BPSK-символа равна единице.
|
||||
|
||||
Для одного бита на символ:
|
||||
|
||||
Es/N0 = Eb/N0
|
||||
"""
|
||||
|
||||
eb_n0_linear = 10.0 ** (
|
||||
eb_n0_db / 10.0
|
||||
)
|
||||
|
||||
# Для комплексного AWGN каждая компонента
|
||||
# I и Q получает половину полной мощности шума.
|
||||
noise_sigma = sqrt(
|
||||
1.0
|
||||
/ (
|
||||
2.0
|
||||
* eb_n0_linear
|
||||
)
|
||||
)
|
||||
|
||||
noise = noise_sigma * (
|
||||
random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
+ 1j
|
||||
* random_generator.standard_normal(
|
||||
len(iq_samples)
|
||||
)
|
||||
)
|
||||
|
||||
return iq_samples + noise
|
||||
|
||||
|
||||
def bpsk_demodulate(
|
||||
received_iq: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""
|
||||
Демодулировать BPSK по знаку компоненты I.
|
||||
|
||||
I < 0 → бит 0
|
||||
I >= 0 → бит 1
|
||||
"""
|
||||
|
||||
return (
|
||||
received_iq.real >= 0.0
|
||||
).astype(np.uint8)
|
||||
|
||||
|
||||
def theoretical_bpsk_ber(
|
||||
eb_n0_db: float,
|
||||
) -> float:
|
||||
"""
|
||||
Рассчитать теоретический BER когерентной BPSK
|
||||
в канале AWGN.
|
||||
|
||||
BER = 0.5 * erfc(sqrt(Eb/N0))
|
||||
"""
|
||||
|
||||
eb_n0_linear = 10.0 ** (
|
||||
eb_n0_db / 10.0
|
||||
)
|
||||
|
||||
return 0.5 * erfc(
|
||||
sqrt(eb_n0_linear)
|
||||
)
|
||||
|
||||
|
||||
def ber_to_per(
|
||||
ber: float,
|
||||
packet_bit_count: int,
|
||||
) -> float:
|
||||
"""
|
||||
Оценить Packet Error Rate из BER.
|
||||
|
||||
Предполагается:
|
||||
|
||||
- ошибки отдельных битов независимы;
|
||||
- пакет считается повреждённым,
|
||||
если ошибся хотя бы один бит.
|
||||
|
||||
PER = 1 - (1 - BER) ** N
|
||||
"""
|
||||
|
||||
if not 0.0 <= ber <= 1.0:
|
||||
raise ValueError(
|
||||
"BER должен находиться в диапазоне 0...1"
|
||||
)
|
||||
|
||||
if packet_bit_count <= 0:
|
||||
raise ValueError(
|
||||
"packet_bit_count должен быть положительным"
|
||||
)
|
||||
|
||||
if ber == 0.0:
|
||||
return 0.0
|
||||
|
||||
if ber == 1.0:
|
||||
return 1.0
|
||||
|
||||
# Такая запись численно устойчивее,
|
||||
# чем прямое возведение в степень
|
||||
# для очень маленьких BER.
|
||||
return -expm1(
|
||||
packet_bit_count
|
||||
* log1p(-ber)
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Генерация исходной последовательности
|
||||
# ============================================================
|
||||
|
||||
bit_generator = np.random.default_rng(
|
||||
RANDOM_SEED
|
||||
)
|
||||
|
||||
transmitted_bits = bit_generator.integers(
|
||||
low=0,
|
||||
high=2,
|
||||
size=BIT_COUNT,
|
||||
dtype=np.uint8,
|
||||
)
|
||||
|
||||
transmitted_iq = bpsk_modulate(
|
||||
transmitted_bits
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Эксперимент при разных Eb/N0
|
||||
# ============================================================
|
||||
|
||||
results = []
|
||||
|
||||
for experiment_index, eb_n0_db in enumerate(
|
||||
EB_N0_VALUES_DB
|
||||
):
|
||||
|
||||
noise_generator = np.random.default_rng(
|
||||
RANDOM_SEED
|
||||
+ 1000
|
||||
+ experiment_index
|
||||
)
|
||||
|
||||
received_iq = add_awgn(
|
||||
iq_samples=transmitted_iq,
|
||||
eb_n0_db=eb_n0_db,
|
||||
random_generator=noise_generator,
|
||||
)
|
||||
|
||||
received_bits = bpsk_demodulate(
|
||||
received_iq
|
||||
)
|
||||
|
||||
bit_error_count = int(
|
||||
np.count_nonzero(
|
||||
transmitted_bits
|
||||
!= received_bits
|
||||
)
|
||||
)
|
||||
|
||||
experimental_ber = (
|
||||
bit_error_count
|
||||
/ BIT_COUNT
|
||||
)
|
||||
|
||||
theoretical_ber = theoretical_bpsk_ber(
|
||||
eb_n0_db
|
||||
)
|
||||
|
||||
short_packet_per = ber_to_per(
|
||||
theoretical_ber,
|
||||
SHORT_PACKET_BITS,
|
||||
)
|
||||
|
||||
image_packet_per = ber_to_per(
|
||||
theoretical_ber,
|
||||
IMAGE_PACKET_BITS,
|
||||
)
|
||||
|
||||
results.append(
|
||||
{
|
||||
"eb_n0_db": eb_n0_db,
|
||||
"bit_errors": bit_error_count,
|
||||
"experimental_ber": experimental_ber,
|
||||
"theoretical_ber": theoretical_ber,
|
||||
"short_packet_per": short_packet_per,
|
||||
"image_packet_per": image_packet_per,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Вывод основных результатов
|
||||
# ============================================================
|
||||
|
||||
print(
|
||||
"=== Lab014. BER-кривая BPSK ==="
|
||||
)
|
||||
|
||||
print("\nКоличество переданных битов:")
|
||||
|
||||
print(
|
||||
f"{BIT_COUNT:,}".replace(",", " ")
|
||||
)
|
||||
|
||||
print("\nРезультаты:")
|
||||
|
||||
print(
|
||||
f"{'Eb/N0':>9}"
|
||||
f"{'Ошибки':>12}"
|
||||
f"{'BER эксперимент':>19}"
|
||||
f"{'BER теория':>16}"
|
||||
f"{'PER 224 бит':>16}"
|
||||
f"{'PER 4288 бит':>17}"
|
||||
)
|
||||
|
||||
print("-" * 89)
|
||||
|
||||
for result in results:
|
||||
|
||||
print(
|
||||
f"{result['eb_n0_db']:>6.1f} дБ"
|
||||
f"{result['bit_errors']:>12}"
|
||||
f"{result['experimental_ber']:>19.6e}"
|
||||
f"{result['theoretical_ber']:>16.6e}"
|
||||
f"{result['short_packet_per']:>16.6f}"
|
||||
f"{result['image_packet_per']:>17.6f}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Сохранение CSV
|
||||
# ============================================================
|
||||
|
||||
with CSV_PATH.open(
|
||||
"w",
|
||||
newline="",
|
||||
encoding="utf-8-sig",
|
||||
) as csv_file:
|
||||
|
||||
fieldnames = [
|
||||
"eb_n0_db",
|
||||
"bit_errors",
|
||||
"experimental_ber",
|
||||
"theoretical_ber",
|
||||
"short_packet_per_224_bits",
|
||||
"image_packet_per_4288_bits",
|
||||
]
|
||||
|
||||
writer = DictWriter(
|
||||
csv_file,
|
||||
fieldnames=fieldnames,
|
||||
)
|
||||
|
||||
writer.writeheader()
|
||||
|
||||
for result in results:
|
||||
|
||||
writer.writerow(
|
||||
{
|
||||
"eb_n0_db": result["eb_n0_db"],
|
||||
"bit_errors": result["bit_errors"],
|
||||
"experimental_ber": (
|
||||
result["experimental_ber"]
|
||||
),
|
||||
"theoretical_ber": (
|
||||
result["theoretical_ber"]
|
||||
),
|
||||
"short_packet_per_224_bits": (
|
||||
result["short_packet_per"]
|
||||
),
|
||||
"image_packet_per_4288_bits": (
|
||||
result["image_packet_per"]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Подготовка данных для графика
|
||||
# ============================================================
|
||||
|
||||
eb_n0_plot_values = np.array(
|
||||
[
|
||||
result["eb_n0_db"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
experimental_ber_values = np.array(
|
||||
[
|
||||
result["experimental_ber"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
theoretical_ber_values = np.array(
|
||||
[
|
||||
result["theoretical_ber"]
|
||||
for result in results
|
||||
],
|
||||
dtype=np.float64,
|
||||
)
|
||||
|
||||
|
||||
# Нулевой измеренный BER невозможно показать
|
||||
# на логарифмической шкале.
|
||||
#
|
||||
# Поэтому для графика ставим такую точку
|
||||
# на уровень половины одного наблюдаемого события.
|
||||
measurement_floor = (
|
||||
0.5 / BIT_COUNT
|
||||
)
|
||||
|
||||
experimental_ber_for_plot = np.maximum(
|
||||
experimental_ber_values,
|
||||
measurement_floor,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Построение BER-графика
|
||||
# ============================================================
|
||||
|
||||
figure = plt.figure(
|
||||
figsize=(10, 7)
|
||||
)
|
||||
|
||||
plt.semilogy(
|
||||
eb_n0_plot_values,
|
||||
theoretical_ber_values,
|
||||
marker="o",
|
||||
label="Теоретический BER BPSK",
|
||||
)
|
||||
|
||||
plt.semilogy(
|
||||
eb_n0_plot_values,
|
||||
experimental_ber_for_plot,
|
||||
marker="s",
|
||||
linestyle="--",
|
||||
label="Экспериментальный BER",
|
||||
)
|
||||
|
||||
plt.axhline(
|
||||
measurement_floor,
|
||||
linestyle=":",
|
||||
label=(
|
||||
"Предел измерения "
|
||||
f"{measurement_floor:.1e}"
|
||||
),
|
||||
)
|
||||
|
||||
plt.xlabel(
|
||||
"Eb/N0, дБ"
|
||||
)
|
||||
|
||||
plt.ylabel(
|
||||
"BER"
|
||||
)
|
||||
|
||||
plt.title(
|
||||
"BPSK в канале AWGN: "
|
||||
"эксперимент и теория"
|
||||
)
|
||||
|
||||
plt.grid(
|
||||
True,
|
||||
which="both",
|
||||
)
|
||||
|
||||
plt.legend()
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
figure.savefig(
|
||||
GRAPH_PATH,
|
||||
dpi=160,
|
||||
)
|
||||
|
||||
plt.close(
|
||||
figure
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Инженерные контрольные точки
|
||||
# ============================================================
|
||||
|
||||
print("\nИнженерные контрольные точки:")
|
||||
|
||||
for target_db in [
|
||||
6.0,
|
||||
8.0,
|
||||
10.0,
|
||||
]:
|
||||
|
||||
result = next(
|
||||
item
|
||||
for item in results
|
||||
if item["eb_n0_db"] == target_db
|
||||
)
|
||||
|
||||
print(
|
||||
f"\nEb/N0 = {target_db:.1f} дБ"
|
||||
)
|
||||
|
||||
print(
|
||||
"Теоретический BER:",
|
||||
f"{result['theoretical_ber']:.6e}",
|
||||
)
|
||||
|
||||
print(
|
||||
"PER короткого пакета 224 бита:",
|
||||
f"{result['short_packet_per'] * 100:.2f} %",
|
||||
)
|
||||
|
||||
print(
|
||||
"PER JPEG-пакета 4288 бит:",
|
||||
f"{result['image_packet_per'] * 100:.2f} %",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Автоматические проверки
|
||||
# ============================================================
|
||||
|
||||
assert len(transmitted_bits) == BIT_COUNT
|
||||
|
||||
assert len(received_bits) == BIT_COUNT
|
||||
|
||||
assert all(
|
||||
0.0
|
||||
<= result["experimental_ber"]
|
||||
<= 1.0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert all(
|
||||
0.0
|
||||
<= result["theoretical_ber"]
|
||||
<= 1.0
|
||||
for result in results
|
||||
)
|
||||
|
||||
assert GRAPH_PATH.exists()
|
||||
|
||||
assert CSV_PATH.exists()
|
||||
|
||||
|
||||
print("\nГрафик BER:")
|
||||
|
||||
print(GRAPH_PATH)
|
||||
|
||||
print("\nТаблица CSV:")
|
||||
|
||||
print(CSV_PATH)
|
||||
|
||||
print(
|
||||
"\nПроверка пройдена: "
|
||||
"экспериментальная BER-кривая построена."
|
||||
)
|
||||
Reference in New Issue
Block a user