""" Lab015. Подбор оптимального размера фрагмента изображения. Программа сравнивает фрагменты размером: - 64 байта; - 128 байт; - 256 байт; - 512 байт; - 1024 байта. Для каждого размера рассчитываются: 1. Количество фрагментов изображения. 2. Служебные расходы протокола. 3. Теоретический BER BPSK. 4. Вероятность успешной доставки DATA-пакета. 5. Вероятность успешной доставки ACK. 6. Среднее число передач одного фрагмента. 7. Ожидаемый полный радиообмен с ARQ. 8. Время передачи фотографии при 20 кбит/с. 9. Эффективная полезная скорость. Модель канала: - когерентная BPSK; - AWGN; - ошибки битов независимы; - повреждённый пакет отбрасывается по CRC; - применяется Stop-and-Wait ARQ; - DATA и ACK работают при одинаковом Eb/N0. Не учитываются: - ожидание тайм-аута; - паузы между DATA и ACK; - преамбула; - синхронизация; - FEC; - многолучёвость; - частотные и фазовые ошибки. """ from csv import DictWriter from math import ( erfc, exp, log1p, sqrt, ) from pathlib import Path import matplotlib.pyplot as plt from protocol.image_fragments import ( encode_image_fragment, split_image_bytes, ) from protocol.packet import ( MESSAGE_TYPE_ACK, MESSAGE_TYPE_IMAGE_FRAGMENT, build_packet, ) # ============================================================ # Настройки # ============================================================ SOURCE_PATH = Path( "data/raw/lab009_source.jpg" ) OUTPUT_DIRECTORY = Path( "data/processed/lab015" ) OUTPUT_DIRECTORY.mkdir( parents=True, exist_ok=True, ) FRAGMENT_SIZES = [ 64, 128, 256, 512, 1024, ] EB_N0_VALUES_DB = [ 6.0, 8.0, 10.0, 12.0, ] CHANNEL_BITRATE_BPS = 20_000 IMAGE_ID_BASE = 2026071600 CSV_PATH = ( OUTPUT_DIRECTORY / "lab015_fragment_results.csv" ) TIME_GRAPH_PATH = ( OUTPUT_DIRECTORY / "lab015_transfer_time.png" ) GOODPUT_GRAPH_PATH = ( OUTPUT_DIRECTORY / "lab015_effective_goodput.png" ) # ============================================================ # Вспомогательные функции # ============================================================ 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 packet_success_probability( ber: float, packet_bit_count: int, ) -> float: """ Вероятность того, что весь пакет будет принят без единой битовой ошибки. P_success = (1 - BER) ** N """ if not 0.0 <= ber <= 1.0: raise ValueError( "BER должен находиться в диапазоне 0...1" ) if packet_bit_count <= 0: raise ValueError( "Размер пакета должен быть положительным" ) if ber == 0.0: return 1.0 if ber == 1.0: return 0.0 # Численно устойчивый вариант выражения: # # (1 - BER) ** N return exp( packet_bit_count * log1p(-ber) ) def format_bytes( byte_count: float, ) -> str: """ Представить объём в удобном виде. """ if byte_count < 1024: return f"{byte_count:.0f} байт" kibibytes = byte_count / 1024 if kibibytes < 1024: return f"{kibibytes:.2f} КиБ" mebibytes = kibibytes / 1024 return f"{mebibytes:.2f} МиБ" def format_duration( seconds: float, ) -> str: """ Представить длительность в удобном виде. """ if seconds < 1: return f"{seconds * 1000:.0f} мс" if seconds < 60: return f"{seconds:.2f} с" if seconds < 3600: minutes = int(seconds // 60) remaining_seconds = seconds % 60 return ( f"{minutes} мин " f"{remaining_seconds:.1f} с" ) hours = int(seconds // 3600) remaining_minutes = ( seconds % 3600 ) / 60 return ( f"{hours} ч " f"{remaining_minutes:.1f} мин" ) # ============================================================ # Проверка исходного JPEG # ============================================================ if not SOURCE_PATH.exists(): raise FileNotFoundError( f"Не найден файл: {SOURCE_PATH}. " "Сначала необходимо выполнить Lab009." ) source_bytes = SOURCE_PATH.read_bytes() if not source_bytes: raise ValueError( "Исходный JPEG пуст" ) source_size = len(source_bytes) # ============================================================ # Размер ACK # ============================================================ ack_packet = build_packet( payload=b"", message_type=MESSAGE_TYPE_ACK, sequence_number=0, ) ack_packet_size_bytes = len( ack_packet ) ack_packet_size_bits = ( ack_packet_size_bytes * 8 ) # ============================================================ # Основной расчёт # ============================================================ results = [] for fragment_size_index, fragment_size in enumerate( FRAGMENT_SIZES ): fragments = split_image_bytes( image_bytes=source_bytes, image_id=( IMAGE_ID_BASE + fragment_size_index ), fragment_data_size=fragment_size, ) data_packet_sizes_bytes = [] for fragment in fragments: fragment_payload = encode_image_fragment( fragment ) data_packet = build_packet( payload=fragment_payload, message_type=( MESSAGE_TYPE_IMAGE_FRAGMENT ), sequence_number=( fragment.fragment_index ), ) data_packet_sizes_bytes.append( len(data_packet) ) ideal_data_bytes = sum( data_packet_sizes_bytes ) ideal_ack_bytes = ( len(fragments) * ack_packet_size_bytes ) ideal_total_bytes = ( ideal_data_bytes + ideal_ack_bytes ) ideal_efficiency_percent = ( source_size / ideal_total_bytes * 100 ) for eb_n0_db in EB_N0_VALUES_DB: ber = theoretical_bpsk_ber( eb_n0_db ) ack_success_probability = ( packet_success_probability( ber=ber, packet_bit_count=( ack_packet_size_bits ), ) ) expected_data_bytes = 0.0 expected_ack_bytes = 0.0 expected_data_transmissions = 0.0 data_success_probabilities = [] for data_packet_size_bytes in ( data_packet_sizes_bytes ): data_packet_size_bits = ( data_packet_size_bytes * 8 ) data_success_probability = ( packet_success_probability( ber=ber, packet_bit_count=( data_packet_size_bits ), ) ) data_success_probabilities.append( data_success_probability ) # Для успешного завершения попытки должны # одновременно правильно пройти DATA и ACK. confirmed_attempt_probability = ( data_success_probability * ack_success_probability ) if confirmed_attempt_probability == 0.0: raise RuntimeError( "Вероятность подтверждения " "оказалась равной нулю" ) # Число DATA-передач до успешного ACK # подчиняется геометрическому распределению. expected_attempt_count = ( 1.0 / confirmed_attempt_probability ) expected_data_transmissions += ( expected_attempt_count ) expected_data_bytes += ( data_packet_size_bytes * expected_attempt_count ) # ACK передаётся только после правильно # принятого DATA. # # Среднее количество передач ACK до # успешного ACK равно 1 / P_ACK. expected_ack_bytes += ( ack_packet_size_bytes / ack_success_probability ) expected_total_bytes = ( expected_data_bytes + expected_ack_bytes ) expected_transfer_seconds = ( expected_total_bytes * 8 / CHANNEL_BITRATE_BPS ) expected_efficiency_percent = ( source_size / expected_total_bytes * 100 ) effective_goodput_bps = ( source_size * 8 / expected_transfer_seconds ) average_data_transmissions = ( expected_data_transmissions / len(fragments) ) minimum_data_success_probability = min( data_success_probabilities ) maximum_data_success_probability = max( data_success_probabilities ) results.append( { "fragment_size": fragment_size, "fragment_count": len(fragments), "eb_n0_db": eb_n0_db, "ber": ber, "ideal_total_bytes": ( ideal_total_bytes ), "ideal_efficiency_percent": ( ideal_efficiency_percent ), "ack_success_probability": ( ack_success_probability ), "minimum_data_success_probability": ( minimum_data_success_probability ), "maximum_data_success_probability": ( maximum_data_success_probability ), "average_data_transmissions": ( average_data_transmissions ), "expected_total_bytes": ( expected_total_bytes ), "expected_transfer_seconds": ( expected_transfer_seconds ), "expected_efficiency_percent": ( expected_efficiency_percent ), "effective_goodput_bps": ( effective_goodput_bps ), } ) # ============================================================ # Вывод общих данных # ============================================================ print( "=== Lab015. Оптимальный размер фрагмента ===" ) print("\nИсходный JPEG:") print(SOURCE_PATH) print("\nРазмер JPEG:") print( format_bytes(source_size) ) print("\nСкорость физического канала:") print( CHANNEL_BITRATE_BPS / 1000, "кбит/с", ) print("\nРазмер ACK-пакета:") print( ack_packet_size_bytes, "байт", ) # ============================================================ # Вывод результатов отдельно для каждого Eb/N0 # ============================================================ for eb_n0_db in EB_N0_VALUES_DB: selected_results = [ result for result in results if result["eb_n0_db"] == eb_n0_db ] print( f"\n--- Eb/N0 = {eb_n0_db:.1f} дБ ---" ) print( f"{'Фрагмент':>10}" f"{'Кол-во':>9}" f"{'P DATA':>12}" f"{'Попыток':>11}" f"{'Объём с ARQ':>16}" f"{'Время':>16}" f"{'Goodput':>13}" ) print("-" * 87) for result in selected_results: print( f"{result['fragment_size']:>8} Б" f"{result['fragment_count']:>9}" f"{result['minimum_data_success_probability']:>12.6f}" f"{result['average_data_transmissions']:>11.2f}" f"{format_bytes(result['expected_total_bytes']):>16}" f"{format_duration(result['expected_transfer_seconds']):>16}" f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с" ) optimum_result = min( selected_results, key=lambda item: ( item["expected_transfer_seconds"] ), ) print( "\nОптимальный размер фрагмента:" ) print( optimum_result["fragment_size"], "байт", ) print( "Ожидаемое время:", format_duration( optimum_result[ "expected_transfer_seconds" ] ), ) print( "Полезная скорость:", f"{optimum_result['effective_goodput_bps'] / 1000:.2f}", "кбит/с", ) # ============================================================ # Сохранение CSV # ============================================================ with CSV_PATH.open( "w", newline="", encoding="utf-8-sig", ) as csv_file: fieldnames = [ "fragment_size", "fragment_count", "eb_n0_db", "ber", "ideal_total_bytes", "ideal_efficiency_percent", "ack_success_probability", "minimum_data_success_probability", "maximum_data_success_probability", "average_data_transmissions", "expected_total_bytes", "expected_transfer_seconds", "expected_efficiency_percent", "effective_goodput_bps", ] writer = DictWriter( csv_file, fieldnames=fieldnames, ) writer.writeheader() writer.writerows(results) # ============================================================ # График ожидаемого времени передачи # ============================================================ plt.figure( figsize=(10, 7) ) for eb_n0_db in EB_N0_VALUES_DB: selected_results = [ result for result in results if result["eb_n0_db"] == eb_n0_db ] fragment_sizes = [ result["fragment_size"] for result in selected_results ] transfer_times = [ result["expected_transfer_seconds"] for result in selected_results ] plt.plot( fragment_sizes, transfer_times, marker="o", label=f"Eb/N0 = {eb_n0_db:.0f} дБ", ) plt.xscale( "log", base=2, ) plt.yscale( "log", ) plt.xlabel( "Размер данных фрагмента, байт" ) plt.ylabel( "Ожидаемое время передачи, с" ) plt.title( "Влияние размера фрагмента " "на время передачи JPEG" ) plt.grid( True, which="both", ) plt.legend() plt.tight_layout() plt.savefig( TIME_GRAPH_PATH, dpi=160, ) plt.close() # ============================================================ # График эффективной полезной скорости # ============================================================ plt.figure( figsize=(10, 7) ) for eb_n0_db in EB_N0_VALUES_DB: selected_results = [ result for result in results if result["eb_n0_db"] == eb_n0_db ] fragment_sizes = [ result["fragment_size"] for result in selected_results ] goodput_values = [ result["effective_goodput_bps"] / 1000 for result in selected_results ] plt.plot( fragment_sizes, goodput_values, marker="o", label=f"Eb/N0 = {eb_n0_db:.0f} дБ", ) plt.xscale( "log", base=2, ) plt.xlabel( "Размер данных фрагмента, байт" ) plt.ylabel( "Полезная скорость, кбит/с" ) plt.title( "Полезная скорость JPEG " "с учётом CRC и ARQ" ) plt.grid( True, which="both", ) plt.legend() plt.tight_layout() plt.savefig( GOODPUT_GRAPH_PATH, dpi=160, ) plt.close() # ============================================================ # Автоматические проверки # ============================================================ assert results assert CSV_PATH.exists() assert TIME_GRAPH_PATH.exists() assert GOODPUT_GRAPH_PATH.exists() assert all( result["expected_transfer_seconds"] > 0 for result in results ) assert all( 0.0 < result["ack_success_probability"] <= 1.0 for result in results ) print("\nCSV:") print(CSV_PATH) print("\nГрафик времени:") print(TIME_GRAPH_PATH) print("\nГрафик полезной скорости:") print(GOODPUT_GRAPH_PATH) print( "\nПроверка пройдена: " "оптимальный размер фрагмента рассчитан." )