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>
468 lines
8.6 KiB
Python
468 lines
8.6 KiB
Python
"""
|
|
Lab017. Фильтрация Eb/N0 и гистерезис режимов.
|
|
|
|
Эксперимент сравнивает два адаптера:
|
|
|
|
1. Наивный адаптер:
|
|
переключает режим при каждом пересечении порога.
|
|
|
|
2. Устойчивый адаптер:
|
|
использует:
|
|
- экспоненциальное усреднение;
|
|
- гистерезис;
|
|
- подтверждение повышения;
|
|
- аварийное быстрое понижение.
|
|
|
|
Профиль Eb/N0 специально содержит колебания
|
|
около порогов переключения.
|
|
"""
|
|
|
|
from csv import DictWriter
|
|
from pathlib import Path
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
from protocol.link_controller import (
|
|
FRAGMENT_MODE_1024,
|
|
IMAGE_OFF,
|
|
LinkModeController,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Настройки
|
|
# ============================================================
|
|
|
|
OUTPUT_DIRECTORY = Path(
|
|
"data/processed/lab017"
|
|
)
|
|
|
|
OUTPUT_DIRECTORY.mkdir(
|
|
parents=True,
|
|
exist_ok=True,
|
|
)
|
|
|
|
CSV_PATH = (
|
|
OUTPUT_DIRECTORY
|
|
/ "lab017_hysteresis_results.csv"
|
|
)
|
|
|
|
GRAPH_PATH = (
|
|
OUTPUT_DIRECTORY
|
|
/ "lab017_hysteresis_comparison.png"
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Профиль качества канала
|
|
# ============================================================
|
|
|
|
EB_N0_PROFILE_DB = [
|
|
10.2,
|
|
9.7,
|
|
9.4,
|
|
9.6,
|
|
9.3,
|
|
9.7,
|
|
9.2,
|
|
9.6,
|
|
9.0,
|
|
8.7,
|
|
8.4,
|
|
8.6,
|
|
8.3,
|
|
8.7,
|
|
8.2,
|
|
8.6,
|
|
8.0,
|
|
7.8,
|
|
7.5,
|
|
7.7,
|
|
7.4,
|
|
7.8,
|
|
7.2,
|
|
6.8,
|
|
6.5,
|
|
6.3,
|
|
6.6,
|
|
6.2,
|
|
6.7,
|
|
6.1,
|
|
5.5,
|
|
6.0,
|
|
6.4,
|
|
6.7,
|
|
6.9,
|
|
7.1,
|
|
7.3,
|
|
8.0,
|
|
8.8,
|
|
9.0,
|
|
9.4,
|
|
9.8,
|
|
10.0,
|
|
10.2,
|
|
10.1,
|
|
]
|
|
|
|
|
|
# ============================================================
|
|
# Наивное решение без гистерезиса
|
|
# ============================================================
|
|
|
|
def choose_naive_mode(
|
|
eb_n0_db: float,
|
|
) -> int:
|
|
"""
|
|
Немедленно выбрать режим по одному измерению.
|
|
|
|
Гистерезиса и фильтрации нет.
|
|
"""
|
|
|
|
if eb_n0_db < 6.5:
|
|
return 0
|
|
|
|
if eb_n0_db < 8.5:
|
|
return 128
|
|
|
|
if eb_n0_db < 9.5:
|
|
return 512
|
|
|
|
return 1024
|
|
|
|
|
|
# ============================================================
|
|
# Запуск устойчивого контроллера
|
|
# ============================================================
|
|
|
|
controller = LinkModeController(
|
|
initial_mode=FRAGMENT_MODE_1024,
|
|
filter_alpha=0.35,
|
|
upgrade_confirmation_count=3,
|
|
minimum_hold_steps=2,
|
|
)
|
|
|
|
results = []
|
|
|
|
for measured_eb_n0_db in EB_N0_PROFILE_DB:
|
|
|
|
stable_result = controller.update(
|
|
measured_eb_n0_db
|
|
)
|
|
|
|
naive_mode = choose_naive_mode(
|
|
measured_eb_n0_db
|
|
)
|
|
|
|
results.append(
|
|
{
|
|
"step": stable_result.step,
|
|
"measured_eb_n0_db": (
|
|
stable_result.measured_eb_n0_db
|
|
),
|
|
"filtered_eb_n0_db": (
|
|
stable_result.filtered_eb_n0_db
|
|
),
|
|
"naive_mode": naive_mode,
|
|
"stable_mode": (
|
|
stable_result.selected_mode
|
|
),
|
|
"mode_changed": (
|
|
stable_result.mode_changed
|
|
),
|
|
"pending_upgrade_mode": (
|
|
stable_result.pending_upgrade_mode
|
|
),
|
|
"pending_upgrade_count": (
|
|
stable_result.pending_upgrade_count
|
|
),
|
|
"reason": stable_result.reason,
|
|
}
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Число переключений
|
|
# ============================================================
|
|
|
|
def count_mode_switches(
|
|
modes: list[int],
|
|
) -> int:
|
|
"""
|
|
Посчитать число изменений режима.
|
|
"""
|
|
|
|
return sum(
|
|
previous_mode != current_mode
|
|
for previous_mode, current_mode in zip(
|
|
modes,
|
|
modes[1:],
|
|
)
|
|
)
|
|
|
|
|
|
naive_modes = [
|
|
result["naive_mode"]
|
|
for result in results
|
|
]
|
|
|
|
stable_modes = [
|
|
result["stable_mode"]
|
|
for result in results
|
|
]
|
|
|
|
naive_switch_count = count_mode_switches(
|
|
naive_modes
|
|
)
|
|
|
|
stable_switch_count = count_mode_switches(
|
|
stable_modes
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Вывод результатов
|
|
# ============================================================
|
|
|
|
print(
|
|
"=== Lab017. Гистерезис режима канала ==="
|
|
)
|
|
|
|
print("\nРезультаты по шагам:")
|
|
|
|
print(
|
|
f"{'Шаг':>5}"
|
|
f"{'Eb/N0':>10}"
|
|
f"{'Фильтр':>11}"
|
|
f"{'Наивный':>11}"
|
|
f"{'Устойчивый':>13}"
|
|
f"{'Изменён':>10}"
|
|
f"{'Причина':>45}"
|
|
)
|
|
|
|
print("-" * 105)
|
|
|
|
for result in results:
|
|
|
|
print(
|
|
f"{result['step']:>5}"
|
|
f"{result['measured_eb_n0_db']:>7.1f} дБ"
|
|
f"{result['filtered_eb_n0_db']:>8.2f} дБ"
|
|
f"{result['naive_mode']:>11}"
|
|
f"{result['stable_mode']:>13}"
|
|
f"{str(result['mode_changed']):>10}"
|
|
f" {result['reason']}"
|
|
)
|
|
|
|
|
|
print("\nЧисло переключений:")
|
|
|
|
print(
|
|
"Наивный адаптер:",
|
|
naive_switch_count,
|
|
)
|
|
|
|
print(
|
|
"Устойчивый адаптер:",
|
|
stable_switch_count,
|
|
)
|
|
|
|
print(
|
|
"Предотвращено лишних переключений:",
|
|
naive_switch_count - stable_switch_count,
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Сохранение CSV
|
|
# ============================================================
|
|
|
|
with CSV_PATH.open(
|
|
"w",
|
|
newline="",
|
|
encoding="utf-8-sig",
|
|
) as csv_file:
|
|
|
|
writer = DictWriter(
|
|
csv_file,
|
|
fieldnames=list(
|
|
results[0].keys()
|
|
),
|
|
)
|
|
|
|
writer.writeheader()
|
|
writer.writerows(results)
|
|
|
|
|
|
# ============================================================
|
|
# График
|
|
# ============================================================
|
|
|
|
steps = [
|
|
result["step"]
|
|
for result in results
|
|
]
|
|
|
|
measured_values = [
|
|
result["measured_eb_n0_db"]
|
|
for result in results
|
|
]
|
|
|
|
filtered_values = [
|
|
result["filtered_eb_n0_db"]
|
|
for result in results
|
|
]
|
|
|
|
|
|
figure, axes = plt.subplots(
|
|
2,
|
|
1,
|
|
figsize=(12, 9),
|
|
sharex=True,
|
|
)
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
# Верхний график: Eb/N0
|
|
# ------------------------------------------------------------
|
|
|
|
axes[0].plot(
|
|
steps,
|
|
measured_values,
|
|
marker=".",
|
|
label="Измеренное Eb/N0",
|
|
)
|
|
|
|
axes[0].plot(
|
|
steps,
|
|
filtered_values,
|
|
linewidth=2,
|
|
label="Фильтрованное Eb/N0",
|
|
)
|
|
|
|
axes[0].axhline(
|
|
6.5,
|
|
linestyle=":",
|
|
label="Порог наивного адаптера",
|
|
)
|
|
|
|
axes[0].axhline(
|
|
8.5,
|
|
linestyle=":",
|
|
)
|
|
|
|
axes[0].axhline(
|
|
9.5,
|
|
linestyle=":",
|
|
)
|
|
|
|
axes[0].set_ylabel(
|
|
"Eb/N0, дБ"
|
|
)
|
|
|
|
axes[0].set_title(
|
|
"Фильтрация измерения качества канала"
|
|
)
|
|
|
|
axes[0].grid(
|
|
True
|
|
)
|
|
|
|
axes[0].legend()
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
# Нижний график: режимы
|
|
# ------------------------------------------------------------
|
|
|
|
axes[1].step(
|
|
steps,
|
|
naive_modes,
|
|
where="mid",
|
|
marker=".",
|
|
label="Наивный адаптер",
|
|
)
|
|
|
|
axes[1].step(
|
|
steps,
|
|
stable_modes,
|
|
where="mid",
|
|
marker="o",
|
|
label="Гистерезис и фильтрация",
|
|
)
|
|
|
|
axes[1].set_yticks(
|
|
[
|
|
0,
|
|
128,
|
|
512,
|
|
1024,
|
|
],
|
|
[
|
|
"IMAGE OFF",
|
|
"128 B",
|
|
"512 B",
|
|
"1024 B",
|
|
],
|
|
)
|
|
|
|
axes[1].set_xlabel(
|
|
"Шаг времени"
|
|
)
|
|
|
|
axes[1].set_ylabel(
|
|
"Режим"
|
|
)
|
|
|
|
axes[1].set_title(
|
|
"Сравнение переключений режимов"
|
|
)
|
|
|
|
axes[1].grid(
|
|
True
|
|
)
|
|
|
|
axes[1].legend()
|
|
|
|
|
|
figure.tight_layout()
|
|
|
|
figure.savefig(
|
|
GRAPH_PATH,
|
|
dpi=160,
|
|
)
|
|
|
|
plt.close(
|
|
figure
|
|
)
|
|
|
|
|
|
# ============================================================
|
|
# Проверки
|
|
# ============================================================
|
|
|
|
assert results
|
|
|
|
assert IMAGE_OFF in stable_modes
|
|
|
|
assert FRAGMENT_MODE_1024 in stable_modes
|
|
|
|
assert stable_switch_count < naive_switch_count
|
|
|
|
assert CSV_PATH.exists()
|
|
|
|
assert GRAPH_PATH.exists()
|
|
|
|
|
|
print("\nCSV:")
|
|
|
|
print(CSV_PATH)
|
|
|
|
print("\nГрафик:")
|
|
|
|
print(GRAPH_PATH)
|
|
|
|
print(
|
|
"\nПроверка пройдена: "
|
|
"гистерезис уменьшил количество переключений."
|
|
) |