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

@@ -7,7 +7,7 @@ model: sonnet
Ты проверяешь чужую работу в проекте SDR Rover Link: учебно-исследовательский
цифровой радиоканал для наземного ровера. Python, модули протокола в `protocol/`,
лабораторные работы в `tests/labNNN_*.py`, журнал проекта в `PROJECT_LOG.md`.
лабораторные работы в `experiments/labNNN_*.py`, журнал проекта в `PROJECT_LOG.md`.
Ты не автор кода. Твоя ценность именно в том, что ты не участвовал в его написании.
@@ -18,7 +18,7 @@ model: sonnet
Если пересказ расходится с первоисточником — это находка, и часто самая важная.
2. **Запусти сам.** Быстрая проверка: `python tools/quick_gate.py`.
Полный прогон лабы: `python tests/labNNN_имя.py`. Не верь чужому «тесты проходят».
Полный прогон лабы: `python experiments/labNNN_имя.py`. Не верь чужому «тесты проходят».
3. **Пересчитай ключевые числа независимым способом.** Это главный инструмент.
Симуляция BER проверяется аналитической формулой, счётчик отброшенных пакетов —

View File

@@ -22,16 +22,16 @@
| Звено | Где взять |
|---|---|
| Разбиение JPEG на фрагменты и сборка обратно | `protocol/image_fragments.py`, примеры в `tests/lab007_image_fragmentation.py` |
| Разбиение JPEG на фрагменты и сборка обратно | `protocol/image_fragments.py`, примеры в `experiments/lab007_image_fragmentation.py` |
| Пакет с CRC32, кодирование и разбор | `protocol/packet.py` |
| Подбор размера и качества JPEG | `tests/lab010_image_optimization.py`, готовые профили в `data/processed/lab010` |
| Формирование радиокадра с преамбулой и маркером | `build_radio_frame` в `tests/lab018_bpsk_radio_frame.py` |
| Подбор размера и качества JPEG | `experiments/lab010_image_optimization.py`, готовые профили в `data/processed/lab010` |
| Формирование радиокадра с преамбулой и маркером | `build_radio_frame` в `experiments/lab018_bpsk_radio_frame.py` |
| Фильтр приподнятого косинуса | `root_raised_cosine_taps` там же |
| Модуляция BPSK | `bpsk_modulate` там же |
| Поиск кадра в потоке отсчётов | `find_radio_frame` в `tests/lab019_bpsk_receiver.py` |
| Поиск кадра в потоке отсчётов | `find_radio_frame` в `experiments/lab019_bpsk_receiver.py` |
| Оценка и коррекция ухода частоты | `estimate_carrier_parameters`, `correct_phase_and_frequency` там же |
| Демодуляция и разбор кадра | `bpsk_demodulate`, `decode_radio_frame` там же |
| Работа с Pluto на приём | `tests/lab024a_pluto_rx_capture.py` |
| Работа с Pluto на приём | `experiments/lab024a_pluto_rx_capture.py` |
**Единственное, чего нет в проекте — передача.** Это и есть основной объём работы.
@@ -250,7 +250,7 @@
- Параметры тракта из раздела 5 не менять.
- Все константы вынести в начало файла с указанием источника, как это сделано в Lab041.
- Никаких вписанных руками результатов: каждое число в отчёте должно вычисляться из измерений текущего запуска. Это отдельно проверяется при приёмке.
- Файл лабораторной: `tests/lab042_pluto_image_loopback.py`, запуск через `python -m tests.lab042_pluto_image_loopback` из корня проекта.
- Файл лабораторной: `experiments/lab042_pluto_image_loopback.py`, запуск через `python -m experiments.lab042_pluto_image_loopback` из корня проекта.
---

0
experiments/__init__.py Normal file
View File

View File

