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

@@ -527,22 +527,31 @@ def _failed_result(plan: FramePlan, correlation: float = 0.0, phase: int = 0) ->
def _error_vector_magnitude(received: np.ndarray, reference: np.ndarray) -> float:
"""Среднеквадратичное отклонение принятых символов от переданных.
Нормируется на энергию переданных символов, поэтому не зависит от
общего усиления тракта.
Нормируется на мощность подогнанного сигнала, то есть на мощность
масштабированного эталона scale*reference, а не на единичный эталон.
Это принципиально: эталон здесь — единичные BPSK-символы мощностью 1,
и нормировка на него делала метрику тем меньше, чем слабее сигнал,
вплоть до почти нуля при непрошедшей контрольной сумме. Нормировка на
мощность подогнанного сигнала сохраняет независимость от общего
усиления тракта и при этом растёт с ухудшением отношения сигнал/шум.
Возвращает NaN, если величину вычислить нельзя: ноль означал бы
идеальный приём там, где сигнала нет.
"""
# Ноль здесь возвращать нельзя: он означал бы идеальный приём там, где
# величину вычислить не удалось. Такое значение уже один раз скрыло
# отказ приёмника, показав 0,000% при непрошедшей контрольной сумме.
if not len(received) or len(received) != len(reference):
return float("nan")
scale = np.vdot(reference, received) / np.vdot(reference, reference)
difference = received - scale * reference
reference_power = float(np.mean(np.abs(reference) ** 2))
if reference_power <= 0.0:
reference_energy = np.vdot(reference, reference)
if reference_energy == 0:
return float("nan")
return float(np.sqrt(np.mean(np.abs(difference) ** 2) / reference_power))
scale = np.vdot(reference, received) / reference_energy
difference = received - scale * reference
signal_power = float(np.abs(scale) ** 2 * np.mean(np.abs(reference) ** 2))
if signal_power <= 0.0:
return float("nan")
return float(np.sqrt(np.mean(np.abs(difference) ** 2) / signal_power))
# ==================================================================== прогон
@@ -886,6 +895,39 @@ def save_plots(metrics: TransferMetrics, results: tuple[FrameResult, ...], segme
plt.close(figure)
def created_files(metrics: TransferMetrics) -> tuple[Path, ...]:
"""Вернуть файлы, действительно созданные прогоном.
Принятое изображение попадает в список только если оно собрано:
объявлять несуществующий файл созданным нельзя.
"""
paths = [SUMMARY_CSV, FRAMES_CSV, REPORT_PATH, *PLOT_PATHS]
if metrics.image_reassembled:
paths.insert(0, RECEIVED_IMAGE_PATH)
return tuple(paths)
def transfer_meets_criteria(
metrics: TransferMetrics,
tests: tuple[FunctionalTestResult, ...],
) -> bool:
"""Проверить, достигнут ли критерий приёмки Lab042.
Критерий: пройдены все функциональные проверки, изображение собрано
побайтово идентично переданному и восстановлены все фрагменты. По этому
признаку `main` выбирает код завершения, чтобы автоматика не приняла
провал за успех.
"""
return (
bool(tests)
and all(item.passed for item in tests)
and metrics.image_identical
and metrics.fragments_recovered == metrics.fragment_count
)
def write_report(
metrics: TransferMetrics,
results: tuple[FrameResult, ...],
@@ -990,10 +1032,7 @@ def write_report(
"Созданные файлы",
]
)
lines.extend(
f"- {path.as_posix()}"
for path in (RECEIVED_IMAGE_PATH, SUMMARY_CSV, FRAMES_CSV, REPORT_PATH, *PLOT_PATHS)
)
lines.extend(f"- {path.as_posix()}" for path in created_files(metrics))
REPORT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
@@ -1025,7 +1064,7 @@ def validate_outputs(metrics: TransferMetrics, results: tuple[FrameResult, ...],
assert not any(path.suffix.lower() in forbidden for path in actual)
def main() -> None:
def main() -> int:
parser = argparse.ArgumentParser(description="Lab042: передача JPEG через кабельную петлю")
parser.add_argument(
"--loopback",
@@ -1078,13 +1117,20 @@ def main() -> None:
write_report(metrics, results, tests)
validate_outputs(metrics, results, tests)
success = transfer_meets_criteria(metrics, tests)
passed_tests = sum(item.passed for item in tests)
print(
f"Lab042 завершена, режим {metrics.loopback_mode}: "
f"фрагментов {metrics.fragments_recovered}/{metrics.fragment_count}, "
f"изображение {'идентично' if metrics.image_identical else 'НЕ совпадает'}, "
f"{len(tests)}/{len(tests)} проверок, два CSV и четыре PNG"
f"{passed_tests}/{len(tests)} проверок, два CSV и четыре PNG"
)
if not success:
print("Критерий Lab042 НЕ достигнут: изображение не собрано побайтово. Код завершения ненулевой.")
return 0 if success else 1
if __name__ == "__main__":
main()
raise SystemExit(main())