Skip to content

Reports

HTML report generation with optional embedded Plotly interactive charts.

Snapshot Report

snapshot

Snapshot link budget HTML report generation.

render_snapshot_report

render_snapshot_report(breakdown, config, output_path)

Generate standalone HTML report for a snapshot link budget.

Parameters:

Name Type Description Default
breakdown dict of str to float

Itemised link budget breakdown (keys are parameter names, values in dB or Kelvin).

required
config dict

Raw config dictionary for metadata display.

required
output_path str or Path

File path for the generated HTML report.

required

Returns:

Type Description
Path

Path to the written HTML file.

Source code in src/opensatcom/reports/snapshot.py
def render_snapshot_report(
    breakdown: dict[str, float],
    config: dict[str, Any],
    output_path: str | Path,
) -> Path:
    """Generate standalone HTML report for a snapshot link budget.

    Parameters
    ----------
    breakdown : dict of str to float
        Itemised link budget breakdown (keys are parameter names,
        values in dB or Kelvin).
    config : dict
        Raw config dictionary for metadata display.
    output_path : str or Path
        File path for the generated HTML report.

    Returns
    -------
    Path
        Path to the written HTML file.
    """
    output_path = Path(output_path)

    margin = breakdown.get("margin_db", 0.0)
    margin_color = "#2e7d32" if margin >= 0 else "#c62828"

    rows = ""
    for key, val in breakdown.items():
        if key == "rx_system_temp_k":
            rows += f"<tr><td>{key}</td><td>{val:.1f} K</td></tr>\n"
        else:
            rows += f"<tr><td>{key}</td><td>{val:.2f} dB</td></tr>\n"

    html = f"""<!DOCTYPE html>
<html>
<head>
<title>OpenSatCom Snapshot Link Budget</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
       max-width: 800px; margin: 40px auto; padding: 0 20px; color: #333; }}
h1 {{ color: #1565c0; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #f5f5f5; }}
.margin {{ font-size: 1.4em; font-weight: bold; color: {margin_color}; }}
.footer {{ margin-top: 40px; font-size: 0.85em; color: #888; }}
</style>
</head>
<body>
<h1>Snapshot Link Budget Report</h1>
<p class="margin">Link Margin: {margin:.2f} dB</p>

<h2>Link Breakdown</h2>
<table>
<tr><th>Parameter</th><th>Value</th></tr>
{rows}
</table>

<div class="footer">
Generated by OpenSatCom v0.1.0
</div>
</body>
</html>"""

    output_path.write_text(html)
    return output_path

Mission Report

mission

Mission/time-series HTML report generation.

render_mission_report

render_mission_report(summary, times_s, margin_db, elev_deg, outages_mask, config, output_path, plots_dir=None, doppler_hz=None)

Generate standalone HTML report for a mission simulation.

Parameters:

Name Type Description Default
summary dict of str to float

Scalar summary metrics (availability, margin stats, etc.).

required
times_s ndarray

Time stamps in seconds, shape (N,).

required
margin_db ndarray

Link margin time series in dB, shape (N,).

required
elev_deg ndarray

Elevation angles in degrees, shape (N,).

required
outages_mask ndarray

Boolean outage mask, shape (N,).

required
config dict

Raw config dictionary for metadata display.

required
output_path str or Path

File path for the generated HTML report.

required
plots_dir str, Path, or None

Optional directory to save static plot images alongside the report.

None
doppler_hz ndarray or None

Doppler shift time series in Hz. If provided, included in the report.

None

Returns:

Type Description
Path

Path to the written HTML file.

