Lab042: validate Pluto image hardware loopback

This commit is contained in:
LittleSam129
2026-08-17 18:29:14 +03:00
parent fce14f038e
commit fa2e473c6d
11 changed files with 276 additions and 53 deletions

View File

@@ -0,0 +1,125 @@
"""Проверки отчётности Lab042: неуспешный путь не должен маскироваться.
Три дефекта, найденные при разборе кабельного прогона, закрыты правками в
experiments/lab042_pluto_image_loopback.py. Эти тесты фиксируют их, чтобы
провал лаборатории нельзя было принять за успех:
1. EVM не занижается искусственно при слабом сигнале и не превращается в
ноль, когда её нельзя вычислить;
2. отсутствующее принятое изображение не объявляется созданным файлом;
3. невыполнение критерия лаборатории даёт ненулевой код завершения.
"""
from __future__ import annotations
import math
from types import SimpleNamespace
import numpy as np
import pytest
from experiments.lab042_pluto_image_loopback import (
RECEIVED_IMAGE_PATH,
FunctionalTestResult,
_error_vector_magnitude,
created_files,
transfer_meets_criteria,
)
def _reference(length: int = 512) -> np.ndarray:
bits = np.random.default_rng(length).integers(0, 2, size=length).astype(np.float64)
return (2.0 * bits - 1.0).astype(np.complex128)
# ------------------------------------------------------------------ EVM
def test_evm_is_near_zero_for_a_clean_signal() -> None:
reference = _reference()
assert _error_vector_magnitude(reference.copy(), reference) < 1e-9
def test_evm_is_invariant_to_overall_gain() -> None:
"""Одно и то же отношение сигнал/шум при разной амплитуде — одна EVM."""
rng = np.random.default_rng(1)
reference = _reference()
noise = 0.1 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference)))
strong = _error_vector_magnitude(reference + noise, reference)
weak = _error_vector_magnitude(0.001 * (reference + noise), reference)
assert math.isclose(strong, weak, rel_tol=1e-6)
def test_evm_grows_when_the_signal_is_weak_and_noisy() -> None:
"""Регрессия: прежняя нормировка на единичный эталон занижала EVM у
слабого сигнала почти до нуля. Теперь слабый шумный сигнал обязан дать
заметную EVM, а не подделку под идеальный приём.
"""
rng = np.random.default_rng(2)
reference = _reference()
noise = 0.5 * (rng.standard_normal(len(reference)) + 1j * rng.standard_normal(len(reference)))
weak_noisy = 0.001 * (reference + noise)
assert _error_vector_magnitude(weak_noisy, reference) > 0.1
def test_evm_is_not_a_number_when_it_cannot_be_computed() -> None:
reference = _reference()
assert math.isnan(_error_vector_magnitude(reference[:10], reference))
assert math.isnan(_error_vector_magnitude(np.zeros(0, dtype=complex), np.zeros(0, dtype=complex)))
# ------------------------------------------------------------------ состав файлов
def _metrics(**overrides) -> SimpleNamespace:
base = dict(
image_reassembled=True,
image_identical=True,
fragments_recovered=10,
fragment_count=10,
)
base.update(overrides)
return SimpleNamespace(**base)
def test_received_image_listed_only_when_reassembled() -> None:
with_image = created_files(_metrics(image_reassembled=True))
without_image = created_files(_metrics(image_reassembled=False, image_identical=False))
assert RECEIVED_IMAGE_PATH in with_image
assert RECEIVED_IMAGE_PATH not in without_image
# ------------------------------------------------------------------ критерий и код возврата
def _all_passed() -> tuple[FunctionalTestResult, ...]:
return tuple(FunctionalTestResult(f"{index:02d}", True, "ок") for index in range(11))
def test_criteria_met_on_a_clean_transfer() -> None:
assert transfer_meets_criteria(_metrics(), _all_passed()) is True
def test_criteria_failed_when_image_differs() -> None:
assert transfer_meets_criteria(_metrics(image_identical=False), _all_passed()) is False
def test_criteria_failed_when_a_fragment_is_missing() -> None:
metrics = _metrics(image_identical=False, fragments_recovered=9)
assert transfer_meets_criteria(metrics, _all_passed()) is False
def test_criteria_failed_when_a_functional_check_failed() -> None:
tests = _all_passed()[:-1] + (FunctionalTestResult("bad", False, "провал"),)
assert transfer_meets_criteria(_metrics(), tests) is False
def test_criteria_failed_without_any_tests() -> None:
assert transfer_meets_criteria(_metrics(), ()) is False