initial commit

This commit is contained in:
LittleSam129
2026-07-20 13:58:58 +03:00
commit acf7a76546
104 changed files with 27464 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
"""
Адаптация параметров 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,
)