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>
811 lines
18 KiB
Python
811 lines
18 KiB
Python
"""
|
||
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Проверка пройдена: "
|
||
"оптимальный размер фрагмента рассчитан."
|
||
)
|