Files
MeshTerm/meshterm/viz/tx_plot.py
T
Jean-Pierre MartineauandClaude Opus 4.8 2ec18138a7 Rename project from MeshTools to MeshTerm
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:40:49 -04:00

99 lines
3.2 KiB
Python

"""Plotly renderer for remote-admin TX-power optimization sweeps.
Produces a self-contained, interactive HTML chart: median SNR measured *at the target*
and trace success rate versus the admin node's TX power, with the chosen optimum marked.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import plotly.graph_objects as go
from ..core.models import TxOptResult
_TEMPLATE = "plotly_dark"
_BRAND = "#5eead4"
_MUTED = "#94a3b8"
def render_tx_optimization(result: TxOptResult, output_dir: Path) -> Path:
"""Render a TX-power optimization sweep to an interactive HTML file.
Args:
result: The optimization result to visualize.
output_dir: Directory the HTML file is written to (created if needed).
Returns:
Path to the written HTML file.
"""
levels = result.sorted_by_tx()
tx = [lv.tx_power for lv in levels]
snr = [lv.target_snr for lv in levels]
success = [lv.success_rate * 100 for lv in levels]
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=tx, y=snr, name="target SNR (dB)", mode="lines+markers",
line=dict(color=_BRAND, width=3), marker=dict(size=8),
connectgaps=False,
hovertemplate="TX %{x}<br>SNR %{y:+.1f} dB<extra></extra>",
)
)
fig.add_trace(
go.Bar(
x=tx, y=success, name="success rate (%)", yaxis="y2",
marker=dict(color=_MUTED), opacity=0.25,
hovertemplate="TX %{x}<br>%{y:.0f}% success<extra></extra>",
)
)
if result.best_snr is not None:
fig.add_vline(x=result.best_tx, line=dict(color="#4ade80", width=2, dash="dash"))
fig.add_trace(
go.Scatter(
x=[result.best_tx], y=[result.best_snr],
name=f"optimum (TX {result.best_tx})", mode="markers",
marker=dict(color="#4ade80", size=16, symbol="star"),
hovertemplate=f"optimum<br>TX {result.best_tx}"
"<br>%{y:+.1f} dB<extra></extra>",
)
)
fig.update_layout(
template=_TEMPLATE,
title=dict(
text=f"TX-power optimization — {result.admin_node} → {result.target}"
f" · optimum TX {result.best_tx}",
font=dict(color=_BRAND),
),
xaxis=dict(title="admin-node TX power", dtick=1),
yaxis=dict(title="target SNR (dB)"),
yaxis2=dict(
title="success rate (%)", overlaying="y", side="right",
range=[0, 105], showgrid=False,
),
legend=dict(orientation="h", yanchor="bottom", y=1.02),
bargap=0.4,
)
output_dir.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
path = output_dir / f"tx-optimize_{_slug(result.target)}_{stamp}.html"
fig.write_html(path, include_plotlyjs=True) # fully self-contained / offline
return path
def _slug(value: str) -> str:
"""Make a filesystem-safe slug from a node name.
Args:
value: Arbitrary node name or key prefix.
Returns:
A lowercase slug containing only alphanumerics, dashes, and underscores.
"""
return "".join(c if c.isalnum() or c in "-_" else "-" for c in value).strip("-").lower()