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>
This commit is contained in:
LittleSam129
2026-08-10 14:34:58 +03:00
parent c9569164e0
commit c486039053
55 changed files with 63 additions and 62 deletions

View File

@@ -0,0 +1,450 @@
"""
Lab009. Расчёт параметров передачи реальной фотографии.
Программа:
1. Читает реальный JPEG-файл.
2. Определяет его разрешение и размер.
3. Разбивает файл на фрагменты.
4. Формирует реальные пакеты протокола.
5. Учитывает ACK для каждого фрагмента.
6. Рассчитывает время передачи при разных скоростях канала.
Расчёт времени показывает чистое эфирное время без учёта:
- пауз между пакетами;
- обработки на передатчике и приёмнике;
- ожидания ACK;
- повторных передач;
- преамбулы и служебных символов физического уровня.
"""
from pathlib import Path
from PIL import Image
from protocol.image_fragments import (
FRAGMENT_HEADER_SIZE,
encode_image_fragment,
split_image_bytes,
)
from protocol.packet import (
CRC_SIZE,
HEADER_SIZE,
MESSAGE_TYPE_ACK,
MESSAGE_TYPE_IMAGE_FRAGMENT,
build_packet,
)
# ============================================================
# Настройки
# ============================================================
SOURCE_PATH = Path(
"data/raw/lab009_source.jpg"
)
IMAGE_ID = 20260713
# Сколько байтов JPEG помещается в один фрагмент.
FRAGMENT_DATA_SIZE = 512
# Скорости полезного цифрового канала в кбит/с.
BITRATES_KBPS = [
1,
5,
10,
20,
50,
100,
200,
]
# ============================================================
# Вспомогательные функции
# ============================================================
def format_bytes(byte_count: int) -> str:
"""
Представить размер одновременно в байтах и килобайтах.
"""
kibibytes = byte_count / 1024
return (
f"{byte_count} байт "
f"({kibibytes:.2f} КиБ)"
)
def format_duration(seconds: float) -> str:
"""
Представить длительность в удобной форме.
"""
if seconds < 1:
return f"{seconds * 1000:.1f} мс"
if seconds < 60:
return f"{seconds:.2f} с"
minutes = int(seconds // 60)
remaining_seconds = seconds % 60
return (
f"{minutes} мин "
f"{remaining_seconds:.1f} с"
)
# ============================================================
# Проверка исходного файла
# ============================================================
if not SOURCE_PATH.exists():
raise FileNotFoundError(
f"Не найден файл: {SOURCE_PATH}"
)
source_bytes = SOURCE_PATH.read_bytes()
if not source_bytes:
raise ValueError(
"Исходный JPEG-файл пуст"
)
# ============================================================
# Чтение параметров изображения
# ============================================================
with Image.open(SOURCE_PATH) as image:
image_format = image.format
image_mode = image.mode
image_width, image_height = image.size
# Проверяем, что файл действительно читается как изображение.
image.verify()
# ============================================================
# Разбиение JPEG на фрагменты
# ============================================================
fragments = split_image_bytes(
image_bytes=source_bytes,
image_id=IMAGE_ID,
fragment_data_size=FRAGMENT_DATA_SIZE,
)
# ============================================================
# Формирование DATA-пакетов
# ============================================================
data_packets = []
for fragment in fragments:
fragment_payload = encode_image_fragment(
fragment
)
packet = build_packet(
payload=fragment_payload,
message_type=MESSAGE_TYPE_IMAGE_FRAGMENT,
sequence_number=fragment.fragment_index,
)
data_packets.append(packet)
# ============================================================
# Формирование типового ACK-пакета
# ============================================================
ack_packet = build_packet(
payload=b"",
message_type=MESSAGE_TYPE_ACK,
sequence_number=0,
)
ack_packet_size = len(ack_packet)
# ============================================================
# Расчёт объёмов
# ============================================================
source_size = len(source_bytes)
fragment_count = len(fragments)
# Полный размер всех DATA-пакетов.
total_data_packet_bytes = sum(
len(packet)
for packet in data_packets
)
# Для каждого DATA-пакета ожидается один ACK.
total_ack_bytes = (
fragment_count
* ack_packet_size
)
# Идеальный радиообмен:
# каждый DATA и каждый ACK переданы ровно один раз.
total_radio_bytes = (
total_data_packet_bytes
+ total_ack_bytes
)
total_radio_bits = (
total_radio_bytes
* 8
)
# ============================================================
# Детализация служебных расходов
# ============================================================
total_fragment_header_bytes = (
fragment_count
* FRAGMENT_HEADER_SIZE
)
data_protocol_overhead = (
fragment_count
* (HEADER_SIZE + CRC_SIZE)
)
total_overhead_bytes = (
total_radio_bytes
- source_size
)
payload_efficiency = (
source_size
/ total_radio_bytes
* 100
)
# ============================================================
# Вывод параметров фотографии
# ============================================================
print("=== Lab009. Расчёт передачи реальной фотографии ===")
print("\nИсходный файл:")
print(SOURCE_PATH)
print("\nФормат изображения:")
print(image_format)
print("\nЦветовой режим:")
print(image_mode)
print("\nРазрешение:")
print(
image_width,
"x",
image_height,
"пикселей",
)
print("\nРазмер исходного JPEG:")
print(
format_bytes(source_size)
)
# ============================================================
# Вывод структуры передачи
# ============================================================
print("\n--- Фрагментация ---")
print(
"Размер данных одного полного фрагмента:",
FRAGMENT_DATA_SIZE,
"байт",
)
print(
"Количество фрагментов:",
fragment_count,
)
print(
"Размер заголовка фрагмента:",
FRAGMENT_HEADER_SIZE,
"байт",
)
print(
"Размер заголовка основного пакета:",
HEADER_SIZE,
"байт",
)
print(
"Размер CRC-32:",
CRC_SIZE,
"байт",
)
print(
"Размер ACK-пакета:",
ack_packet_size,
"байт",
)
# ============================================================
# Вывод полного объёма
# ============================================================
print("\n--- Объём передачи без повторов ---")
print("Исходные данные JPEG:")
print(
format_bytes(source_size)
)
print("\nЗаголовки фрагментов:")
print(
format_bytes(total_fragment_header_bytes)
)
print("\nСлужебные поля и CRC DATA-пакетов:")
print(
format_bytes(data_protocol_overhead)
)
print("\nВсе DATA-пакеты:")
print(
format_bytes(total_data_packet_bytes)
)
print("\nВсе ACK-пакеты:")
print(
format_bytes(total_ack_bytes)
)
print("\nПолный радиообмен DATA + ACK:")
print(
format_bytes(total_radio_bytes)
)
print("\nОбщие служебные расходы:")
print(
format_bytes(total_overhead_bytes)
)
print(
"\nПолезная эффективность протокола:"
)
print(
f"{payload_efficiency:.2f} %"
)
# ============================================================
# Расчёт времени передачи
# ============================================================
print("\n--- Чистое эфирное время ---")
print(
"Скорость DATA без ACK DATA + ACK"
)
for bitrate_kbps in BITRATES_KBPS:
bitrate_bits_per_second = (
bitrate_kbps
* 1000
)
data_time_seconds = (
total_data_packet_bytes
* 8
/ bitrate_bits_per_second
)
full_time_seconds = (
total_radio_bits
/ bitrate_bits_per_second
)
print(
f"{bitrate_kbps:>4} кбит/с"
f" "
f"{format_duration(data_time_seconds):>12}"
f" "
f"{format_duration(full_time_seconds):>12}"
)
# ============================================================
# Сценарий с повторами
# ============================================================
# Коэффициент 2 означает, что суммарно передано
# примерно в два раза больше байтов из-за ошибок,
# потерь и повторных передач.
RETRY_TRAFFIC_MULTIPLIER = 2.0
print(
"\n--- Оценка при удвоении трафика из-за повторов ---"
)
for bitrate_kbps in BITRATES_KBPS:
bitrate_bits_per_second = (
bitrate_kbps
* 1000
)
estimated_seconds = (
total_radio_bits
* RETRY_TRAFFIC_MULTIPLIER
/ bitrate_bits_per_second
)
print(
f"{bitrate_kbps:>4} кбит/с"
f" "
f"{format_duration(estimated_seconds)}"
)
# ============================================================
# Автоматические проверки
# ============================================================
assert fragment_count > 0
assert (
total_data_packet_bytes
> source_size
)
assert (
total_radio_bytes
> total_data_packet_bytes
)
assert (
0 < payload_efficiency < 100
)
print(
"\nПроверка пройдена: "
"параметры передачи фотографии рассчитаны."
)