""" 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-кривая построена." )