Files
SDR-Rover/experiments/lab016_adaptive_fragment_size.py
LittleSam129 c486039053 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>
2026-08-10 14:34:58 +03:00

605 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Lab016. Адаптивный выбор размера фрагмента изображения.
Программа имитирует изменение качества канала во времени.
Для каждой оценки Eb/N0 передатчик решает:
- отключить изображения;
- использовать 128 байт;
- использовать 512 байт;
- использовать 1024 байта.
Дополнительно рассчитывается ожидаемое время передачи
реального JPEG-файла.
"""
from csv import DictWriter
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from protocol.image_fragments import (
encode_image_fragment,
split_image_bytes,
)
from protocol.link_adaptation import (
choose_image_mode,
packet_success_probability,
)
from protocol.packet import (
MESSAGE_TYPE_ACK,
MESSAGE_TYPE_IMAGE_FRAGMENT,
build_packet,
)
# ============================================================
# Настройки
# ============================================================
CHANNEL_BITRATE_BPS = 20_000
MAX_ATTEMPTS = 5
CANDIDATE_FRAGMENT_SIZES = (
128,
512,
1024,
)
# Изменение качества канала во времени.
EB_N0_PROFILE_DB = [
12.0,
10.0,
9.0,
8.0,
7.0,
6.0,
7.0,
8.0,
9.0,
10.0,
12.0,
]
OUTPUT_DIRECTORY = Path(
"data/processed/lab016"
)
OUTPUT_DIRECTORY.mkdir(
parents=True,
exist_ok=True,
)
CSV_PATH = (
OUTPUT_DIRECTORY
/ "lab016_adaptation_results.csv"
)
MODE_GRAPH_PATH = (
OUTPUT_DIRECTORY
/ "lab016_selected_mode.png"
)
TIME_GRAPH_PATH = (
OUTPUT_DIRECTORY
/ "lab016_expected_transfer_time.png"
)
# ============================================================
# Выбор исходного кадра
# ============================================================
source_candidates = [
Path(
"data/processed/lab012/"
"03_color_320_q15.jpg"
),
Path(
"data/raw/lab009_source.jpg"
),
]
SOURCE_PATH = next(
(
path
for path in source_candidates
if path.exists()
),
None,
)
if SOURCE_PATH is None:
raise FileNotFoundError(
"Не найден кадр для передачи. "
"Необходимо выполнить Lab009 или Lab012."
)
source_bytes = SOURCE_PATH.read_bytes()
if not source_bytes:
raise ValueError(
"Исходный JPEG пуст"
)
# ============================================================
# Оценка передачи всего изображения
# ============================================================
def estimate_image_transfer(
image_bytes: bytes,
fragment_size: int,
ber: float,
image_id: int,
) -> dict:
"""
Оценить передачу полного изображения с бесконечным ARQ.
Расчёт учитывает фактический размер последнего фрагмента.
"""
fragments = split_image_bytes(
image_bytes=image_bytes,
image_id=image_id,
fragment_data_size=fragment_size,
)
ack_packet = build_packet(
payload=b"",
message_type=MESSAGE_TYPE_ACK,
sequence_number=0,
)
ack_size_bytes = len(
ack_packet
)
ack_success_probability = (
packet_success_probability(
ber=ber,
packet_bit_count=(
ack_size_bytes * 8
),
)
)
expected_total_bytes = 0.0
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_success_probability = (
packet_success_probability(
ber=ber,
packet_bit_count=(
len(data_packet) * 8
),
)
)
confirmed_probability = (
data_success_probability
* ack_success_probability
)
expected_total_bytes += (
len(data_packet)
/ confirmed_probability
)
expected_total_bytes += (
ack_size_bytes
/ ack_success_probability
)
expected_seconds = (
expected_total_bytes
* 8
/ CHANNEL_BITRATE_BPS
)
effective_goodput_bps = (
len(image_bytes)
* 8
/ expected_seconds
)
return {
"fragment_count": len(fragments),
"expected_total_bytes": expected_total_bytes,
"expected_seconds": expected_seconds,
"effective_goodput_bps": (
effective_goodput_bps
),
}
# ============================================================
# Адаптация по профилю канала
# ============================================================
results = []
for step_index, eb_n0_db in enumerate(
EB_N0_PROFILE_DB
):
decision = choose_image_mode(
eb_n0_db=eb_n0_db,
candidate_fragment_sizes=(
CANDIDATE_FRAGMENT_SIZES
),
channel_bitrate_bps=(
CHANNEL_BITRATE_BPS
),
max_attempts=MAX_ATTEMPTS,
minimum_success_probability=0.85,
minimum_goodput_bps=2_000.0,
)
if decision.images_enabled:
selected_estimate = next(
estimate
for estimate in decision.estimates
if (
estimate.fragment_size
== decision.selected_fragment_size
)
)
image_result = estimate_image_transfer(
image_bytes=source_bytes,
fragment_size=(
decision.selected_fragment_size
),
ber=selected_estimate.ber,
image_id=2026071700 + step_index,
)
fragment_size = (
decision.selected_fragment_size
)
fragment_count = (
image_result["fragment_count"]
)
expected_seconds = (
image_result["expected_seconds"]
)
effective_goodput_bps = (
image_result[
"effective_goodput_bps"
]
)
success_with_retries = (
selected_estimate
.success_probability_with_retries
)
expected_attempts = (
selected_estimate.expected_attempts
)
mode_name = (
f"{fragment_size} B"
)
else:
fragment_size = 0
fragment_count = 0
expected_seconds = None
effective_goodput_bps = 0.0
success_with_retries = 0.0
expected_attempts = 0.0
mode_name = "IMAGE OFF"
results.append(
{
"step": step_index,
"eb_n0_db": eb_n0_db,
"mode": mode_name,
"fragment_size": fragment_size,
"fragment_count": fragment_count,
"expected_seconds": expected_seconds,
"effective_goodput_bps": (
effective_goodput_bps
),
"success_with_retries": (
success_with_retries
),
"expected_attempts": (
expected_attempts
),
"reason": decision.reason,
}
)
# ============================================================
# Вывод
# ============================================================
print(
"=== Lab016. Адаптация размера фрагмента ==="
)
print("\nИсходный кадр:")
print(SOURCE_PATH)
print("\nРазмер JPEG:")
print(
len(source_bytes),
"байт",
)
print("\nРезультаты адаптации:")
print(
f"{'Шаг':>5}"
f"{'Eb/N0':>10}"
f"{'Режим':>14}"
f"{'Фрагм.':>9}"
f"{'Попыток':>11}"
f"{'Успех x5':>12}"
f"{'Время кадра':>15}"
f"{'Goodput':>13}"
)
print("-" * 89)
for result in results:
if result["expected_seconds"] is None:
time_text = ""
else:
time_text = (
f"{result['expected_seconds']:.2f} с"
)
print(
f"{result['step']:>5}"
f"{result['eb_n0_db']:>7.1f} дБ"
f"{result['mode']:>14}"
f"{result['fragment_count']:>9}"
f"{result['expected_attempts']:>11.2f}"
f"{result['success_with_retries'] * 100:>10.1f} %"
f"{time_text:>15}"
f"{result['effective_goodput_bps'] / 1000:>10.2f} кбит/с"
)
# ============================================================
# Сохранение CSV
# ============================================================
with CSV_PATH.open(
"w",
newline="",
encoding="utf-8-sig",
) as csv_file:
fieldnames = list(
results[0].keys()
)
writer = DictWriter(
csv_file,
fieldnames=fieldnames,
)
writer.writeheader()
writer.writerows(results)
# ============================================================
# График выбранного режима
# ============================================================
steps = [
result["step"]
for result in results
]
fragment_sizes = [
result["fragment_size"]
for result in results
]
plt.figure(
figsize=(11, 6)
)
plt.step(
steps,
fragment_sizes,
where="mid",
marker="o",
)
plt.yticks(
[
0,
128,
512,
1024,
],
[
"IMAGE OFF",
"128 B",
"512 B",
"1024 B",
],
)
plt.xlabel(
"Шаг времени"
)
plt.ylabel(
"Выбранный режим"
)
plt.title(
"Автоматический выбор размера фрагмента"
)
plt.grid(
True
)
plt.tight_layout()
plt.savefig(
MODE_GRAPH_PATH,
dpi=160,
)
plt.close()
# ============================================================
# График времени передачи кадра
# ============================================================
transfer_times = np.array(
[
(
result["expected_seconds"]
if result["expected_seconds"] is not None
else np.nan
)
for result in results
],
dtype=np.float64,
)
plt.figure(
figsize=(11, 6)
)
plt.plot(
steps,
transfer_times,
marker="o",
)
plt.xlabel(
"Шаг времени"
)
plt.ylabel(
"Ожидаемое время передачи кадра, с"
)
plt.title(
"Время передачи кадра при адаптации канала"
)
plt.grid(
True
)
plt.tight_layout()
plt.savefig(
TIME_GRAPH_PATH,
dpi=160,
)
plt.close()
# ============================================================
# Проверки ожидаемой логики
# ============================================================
decision_at_6_db = next(
result
for result in results
if result["eb_n0_db"] == 6.0
)
decision_at_8_db = next(
result
for result in results
if result["eb_n0_db"] == 8.0
)
decision_at_9_db = next(
result
for result in results
if result["eb_n0_db"] == 9.0
)
decision_at_10_db = next(
result
for result in results
if result["eb_n0_db"] == 10.0
)
assert (
decision_at_6_db["fragment_size"]
== 0
)
assert (
decision_at_8_db["fragment_size"]
== 128
)
assert (
decision_at_9_db["fragment_size"]
== 512
)
assert (
decision_at_10_db["fragment_size"]
== 1024
)
assert CSV_PATH.exists()
assert MODE_GRAPH_PATH.exists()
assert TIME_GRAPH_PATH.exists()
print("\nCSV:")
print(CSV_PATH)
print("\nГрафик режимов:")
print(MODE_GRAPH_PATH)
print("\nГрафик времени:")
print(TIME_GRAPH_PATH)
print(
"\nПроверка пройдена: "
"адаптивный выбор режима работает."
)