""" Адаптация параметров SDR Rover Link к качеству канала. Модуль оценивает несколько размеров фрагмента изображения и выбирает режим с максимальной полезной скоростью. Используемая модель: - когерентная BPSK; - канал AWGN; - независимые битовые ошибки; - CRC отбрасывает повреждённые пакеты; - Stop-and-Wait ARQ; - одинаковое Eb/N0 для DATA и ACK. """ from dataclasses import dataclass from math import ( erfc, exp, log1p, sqrt, ) from protocol.image_fragments import ( FRAGMENT_HEADER_SIZE, ) from protocol.packet import ( CRC_SIZE, HEADER_SIZE, ) # ============================================================ # Результат оценки одного размера фрагмента # ============================================================ @dataclass(frozen=True) class FragmentModeEstimate: """ Оценка одного возможного размера фрагмента. """ fragment_size: int ber: float data_packet_size_bytes: int ack_packet_size_bytes: int data_success_probability: float ack_success_probability: float confirmed_attempt_probability: float expected_attempts: float success_probability_with_retries: float expected_goodput_bps: float # ============================================================ # Итоговое решение адаптера # ============================================================ @dataclass(frozen=True) class LinkDecision: """ Решение о режиме передачи изображения. """ eb_n0_db: float images_enabled: bool selected_fragment_size: int | None reason: str estimates: tuple[FragmentModeEstimate, ...] # ============================================================ # Теоретический BER # ============================================================ 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 = (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 1.0 if ber == 1.0: return 0.0 return exp( packet_bit_count * log1p(-ber) ) # ============================================================ # Оценка отдельного режима # ============================================================ def evaluate_fragment_mode( eb_n0_db: float, fragment_size: int, channel_bitrate_bps: int, max_attempts: int, ) -> FragmentModeEstimate: """ Оценить передачу полного фрагмента заданного размера. """ if fragment_size <= 0: raise ValueError( "fragment_size должен быть положительным" ) if channel_bitrate_bps <= 0: raise ValueError( "channel_bitrate_bps должен быть положительным" ) if max_attempts <= 0: raise ValueError( "max_attempts должен быть положительным" ) ber = theoretical_bpsk_ber( eb_n0_db ) # DATA содержит: # # заголовок протокола; # заголовок фрагмента изображения; # данные JPEG; # CRC-32. data_packet_size_bytes = ( HEADER_SIZE + FRAGMENT_HEADER_SIZE + fragment_size + CRC_SIZE ) # ACK содержит только заголовок протокола и CRC. ack_packet_size_bytes = ( HEADER_SIZE + CRC_SIZE ) data_success_probability = ( packet_success_probability( ber=ber, packet_bit_count=( data_packet_size_bytes * 8 ), ) ) ack_success_probability = ( packet_success_probability( ber=ber, packet_bit_count=( ack_packet_size_bytes * 8 ), ) ) # Одна попытка считается завершённой, # только если правильно прошли DATA и ACK. confirmed_attempt_probability = ( data_success_probability * ack_success_probability ) if confirmed_attempt_probability == 0.0: expected_attempts = float("inf") expected_goodput_bps = 0.0 else: expected_attempts = ( 1.0 / confirmed_attempt_probability ) # Ожидаемый объём DATA до подтверждения. expected_data_bytes = ( data_packet_size_bytes / confirmed_attempt_probability ) # ACK отправляется только после исправного DATA. expected_ack_bytes = ( ack_packet_size_bytes / ack_success_probability ) expected_total_bytes = ( expected_data_bytes + expected_ack_bytes ) expected_goodput_bps = ( channel_bitrate_bps * fragment_size / expected_total_bytes ) success_probability_with_retries = ( 1.0 - ( 1.0 - confirmed_attempt_probability ) ** max_attempts ) return FragmentModeEstimate( fragment_size=fragment_size, ber=ber, data_packet_size_bytes=data_packet_size_bytes, ack_packet_size_bytes=ack_packet_size_bytes, data_success_probability=( data_success_probability ), ack_success_probability=( ack_success_probability ), confirmed_attempt_probability=( confirmed_attempt_probability ), expected_attempts=expected_attempts, success_probability_with_retries=( success_probability_with_retries ), expected_goodput_bps=( expected_goodput_bps ), ) # ============================================================ # Выбор режима # ============================================================ def choose_image_mode( eb_n0_db: float, candidate_fragment_sizes: tuple[int, ...] = ( 128, 512, 1024, ), channel_bitrate_bps: int = 20_000, max_attempts: int = 5, minimum_success_probability: float = 0.85, minimum_goodput_bps: float = 2_000.0, ) -> LinkDecision: """ Выбрать режим передачи изображения. Режим допускается, если: 1. вероятность подтверждения за max_attempts не ниже minimum_success_probability; 2. ожидаемый goodput не ниже minimum_goodput_bps. Из подходящих режимов выбирается режим с максимальной полезной скоростью. """ if not candidate_fragment_sizes: raise ValueError( "Список размеров фрагментов пуст" ) if not 0.0 <= minimum_success_probability <= 1.0: raise ValueError( "minimum_success_probability должен быть 0...1" ) if minimum_goodput_bps < 0.0: raise ValueError( "minimum_goodput_bps не может быть отрицательным" ) estimates = tuple( evaluate_fragment_mode( eb_n0_db=eb_n0_db, fragment_size=fragment_size, channel_bitrate_bps=channel_bitrate_bps, max_attempts=max_attempts, ) for fragment_size in candidate_fragment_sizes ) eligible_estimates = [ estimate for estimate in estimates if ( estimate.success_probability_with_retries >= minimum_success_probability and estimate.expected_goodput_bps >= minimum_goodput_bps ) ] if not eligible_estimates: return LinkDecision( eb_n0_db=eb_n0_db, images_enabled=False, selected_fragment_size=None, reason=( "Канал недостаточен для надёжной " "передачи изображения" ), estimates=estimates, ) selected_estimate = max( eligible_estimates, key=lambda estimate: ( estimate.expected_goodput_bps ), ) return LinkDecision( eb_n0_db=eb_n0_db, images_enabled=True, selected_fragment_size=( selected_estimate.fragment_size ), reason=( "Выбран режим с максимальной " "ожидаемой полезной скоростью" ), estimates=estimates, )