initial commit
This commit is contained in:
468
Projects/SDR_Rover/tests/lab017_link_hysteresis.py
Normal file
468
Projects/SDR_Rover/tests/lab017_link_hysteresis.py
Normal file
@@ -0,0 +1,468 @@
|
||||
"""
|
||||
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Проверка пройдена: "
|
||||
"гистерезис уменьшил количество переключений."
|
||||
)
|
||||
Reference in New Issue
Block a user