Source code in src/opensatcom/reports/mission.py
def render_mission_report(
    summary: dict[str, float],
    times_s: np.ndarray,
    margin_db: np.ndarray,
    elev_deg: np.ndarray,
    outages_mask: np.ndarray,
    config: dict[str, Any],
    output_path: str | Path,
    plots_dir: str | Path | None = None,
    doppler_hz: np.ndarray | None = None,
) -> Path:
    """Generate standalone HTML report for a mission simulation.

    Parameters
    ----------
    summary : dict of str to float
        Scalar summary metrics (availability, margin stats, etc.).
    times_s : numpy.ndarray
        Time stamps in seconds, shape ``(N,)``.
    margin_db : numpy.ndarray
        Link margin time series in dB, shape ``(N,)``.
    elev_deg : numpy.ndarray
        Elevation angles in degrees, shape ``(N,)``.
    outages_mask : numpy.ndarray
        Boolean outage mask, shape ``(N,)``.
    config : dict
        Raw config dictionary for metadata display.
    output_path : str or Path
        File path for the generated HTML report.
    plots_dir : str, Path, or None
        Optional directory to save static plot images alongside the report.
    doppler_hz : numpy.ndarray or None
        Doppler shift time series in Hz. If provided, included in the report.

    Returns
    -------
    Path
        Path to the written HTML file.
    """
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    output_path = Path(output_path)

    # Generate plots
    plots_html = ""

    # Margin vs time
    fig, ax = plt.subplots(figsize=(10, 4))
    valid = ~outages_mask
    ax.plot(times_s[valid], margin_db[valid], "b-", linewidth=1.5, label="Margin")
    if np.any(outages_mask):
        ax.axhspan(-100, 0, alpha=0.1, color="red", label="Outage region")
    ax.axhline(0, color="red", linestyle="--", alpha=0.5)
    ax.set_xlabel("Time (s)")
    ax.set_ylabel("Margin (dB)")
    ax.set_title("Link Margin vs Time")
    ax.legend()
    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=100)
    if plots_dir:
        plots_dir = Path(plots_dir)
        plots_dir.mkdir(parents=True, exist_ok=True)
        fig.savefig(plots_dir / "margin_vs_time.png", dpi=100)
    plt.close(fig)
    margin_b64 = base64.b64encode(buf.getvalue()).decode()
    plots_html += f'<img src="data:image/png;base64,{margin_b64}" alt="Margin vs Time">\n'

    # Elevation vs time
    fig, ax = plt.subplots(figsize=(10, 4))
    ax.plot(times_s, elev_deg, "g-", linewidth=1.5)
    ax.set_xlabel("Time (s)")
    ax.set_ylabel("Elevation (deg)")
    ax.set_title("Elevation vs Time")
    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=100)
    if plots_dir:
        fig.savefig(plots_dir / "elevation_vs_time.png", dpi=100)
    plt.close(fig)
    elev_b64 = base64.b64encode(buf.getvalue()).decode()
    plots_html += f'<img src="data:image/png;base64,{elev_b64}" alt="Elevation vs Time">\n'

    # Doppler vs time (if available)
    if doppler_hz is not None and len(doppler_hz) > 0:
        fig, ax = plt.subplots(figsize=(10, 4))
        doppler_khz = doppler_hz / 1000.0
        ax.plot(times_s[valid], doppler_khz[valid], "m-", linewidth=1.5)
        ax.set_xlabel("Time (s)")
        ax.set_ylabel("Doppler Shift (kHz)")
        ax.set_title("Doppler Shift vs Time")
        ax.axhline(0, color="gray", linestyle="--", alpha=0.5)
        ax.grid(True, alpha=0.3)
        fig.tight_layout()

        buf = io.BytesIO()
        fig.savefig(buf, format="png", dpi=100)
        if plots_dir:
            fig.savefig(plots_dir / "doppler_vs_time.png", dpi=100)
        plt.close(fig)
        doppler_b64 = base64.b64encode(buf.getvalue()).decode()
        plots_html += f'<img src="data:image/png;base64,{doppler_b64}" alt="Doppler vs Time">\n'

    # Optional Plotly interactive plots
    plotly_html = ""
    try:
        from opensatcom.viz.timeline import plot_elevation_profile, plot_link_margin_timeline

        fig_margin = plot_link_margin_timeline(times_s, margin_db, outages_mask)
        plotly_html += '<h2>Interactive Plots</h2>\n'
        plotly_html += fig_margin.to_html(include_plotlyjs="cdn", full_html=False)
        fig_elev = plot_elevation_profile(times_s, elev_deg)
        plotly_html += fig_elev.to_html(include_plotlyjs=False, full_html=False)
    except ImportError:
        pass  # plotly not installed, skip interactive plots

    # Summary table
    summary_rows = ""
    for key, val in summary.items():
        if "availability" in key:
            summary_rows += f"<tr><td>{key}</td><td>{val:.4f}</td></tr>\n"
        else:
            summary_rows += f"<tr><td>{key}</td><td>{val:.2f}</td></tr>\n"

    # Add Doppler summary if available
    if doppler_hz is not None:
        valid_doppler = doppler_hz[valid]
        if len(valid_doppler) > 0:
            d_min = float(np.min(valid_doppler))
            d_max = float(np.max(valid_doppler))
            summary_rows += (
                f"<tr><td>doppler_hz_min</td><td>{d_min:.1f}</td></tr>\n"
            )
            summary_rows += (
                f"<tr><td>doppler_hz_max</td><td>{d_max:.1f}</td></tr>\n"
            )

    avail = summary.get("availability", 0.0)
    avail_color = "#2e7d32" if avail >= 0.99 else "#e65100" if avail >= 0.95 else "#c62828"

    html = f"""<!DOCTYPE html>
<html>
<head>
<title>OpenSatCom Mission Report</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
       max-width: 900px; margin: 40px auto; padding: 0 20px; color: #333; }}
h1 {{ color: #1565c0; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #f5f5f5; }}
img {{ max-width: 100%; margin: 10px 0; }}
.avail {{ font-size: 1.4em; font-weight: bold; color: {avail_color}; }}
.footer {{ margin-top: 40px; font-size: 0.85em; color: #888; }}
</style>
</head>
<body>
<h1>Mission Simulation Report</h1>
<p class="avail">Availability: {avail:.2%}</p>

<h2>Summary Metrics</h2>
<table>
<tr><th>Metric</th><th>Value</th></tr>
{summary_rows}
</table>

<h2>Time-Series Plots</h2>
{plots_html}

{plotly_html}

<div class="footer">
Generated by OpenSatCom v0.5.0
</div>
</body>
</html>"""

    output_path.write_text(html)
    return output_path