@@ -27,7 +27,7 @@ from protocol.video_packet import (
PacketCRCError,
VideoPacketError,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
EncodedComposite,
SOURCE_VIDEO_PATH,
VideoMetadata,

View File

@@ -32,14 +32,14 @@ from protocol.video_packet import (
ObjectType,
decode_packet,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
EncodedComposite,
SOURCE_VIDEO_PATH,
VideoMetadata,
load_video_profile,
)
from tests.lab029_packet_channel_simulation import (
from experiments.lab029_packet_channel_simulation import (
PAYLOAD_SIZES,
PreparedProfile,
prepare_profiles,

View File

@@ -44,18 +44,18 @@ from protocol.video_packet import (
ObjectType,
decode_packet as decode_inner_packet,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
SOURCE_VIDEO_PATH,
EncodedComposite,
VideoMetadata,
load_video_profile,
)
from tests.lab029_packet_channel_simulation import (
from experiments.lab029_packet_channel_simulation import (
PreparedProfile,
prepare_profiles,
)
from tests.lab029b_time_based_burst_simulation import (
from experiments.lab029b_time_based_burst_simulation import (
BAD_TIME_FRACTION,
CONTROL_STREAM_BITRATE_BPS,
CONTROL_STREAM_BITRATE_KBPS,

View File

@@ -36,18 +36,18 @@ from protocol.video_packet import (
CompositeReassembler,
decode_packet as decode_inner_packet,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
SOURCE_VIDEO_PATH,
EncodedComposite,
VideoMetadata,
load_video_profile,
)
from tests.lab029_packet_channel_simulation import (
from experiments.lab029_packet_channel_simulation import (
PreparedProfile,
prepare_profiles,
)
from tests.lab029b_time_based_burst_simulation import (
from experiments.lab029b_time_based_burst_simulation import (
BAD_TIME_FRACTION,
CONTROL_STREAM_BITRATE_BPS,
CONTROL_STREAM_BITRATE_KBPS,
@@ -55,7 +55,7 @@ from tests.lab029b_time_based_burst_simulation import (
generate_bad_intervals,
percentile,
)
from tests.lab030_packet_erasure_fec import (
from experiments.lab030_packet_erasure_fec import (
FECBlockPlan,
FECMode,
FunctionalTestResult,

View File

@@ -34,18 +34,18 @@ from protocol.video_packet import (
CompositeReassembler,
decode_packet as decode_inner_packet,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
SOURCE_VIDEO_PATH,
EncodedComposite,
VideoMetadata,
load_video_profile,
)
from tests.lab029_packet_channel_simulation import (
from experiments.lab029_packet_channel_simulation import (
PreparedProfile,
prepare_profiles,
)
from tests.lab029b_time_based_burst_simulation import (
from experiments.lab029b_time_based_burst_simulation import (
BAD_TIME_FRACTION,
CONTROL_STREAM_BITRATE_BPS,
CONTROL_STREAM_BITRATE_KBPS,
@@ -53,7 +53,7 @@ from tests.lab029b_time_based_burst_simulation import (
generate_bad_intervals,
percentile,
)
from tests.lab030_packet_erasure_fec import (
from experiments.lab030_packet_erasure_fec import (
FECBlockPlan,
FECMode,
FunctionalTestResult,

View File

@@ -47,21 +47,21 @@ from protocol.video_packet import (
CompositeReassembler,
decode_packet as decode_inner_packet,
)
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
SOURCE_VIDEO_PATH,
EncodedComposite,
VideoMetadata,
load_video_profile,
)
from tests.lab029_packet_channel_simulation import prepare_profiles
from tests.lab030_packet_erasure_fec import (
from experiments.lab029_packet_channel_simulation import prepare_profiles
from experiments.lab030_packet_erasure_fec import (
FECBlockPlan,
SourcePacket,
TransmissionUnit,
prepare_source_packets,
)
from tests.lab032_fec_parameter_sweep import (
from experiments.lab032_fec_parameter_sweep import (
SweepMode,
build_parameter_units,
)
@@ -994,7 +994,7 @@ def write_report(
"8. Созданные файлы и итоговый Git status",
"- protocol/link_packet.py",
"- protocol/priority_scheduler.py",
"- tests/lab033_priority_channel_scheduler.py",
"- experiments/lab033_priority_channel_scheduler.py",
"- data/processed/lab033/lab033_summary.csv",
"- data/processed/lab033/lab033_class_metrics.csv",
"- data/processed/lab033/lab033_report.txt",

View File

@@ -34,10 +34,10 @@ from protocol.video_age_policy import (
schedule_with_video_age,
)
from protocol.video_packet import CompositeReassembler, decode_packet as decode_inner_packet
from tests.lab028_video_packetization import COMPOSITE_FPS
from tests.lab029_packet_channel_simulation import prepare_profiles
from tests.lab030_packet_erasure_fec import SourcePacket, prepare_source_packets
from tests.lab033_priority_channel_scheduler import (
from experiments.lab028_video_packetization import COMPOSITE_FPS
from experiments.lab029_packet_channel_simulation import prepare_profiles
from experiments.lab030_packet_erasure_fec import SourcePacket, prepare_source_packets
from experiments.lab033_priority_channel_scheduler import (
CHANNEL_RATES_KBPS,
STREAM_VIDEO,
VIDEO_PAYLOAD_SIZE,
@@ -982,7 +982,7 @@ def write_report(lab033, continuous, aligned, results, tests):
])
lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests)
lines.extend(["", "8. Созданные файлы"])
lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_age_policy.py"), Path("tests/lab034_stale_video_drop.py"), SUMMARY_CSV_PATH, VIDEO_CSV_PATH, CONTROL_CSV_PATH, REPORT_PATH, *PLOT_PATHS))
lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_age_policy.py"), Path("experiments/lab034_stale_video_drop.py"), SUMMARY_CSV_PATH, VIDEO_CSV_PATH, CONTROL_CSV_PATH, REPORT_PATH, *PLOT_PATHS))
lines.extend([
"",
"9. Итоговый Git status",

View File

@@ -25,8 +25,8 @@ from protocol.video_frame_scheduler import (
schedule_video_frames,
)
from protocol.video_packet import CompositeReassembler, decode_packet as decode_inner_packet
from tests.lab028_video_packetization import COMPOSITE_FPS
from tests.lab034_stale_video_drop import (
from experiments.lab028_video_packetization import COMPOSITE_FPS
from experiments.lab034_stale_video_drop import (
PolicyDefinition as Lab034Policy,
build_aligned_workload,
build_lab033_workload,
@@ -721,7 +721,7 @@ def write_report(aligned,results,tests):
lines.extend(["","5. Интерпретация","- Реактивная Lab034 начинает кадр без гарантии завершения, затем удаляет остаток: уже переданные байты становятся бесполезными, а обновление не публикуется.","- Удаление до первого пакета исключает бесполезную передачу; обслуживание целыми кадрами гарантирует, что начатый кадр будет опубликован.","- Политика самого свежего уменьшает задержку ожидающих данных, но удаляет больше промежуточных кадров; очередь из двух кадров сохраняет больше последовательных обновлений ценой возраста.","- Прогноз полного завершения учитывает весь размер кадра и будущую периодическую высокоприоритетную нагрузку, поэтому полезнее проверки только текущего возраста.","- В модели точно известны команды 20 Гц, телеметрия 10 Гц и аварийная команда 10,0 с; неизвестные будущие дискретные события не моделируются и в реальной системе потребовали бы запаса.","- При устойчивой перегрузке невозможно одновременно сохранить все кадры, исходное JPEG-качество и малую задержку; требуется уменьшить частоту, качество или заранее пропускать кадры.","- Частота обновления, возраст изображения и длительность отсутствия нового изображения оцениваются одновременно: оптимизация одного показателя может ухудшить остальные.","","6. Допущения","- Ошибки и помехи отсутствуют; один общий абстрактный ресурс, форматы Lab028-Lab034 неизменны, активный пакет не прерывается.","- Прогноз не изменяет настоящую очередь; для допущенных кадров сохраняются только агрегированные ошибки, без подробного журнала.","- Политика автоматически не выбирается.","","7. Функциональные проверки"])
lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests)
lines.extend(["","8. Созданные файлы"])
lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_frame_scheduler.py"),Path("tests/lab035_video_frame_admission.py"),SUMMARY_CSV_PATH,VIDEO_CSV_PATH,CONTROL_CSV_PATH,PREDICTION_CSV_PATH,REPORT_PATH,*PLOT_PATHS))
lines.extend(f"- {path.as_posix()}" for path in (Path("protocol/video_frame_scheduler.py"),Path("experiments/lab035_video_frame_admission.py"),SUMMARY_CSV_PATH,VIDEO_CSV_PATH,CONTROL_CSV_PATH,PREDICTION_CSV_PATH,REPORT_PATH,*PLOT_PATHS))
lines.extend(["","9. Итоговый Git status","- Lab035 не добавлена в индекс и не закоммичена.","",git_status])
REPORT_PATH.write_text("\n".join(lines)+"\n",encoding="utf-8")

View File

@@ -31,12 +31,12 @@ from protocol.packet_erasure_fec import (
from protocol.video_age_policy import AgePolicyPacket
from protocol.video_frame_scheduler import FramePolicy, VideoFrameGroup
from protocol.video_packet import decode_packet as decode_inner_packet
from tests.lab028_video_packetization import (
from experiments.lab028_video_packetization import (
COMPOSITE_FPS,
EncodedComposite,
packets_for_composite,
)
from tests.lab033_priority_channel_scheduler import (
from experiments.lab033_priority_channel_scheduler import (
STREAM_CONTROL,
STREAM_EMERGENCY,
STREAM_TELEMETRY,
@@ -44,7 +44,7 @@ from tests.lab033_priority_channel_scheduler import (
build_workload as build_lab033_workload,
deterministic_payload,
)
from tests.lab034_stale_video_drop import parity_for_partial
from experiments.lab034_stale_video_drop import parity_for_partial
OUTPUT_DIRECTORY = Path("data/processed/lab036")
@@ -1318,7 +1318,7 @@ def write_report(
f"- CRC-проверено общих пакетов: {workload.crc_packets_checked}; FEC-блоков: {workload.crc_blocks_checked}.",
"",
"7. Созданные файлы",
"- tests/lab036_long_duration_scheduler.py",
"- experiments/lab036_long_duration_scheduler.py",
"- data/processed/lab036/lab036_summary.csv",
"- data/processed/lab036/lab036_video_metrics.csv",
"- data/processed/lab036/lab036_control_metrics.csv",

View File

@@ -42,8 +42,8 @@ from protocol.packet_erasure_fec import (
from protocol.video_packet import decode_packet as decode_inner_packet
from protocol.video_frame_scheduler import VideoFrameGroup
from protocol.video_age_policy import AgePolicyPacket
from tests.lab028_video_packetization import COMPOSITE_FPS, EncodedComposite, packets_for_composite
from tests.lab033_priority_channel_scheduler import (
from experiments.lab028_video_packetization import COMPOSITE_FPS, EncodedComposite, packets_for_composite
from experiments.lab033_priority_channel_scheduler import (
STREAM_CONTROL,
STREAM_EMERGENCY,
STREAM_TELEMETRY,
@@ -51,7 +51,7 @@ from tests.lab033_priority_channel_scheduler import (
build_workload as build_lab033_workload,
deterministic_payload,
)
from tests.lab034_stale_video_drop import parity_for_partial
from experiments.lab034_stale_video_drop import parity_for_partial
OUTPUT_DIRECTORY = Path("data/processed/lab037")
@@ -1186,13 +1186,13 @@ def write_report(
"",
"Созданные файлы",
"- protocol/control_repetition.py",
"- tests/lab037_lossy_full_link.py",
"- experiments/lab037_lossy_full_link.py",
*[f"- {path.as_posix()}" for path in (SUMMARY_CSV_PATH, COMMAND_CSV_PATH, VIDEO_CSV_PATH, EMERGENCY_CSV_PATH, REPORT_PATH, *PLOT_PATHS)],
"",
"Итоговый git status",
"?? data/processed/lab037/",
"?? protocol/control_repetition.py",
"?? tests/lab037_lossy_full_link.py",
"?? experiments/lab037_lossy_full_link.py",
"",
"Двоичные пакеты, JPEG, подробные пакетные журналы и дампы не сохранялись.",
"Режим защиты автоматически не выбирается: все три режима представлены отдельно.",

View File

@@ -43,12 +43,12 @@ from protocol.packet_erasure_fec import (
decode_outer_symbol,
)
from protocol.video_packet import decode_packet as decode_inner_packet
from tests.lab033_priority_channel_scheduler import (
from experiments.lab033_priority_channel_scheduler import (
STREAM_CONTROL,
STREAM_EMERGENCY,
deterministic_payload,
)
from tests.lab037_lossy_full_link import (
from experiments.lab037_lossy_full_link import (
BAD_TIME_FRACTION,
CHANNEL_RATES_KBPS,
DURATION_SECONDS,
@@ -1577,7 +1577,7 @@ def write_report(
created = (
Path("protocol/control_failsafe.py"),
Path("protocol/emergency_ack.py"),
Path("tests/lab038_control_failsafe.py"),
Path("experiments/lab038_control_failsafe.py"),
SUMMARY_CSV_PATH,
COMMAND_CSV_PATH,
EMERGENCY_CSV_PATH,
@@ -1596,8 +1596,8 @@ def write_report(
"?? protocol/control_failsafe.py",
"?? protocol/control_repetition.py",
"?? protocol/emergency_ack.py",
"?? tests/lab037_lossy_full_link.py",
"?? tests/lab038_control_failsafe.py",
"?? experiments/lab037_lossy_full_link.py",
"?? experiments/lab038_control_failsafe.py",
"",
"Lab038 не добавлен в Git и не закоммичен. Двоичные пакеты, JPEG, дампы и подробные пакетные журналы не сохранялись.",
]

View File

@@ -24,9 +24,9 @@ from protocol.two_stage_failsafe import (
forced_stop,
integrate_kinematics,
)
from tests.lab033_priority_channel_scheduler import STREAM_EMERGENCY
from tests.lab037_lossy_full_link import bad_intervals
from tests.lab038_control_failsafe import (
from experiments.lab033_priority_channel_scheduler import STREAM_EMERGENCY
from experiments.lab037_lossy_full_link import bad_intervals
from experiments.lab038_control_failsafe import (
DURATION_SECONDS,
EMERGENCY_SPREAD_US,
MASTER_SEED,
@@ -1776,7 +1776,7 @@ def write_report(
lines.extend(f"- {'PASS' if item.passed else 'FAIL'} {item.name}: {item.detail}" for item in tests)
created = (
Path("protocol/two_stage_failsafe.py"),
Path("tests/lab039_two_stage_braking.py"),
Path("experiments/lab039_two_stage_braking.py"),
SUMMARY_CSV_PATH,
WATCHDOG_CSV_PATH,
DISTANCE_CSV_PATH,

View File

@@ -37,9 +37,9 @@ from protocol.two_stage_failsafe import (
TwoStageFailsafe,
integrate_kinematics,
)
from tests.lab033_priority_channel_scheduler import STREAM_CONTROL, STREAM_EMERGENCY
from tests.lab037_lossy_full_link import bad_intervals
from tests.lab038_control_failsafe import DURATION_SECONDS, Frame, Unit, Workload, build_workload, packet_is_lost
from experiments.lab033_priority_channel_scheduler import STREAM_CONTROL, STREAM_EMERGENCY
from experiments.lab037_lossy_full_link import bad_intervals
from experiments.lab038_control_failsafe import DURATION_SECONDS, Frame, Unit, Workload, build_workload, packet_is_lost
OUTPUT_DIRECTORY = Path("data/processed/lab040")
@@ -1650,7 +1650,7 @@ def write_report(
created = (
Path("protocol/persistent_emergency.py"),
Path("protocol/safe_reset.py"),
Path("tests/lab040_persistent_emergency.py"),
Path("experiments/lab040_persistent_emergency.py"),
SUMMARY_CSV,
EMERGENCY_CSV,
RESET_CSV,

View File

@@ -94,7 +94,7 @@ VIDEO_OFFERED_LOAD_KBPS = 243.39286666666666
TELEMETRY_OFFERED_LOAD_KBPS = 7.68
# Параметры торможения взяты из профиля "nominal" Lab039, а не подобраны заново.
# tests/lab039_two_stage_braking.py: DecelerationProfile("nominal", 1.0, 3.0).
# experiments/lab039_two_stage_braking.py: DecelerationProfile("nominal", 1.0, 3.0).
# Оценка пути ниже одноступенчатая и потому консервативнее двухступенчатой
# модели Lab039: она не учитывает предварительное замедление Stage1.
LAB039_NOMINAL_STAGE2_MPS2 = 3.0
@@ -1417,7 +1417,7 @@ def write_report(
created = (
Path("protocol/control_messages.py"),
Path("protocol/session_state.py"),
Path("tests/lab041_session_restart_safety.py"),
Path("experiments/lab041_session_restart_safety.py"),
SUMMARY_CSV,
SESSION_CSV,
REJECTION_CSV,

View File

@@ -0,0 +1 @@
"""Быстрые автоматические проверки ядра протокола."""

View File

@@ -24,7 +24,7 @@ import sys
import traceback
PROJECT_ROOT = Path(__file__).resolve().parent.parent
CHECKED_DIRECTORIES = ("protocol", "tests", "tools")
CHECKED_DIRECTORIES = ("protocol", "tests", "experiments", "tools")
def check_syntax() -> list[str]:
@@ -81,13 +81,13 @@ def check_functional_tests() -> tuple[list[str], int, int]:
executed = 0
skipped = 0
for path in sorted((PROJECT_ROOT / "tests").glob("lab*.py")):
for path in sorted((PROJECT_ROOT / "experiments").glob("lab*.py")):
source = path.read_text(encoding="utf-8")
if "def run_functional_tests" not in source:
continue
module_name = f"tests.{path.stem}"
module_name = f"experiments.{path.stem}"
try:
module = importlib.import_module(module_name)