Lab042: validate Pluto image hardware loopback
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user