Beam Map Report

beammap

Beam map HTML report generation.

render_beammap_report

render_beammap_report(beam_map, config, output_path, plots_dir=None)

Generate standalone HTML report for a beam map / capacity analysis.

Includes per-beam summary table, SINR heatmap, and C/(N+I) heatmap. Optionally embeds interactive Plotly maps when available.

Parameters:

Name Type Description Default
beam_map BeamMap

Evaluated beam map with SINR, margin, and per-point results.

required
config dict

Raw config dictionary for metadata display.

required
output_path str or Path

File path for the generated HTML report.

required
plots_dir str, Path, or None

Optional directory to save static plot images alongside the report.

None

Returns:

Type Description
Path

Path to the written HTML file.

Source code in src/opensatcom/reports/beammap.py
def render_beammap_report(
    beam_map: BeamMap,
    config: dict[str, Any],
    output_path: str | Path,
    plots_dir: str | Path | None = None,
) -> Path:
    """Generate standalone HTML report for a beam map / capacity analysis.

    Includes per-beam summary table, SINR heatmap, and C/(N+I) heatmap.
    Optionally embeds interactive Plotly maps when available.

    Parameters
    ----------
    beam_map : BeamMap
        Evaluated beam map with SINR, margin, and per-point results.
    config : dict
        Raw config dictionary for metadata display.
    output_path : str or Path
        File path for the generated HTML report.
    plots_dir : str, Path, or None
        Optional directory to save static plot images alongside the report.

    Returns
    -------
    Path
        Path to the written HTML file.
    """
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    output_path = Path(output_path)

    df = beam_map.to_dataframe()
    summary = beam_map.per_beam_summary()

    # Per-beam summary table
    beam_rows = ""
    for bid, stats in sorted(summary.items()):
        beam_rows += (
            f"<tr><td>{bid}</td>"
            f"<td>{stats['points_served']:.0f}</td>"
            f"<td>{stats['sinr_db_mean']:.2f}</td>"
            f"<td>{stats['margin_db_mean']:.2f}</td></tr>\n"
        )

    # Plots
    plots_html = ""

    # SINR scatter/heatmap
    fig, ax = plt.subplots(figsize=(8, 6))
    sinr_vals = df["sinr_db"].replace([np.inf, -np.inf], np.nan)
    sc = ax.scatter(
        df["az_deg"], df["el_deg"],
        c=sinr_vals, cmap="RdYlGn", s=80, edgecolors="k", linewidth=0.5,
    )
    fig.colorbar(sc, ax=ax, label="SINR (dB)")
    ax.set_xlabel("Azimuth (deg)")
    ax.set_ylabel("Elevation (deg)")
    ax.set_title("SINR Map")
    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=100)
    if plots_dir:
        plots_dir = Path(plots_dir)
        plots_dir.mkdir(parents=True, exist_ok=True)
        fig.savefig(plots_dir / "sinr_map.png", dpi=100)
    plt.close(fig)
    sinr_b64 = base64.b64encode(buf.getvalue()).decode()
    plots_html += f'<img src="data:image/png;base64,{sinr_b64}" alt="SINR Map">\n'

    # C/(N+I) scatter/heatmap
    fig, ax = plt.subplots(figsize=(8, 6))
    sc = ax.scatter(
        df["az_deg"], df["el_deg"],
        c=df["cnir_db"], cmap="RdYlGn", s=80, edgecolors="k", linewidth=0.5,
    )
    fig.colorbar(sc, ax=ax, label="C/(N+I) (dB)")
    ax.set_xlabel("Azimuth (deg)")
    ax.set_ylabel("Elevation (deg)")
    ax.set_title("C/(N+I) Map")
    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=100)
    if plots_dir:
        fig.savefig(plots_dir / "cnir_map.png", dpi=100)
    plt.close(fig)
    cnir_b64 = base64.b64encode(buf.getvalue()).decode()
    plots_html += f'<img src="data:image/png;base64,{cnir_b64}" alt="C/(N+I) Map">\n'

    # Optional Plotly interactive beam map
    plotly_html = ""
    try:
        from opensatcom.viz.heatmaps import plot_beam_map_interactive

        fig_bm = plot_beam_map_interactive(df, metric="sinr_db")
        plotly_html += '<h2>Interactive Maps</h2>\n'
        plotly_html += fig_bm.to_html(include_plotlyjs="cdn", full_html=False)
    except ImportError:
        pass  # plotly not installed

    # Overall stats
    mean_sinr = beam_map.sinr_db_mean
    mean_margin = beam_map.margin_db_mean
    margin_color = "#2e7d32" if mean_margin >= 0 else "#c62828"

    html = f"""<!DOCTYPE html>
<html>
<head>
<title>OpenSatCom Beam Map Report</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
       max-width: 900px; margin: 40px auto; padding: 0 20px; color: #333; }}
h1 {{ color: #1565c0; }}
table {{ border-collapse: collapse; width: 100%; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #f5f5f5; }}
img {{ max-width: 100%; margin: 10px 0; }}
.margin {{ font-size: 1.4em; font-weight: bold; color: {margin_color}; }}
.footer {{ margin-top: 40px; font-size: 0.85em; color: #888; }}
</style>
</head>
<body>
<h1>Beam Map Report</h1>
<p class="margin">Mean Margin: {mean_margin:.2f} dB | Mean SINR: {mean_sinr:.2f} dB</p>

<h2>Per-Beam Summary</h2>
<table>
<tr><th>Beam ID</th><th>Points Served</th><th>Mean SINR (dB)</th><th>Mean Margin (dB)</th></tr>
{beam_rows}
</table>

<h2>Coverage Maps</h2>
{plots_html}

{plotly_html}

<div class="footer">
Generated by OpenSatCom v0.4.0
</div>
</body>
</html>"""

    output_path.write_text(html)
    return output_path