Skip to content

Visualization API

Plotting functions for trade study visualization.

Overview

from phased_array_systems.viz import pareto_plot, scatter_matrix, trade_space_plot

# Also available from submodule
from phased_array_systems.viz.plots import save_figure

Functions

pareto_plot

pareto_plot(results: DataFrame, x: str, y: str, pareto_front: DataFrame | None = None, feasible_mask: Series | None = None, color_by: str | None = None, size_by: str | None = None, ax: Axes | None = None, title: str | None = None, x_label: str | None = None, y_label: str | None = None, show_pareto_line: bool = True, figsize: tuple[float, float] = (8, 6)) -> Figure

Create a Pareto plot showing trade-offs between two objectives.

PARAMETER DESCRIPTION
results

DataFrame with all evaluation results

TYPE: DataFrame

x

Column name for x-axis

TYPE: str

y

Column name for y-axis

TYPE: str

pareto_front

Optional DataFrame with Pareto-optimal points to highlight

TYPE: DataFrame | None DEFAULT: None

feasible_mask

Optional boolean Series marking feasible designs

TYPE: Series | None DEFAULT: None

color_by

Optional column name to color points by

TYPE: str | None DEFAULT: None

size_by

Optional column name to size points by

TYPE: str | None DEFAULT: None

ax

Optional existing Axes to plot on

TYPE: Axes | None DEFAULT: None

title

Plot title

TYPE: str | None DEFAULT: None

x_label

X-axis label (defaults to column name)

TYPE: str | None DEFAULT: None

y_label

Y-axis label (defaults to column name)

TYPE: str | None DEFAULT: None

show_pareto_line

If True, draw line connecting Pareto points

TYPE: bool DEFAULT: True

figsize

Figure size (width, height)

TYPE: tuple[float, float] DEFAULT: (8, 6)

RETURNS DESCRIPTION
Figure

matplotlib Figure object

Source code in src/phased_array_systems/viz/plots.py
def pareto_plot(
    results: pd.DataFrame,
    x: str,
    y: str,
    pareto_front: pd.DataFrame | None = None,
    feasible_mask: pd.Series | None = None,
    color_by: str | None = None,
    size_by: str | None = None,
    ax: plt.Axes | None = None,
    title: str | None = None,
    x_label: str | None = None,
    y_label: str | None = None,
    show_pareto_line: bool = True,
    figsize: tuple[float, float] = (8, 6),
) -> plt.Figure:
    """Create a Pareto plot showing trade-offs between two objectives.

    Args:
        results: DataFrame with all evaluation results
        x: Column name for x-axis
        y: Column name for y-axis
        pareto_front: Optional DataFrame with Pareto-optimal points to highlight
        feasible_mask: Optional boolean Series marking feasible designs
        color_by: Optional column name to color points by
        size_by: Optional column name to size points by
        ax: Optional existing Axes to plot on
        title: Plot title
        x_label: X-axis label (defaults to column name)
        y_label: Y-axis label (defaults to column name)
        show_pareto_line: If True, draw line connecting Pareto points
        figsize: Figure size (width, height)

    Returns:
        matplotlib Figure object
    """
    if ax is None:
        fig, ax = plt.subplots(figsize=figsize)
    else:
        fig = cast(plt.Figure, ax.figure)

    # Determine point sizes
    sizes: Any
    if size_by is not None and size_by in results.columns:
        size_vals = results[size_by]
        sizes = (size_vals - size_vals.min()) / (
            size_vals.max() - size_vals.min() + 1e-10
        ) * 100 + 20
    else:
        sizes = 50

    # Determine colors
    colors: Any
    if color_by is not None and color_by in results.columns:
        colors = results[color_by]
        cmap = plt.get_cmap("viridis")
    else:
        colors = "steelblue"
        cmap = None

    # Plot infeasible points (if mask provided)
    if feasible_mask is not None:
        infeasible = results[~feasible_mask]
        if len(infeasible) > 0:
            ax.scatter(
                infeasible[x],
                infeasible[y],
                c="lightgray",
                s=30,
                alpha=0.5,
                marker="x",
                label="Infeasible",
            )

    # Plot all feasible points
    plot_data = results[feasible_mask] if feasible_mask is not None else results

    scatter = ax.scatter(
        plot_data[x],
        plot_data[y],
        c=colors if color_by is None else plot_data[color_by],
        s=sizes if isinstance(sizes, int) else plot_data[size_by] if size_by else 50,
        alpha=0.7,
        cmap=cmap,
        label="Feasible" if feasible_mask is not None else None,
    )

    # Add colorbar if coloring by a metric
    if color_by is not None and cmap is not None:
        cbar = plt.colorbar(scatter, ax=ax)
        cbar.set_label(color_by)

    # Highlight Pareto front
    if pareto_front is not None and len(pareto_front) > 0:
        ax.scatter(
            pareto_front[x],
            pareto_front[y],
            facecolors="none",
            edgecolors="red",
            s=100,
            linewidths=2,
            label="Pareto Optimal",
            zorder=5,
        )

        # Draw Pareto line
        if show_pareto_line and len(pareto_front) > 1:
            sorted_pareto = pareto_front.sort_values(x)
            ax.plot(
                sorted_pareto[x],
                sorted_pareto[y],
                "r--",
                alpha=0.5,
                linewidth=1.5,
                zorder=4,
            )

    # Labels and title
    ax.set_xlabel(x_label or x)
    ax.set_ylabel(y_label or y)
    ax.set_title(title or f"Pareto Trade-off: {x} vs {y}")

    # Legend
    handles, labels = ax.get_legend_handles_labels()
    if handles:
        ax.legend(loc="best")

    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    return fig

scatter_matrix

scatter_matrix(results: DataFrame, columns: list[str], color_by: str | None = None, diagonal: Literal['hist', 'kde'] = 'hist', figsize: tuple[float, float] | None = None, title: str | None = None) -> Figure

Create a scatter matrix showing pairwise relationships.

PARAMETER DESCRIPTION
results

DataFrame with evaluation results

TYPE: DataFrame

columns

List of column names to include

TYPE: list[str]

color_by

Optional column name to color points by

TYPE: str | None DEFAULT: None

diagonal

What to show on diagonal ("hist" or "kde")

TYPE: Literal['hist', 'kde'] DEFAULT: 'hist'

figsize

Figure size (auto-calculated if None)

TYPE: tuple[float, float] | None DEFAULT: None

title

Overall figure title

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Figure

matplotlib Figure object

Source code in src/phased_array_systems/viz/plots.py
def scatter_matrix(
    results: pd.DataFrame,
    columns: list[str],
    color_by: str | None = None,
    diagonal: Literal["hist", "kde"] = "hist",
    figsize: tuple[float, float] | None = None,
    title: str | None = None,
) -> plt.Figure:
    """Create a scatter matrix showing pairwise relationships.

    Args:
        results: DataFrame with evaluation results
        columns: List of column names to include
        color_by: Optional column name to color points by
        diagonal: What to show on diagonal ("hist" or "kde")
        figsize: Figure size (auto-calculated if None)
        title: Overall figure title

    Returns:
        matplotlib Figure object
    """
    n = len(columns)
    if figsize is None:
        figsize = (n * 2.5, n * 2.5)

    fig, axes = plt.subplots(n, n, figsize=figsize)

    # Determine colors
    point_colors: Any
    if color_by is not None and color_by in results.columns:
        colors = np.asarray(results[color_by].values, dtype=float)
        cmap = plt.get_cmap("viridis")
        norm = plt.Normalize(colors.min(), colors.max())
        point_colors = cmap(norm(colors))
    else:
        point_colors = "steelblue"

    for i, col_i in enumerate(columns):
        for j, col_j in enumerate(columns):
            ax = axes[i, j]

            if i == j:
                # Diagonal: histogram or KDE
                if diagonal == "hist":
                    ax.hist(results[col_i], bins=20, alpha=0.7, color="steelblue")
                else:  # kde
                    from scipy import stats

                    data = results[col_i].dropna()
                    if len(data) > 1:
                        kde = stats.gaussian_kde(data)
                        x_range = np.linspace(data.min(), data.max(), 100)
                        ax.fill_between(x_range, kde(x_range), alpha=0.5)
                        ax.plot(x_range, kde(x_range), color="steelblue")
            else:
                # Off-diagonal: scatter plot
                ax.scatter(
                    results[col_j],
                    results[col_i],
                    c=point_colors,
                    s=10,
                    alpha=0.5,
                )

            # Labels on edges only
            if i == n - 1:
                ax.set_xlabel(col_j, fontsize=8)
            else:
                ax.set_xticklabels([])

            if j == 0:
                ax.set_ylabel(col_i, fontsize=8)
            else:
                ax.set_yticklabels([])

            ax.tick_params(labelsize=6)

    if title:
        fig.suptitle(title, fontsize=12)

    fig.tight_layout()
    if title:
        fig.subplots_adjust(top=0.95)

    return fig

trade_space_plot

trade_space_plot(results: DataFrame, x: str, y: str, z: str, feasible_mask: Series | None = None, pareto_front: DataFrame | None = None, ax: Axes | None = None, figsize: tuple[float, float] = (10, 8), title: str | None = None) -> Figure

Create a 3D trade space visualization.

PARAMETER DESCRIPTION
results

DataFrame with evaluation results

TYPE: DataFrame

x

Column name for x-axis

TYPE: str

y

Column name for y-axis

TYPE: str

z

Column name for z-axis (color)

TYPE: str

feasible_mask

Optional boolean mask for feasible designs

TYPE: Series | None DEFAULT: None

pareto_front

Optional DataFrame with Pareto-optimal points

TYPE: DataFrame | None DEFAULT: None

ax

Optional existing 3D Axes

TYPE: Axes | None DEFAULT: None

figsize

Figure size

TYPE: tuple[float, float] DEFAULT: (10, 8)

title

Plot title

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Figure

matplotlib Figure object

Source code in src/phased_array_systems/viz/plots.py
def trade_space_plot(
    results: pd.DataFrame,
    x: str,
    y: str,
    z: str,
    feasible_mask: pd.Series | None = None,
    pareto_front: pd.DataFrame | None = None,
    ax: plt.Axes | None = None,
    figsize: tuple[float, float] = (10, 8),
    title: str | None = None,
) -> plt.Figure:
    """Create a 3D trade space visualization.

    Args:
        results: DataFrame with evaluation results
        x: Column name for x-axis
        y: Column name for y-axis
        z: Column name for z-axis (color)
        feasible_mask: Optional boolean mask for feasible designs
        pareto_front: Optional DataFrame with Pareto-optimal points
        ax: Optional existing 3D Axes
        figsize: Figure size
        title: Plot title

    Returns:
        matplotlib Figure object
    """
    if ax is None:
        fig = plt.figure(figsize=figsize)
        ax = fig.add_subplot(111, projection="3d")
    else:
        fig = cast(plt.Figure, ax.figure)

    # 3D Axes methods (z-argument scatter, set_zlabel) are not in the base
    # Axes stubs
    ax3d = cast(Any, ax)

    # Plot points
    if feasible_mask is not None:
        # Infeasible
        inf_data = results[~feasible_mask]
        ax3d.scatter(
            inf_data[x],
            inf_data[y],
            inf_data[z],
            c="lightgray",
            alpha=0.3,
            s=20,
            marker="x",
        )
        plot_data = results[feasible_mask]
    else:
        plot_data = results

    scatter = ax3d.scatter(
        plot_data[x],
        plot_data[y],
        plot_data[z],
        c=plot_data[z],
        cmap="viridis",
        alpha=0.7,
        s=40,
    )

    # Highlight Pareto front
    if pareto_front is not None and len(pareto_front) > 0:
        ax3d.scatter(
            pareto_front[x],
            pareto_front[y],
            pareto_front[z],
            c="red",
            s=100,
            marker="*",
            label="Pareto Optimal",
            zorder=5,
        )
        ax3d.legend()

    ax3d.set_xlabel(x)
    ax3d.set_ylabel(y)
    ax3d.set_zlabel(z)

    fig.colorbar(scatter, ax=ax, label=z, shrink=0.5)

    if title:
        ax.set_title(title)

    return fig

save_figure

save_figure(fig: Figure, path: str, dpi: int = 150, transparent: bool = False) -> None

Save a figure to file.

PARAMETER DESCRIPTION
fig

matplotlib Figure to save

TYPE: Figure

path

Output path (extension determines format)

TYPE: str

dpi

Resolution for raster formats

TYPE: int DEFAULT: 150

transparent

Whether to use transparent background

TYPE: bool DEFAULT: False

Source code in src/phased_array_systems/viz/plots.py
def save_figure(
    fig: plt.Figure,
    path: str,
    dpi: int = 150,
    transparent: bool = False,
) -> None:
    """Save a figure to file.

    Args:
        fig: matplotlib Figure to save
        path: Output path (extension determines format)
        dpi: Resolution for raster formats
        transparent: Whether to use transparent background
    """
    fig.savefig(path, dpi=dpi, transparent=transparent, bbox_inches="tight")

Usage Examples

Pareto Plot

from phased_array_systems.viz import pareto_plot
from phased_array_systems.trades import extract_pareto

# Extract Pareto frontier
pareto = extract_pareto(results, [("cost_usd", "minimize"), ("eirp_dbw", "maximize")])

# Create feasibility mask
feasible_mask = results["verification.passes"] == 1.0

# Generate plot
fig = pareto_plot(
    results,
    x="cost_usd",
    y="eirp_dbw",
    pareto_front=pareto,
    feasible_mask=feasible_mask,
    color_by="link_margin_db",
    title="Cost vs EIRP Trade Space",
    x_label="Total Cost (USD)",
    y_label="EIRP (dBW)",
)
fig.savefig("pareto.png", dpi=150, bbox_inches="tight")

Scatter Matrix

from phased_array_systems.viz import scatter_matrix

fig = scatter_matrix(
    feasible,
    columns=["cost_usd", "eirp_dbw", "link_margin_db", "prime_power_w"],
    color_by="n_elements",
    diagonal="hist",
    title="Trade Space Correlations",
)
fig.savefig("scatter.png", dpi=150)

3D Trade Space

from phased_array_systems.viz import trade_space_plot

fig = trade_space_plot(
    results,
    x="cost_usd",
    y="eirp_dbw",
    z="prime_power_w",
    feasible_mask=feasible_mask,
    pareto_front=pareto_3d,
    title="3D Trade Space",
)
fig.savefig("trade_space_3d.png", dpi=150)

Multi-Panel Figure

import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

pareto_plot(results, x="cost_usd", y="eirp_dbw", ax=axes[0])
pareto_plot(results, x="cost_usd", y="link_margin_db", ax=axes[1])

fig.tight_layout()
fig.savefig("combined.png", dpi=150)

Saving Figures

from phased_array_systems.viz import save_figure

fig = pareto_plot(results, x="cost_usd", y="eirp_dbw")

# Various formats
save_figure(fig, "plot.png", dpi=150)
save_figure(fig, "plot.pdf")
save_figure(fig, "plot.svg")
save_figure(fig, "plot_transparent.png", transparent=True)

Plot Customization

Adding Annotations

fig = pareto_plot(results, x="cost_usd", y="eirp_dbw")
ax = fig.axes[0]

# Add reference lines
ax.axhline(y=40, color='r', linestyle='--', label='Min EIRP')
ax.axvline(x=50000, color='g', linestyle='--', label='Budget')

# Add annotation
ax.annotate(
    "Target Region",
    xy=(40000, 45),
    xytext=(55000, 50),
    arrowprops=dict(arrowstyle="->"),
)

ax.legend()

Custom Color Maps

fig = pareto_plot(
    results,
    x="cost_usd",
    y="eirp_dbw",
    color_by="link_margin_db",
)

# Access scatter for custom colorbar
ax = fig.axes[0]
# Colorbar is automatically added when color_by is specified

Non-Interactive Usage

For scripts without display:

import matplotlib
matplotlib.use("Agg")  # Must be before pyplot import
import matplotlib.pyplot as plt

from phased_array_systems.viz import pareto_plot

fig = pareto_plot(results, x="cost_usd", y="eirp_dbw")
fig.savefig("output.png", dpi=150)
plt.close(fig)  # Release memory

See Also

Interactive Plots (plotly)

pareto_plot_interactive

pareto_plot_interactive(results: DataFrame, x: str, y: str, pareto_front: DataFrame | None = None, feasible_mask: Series | None = None, color_by: str | None = None, title: str | None = None) -> Any

Interactive 2-D trade plot with optional Pareto front overlay.

PARAMETER DESCRIPTION
results

DataFrame with evaluation results

TYPE: DataFrame

x

Column for the x axis

TYPE: str

y

Column for the y axis

TYPE: str

pareto_front

Optional DataFrame of Pareto-optimal rows to highlight

TYPE: DataFrame | None DEFAULT: None

feasible_mask

Optional boolean Series; infeasible points greyed out

TYPE: Series | None DEFAULT: None

color_by

Optional column to color points by (continuous colorbar)

TYPE: str | None DEFAULT: None

title

Plot title

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Any

plotly.graph_objects.Figure

Source code in src/phased_array_systems/viz/interactive.py
def pareto_plot_interactive(
    results: pd.DataFrame,
    x: str,
    y: str,
    pareto_front: pd.DataFrame | None = None,
    feasible_mask: pd.Series | None = None,
    color_by: str | None = None,
    title: str | None = None,
) -> Any:
    """Interactive 2-D trade plot with optional Pareto front overlay.

    Args:
        results: DataFrame with evaluation results
        x: Column for the x axis
        y: Column for the y axis
        pareto_front: Optional DataFrame of Pareto-optimal rows to highlight
        feasible_mask: Optional boolean Series; infeasible points greyed out
        color_by: Optional column to color points by (continuous colorbar)
        title: Plot title

    Returns:
        plotly.graph_objects.Figure
    """
    go = _require_plotly()

    hover_cols = [c for c in (x, y, color_by) if c]
    fig = go.Figure()

    if feasible_mask is not None:
        infeasible = results[~feasible_mask]
        if len(infeasible):
            fig.add_trace(
                go.Scatter(
                    x=infeasible[x],
                    y=infeasible[y],
                    mode="markers",
                    name="Infeasible",
                    marker={"color": "lightgray", "symbol": "x", "size": 7},
                    text=_hover_text(infeasible, hover_cols),
                    hoverinfo="text",
                )
            )
        plot_data = results[feasible_mask]
    else:
        plot_data = results

    marker: dict[str, Any] = {"size": 9, "opacity": 0.8}
    if color_by is not None and color_by in plot_data.columns:
        marker.update(
            color=plot_data[color_by],
            colorscale="Viridis",
            colorbar={"title": color_by},
            showscale=True,
        )
    else:
        marker["color"] = "steelblue"

    fig.add_trace(
        go.Scatter(
            x=plot_data[x],
            y=plot_data[y],
            mode="markers",
            name="Feasible" if feasible_mask is not None else "Designs",
            marker=marker,
            text=_hover_text(plot_data, hover_cols),
            hoverinfo="text",
        )
    )

    if pareto_front is not None and len(pareto_front):
        front = pareto_front.sort_values(x)
        fig.add_trace(
            go.Scatter(
                x=front[x],
                y=front[y],
                mode="markers+lines",
                name="Pareto optimal",
                marker={"color": "crimson", "size": 12, "symbol": "star"},
                line={"color": "crimson", "dash": "dash", "width": 1},
                text=_hover_text(front, hover_cols),
                hoverinfo="text",
            )
        )

    fig.update_layout(
        title=title or f"Trade-off: {x} vs {y}",
        xaxis_title=x,
        yaxis_title=y,
        template="plotly_white",
        legend={"orientation": "h", "y": -0.15},
    )
    return fig

trade_space_plot_interactive

trade_space_plot_interactive(results: DataFrame, x: str, y: str, z: str, feasible_mask: Series | None = None, pareto_front: DataFrame | None = None, title: str | None = None) -> Any

Interactive 3-D trade-space scatter.

PARAMETER DESCRIPTION
results

DataFrame with evaluation results

TYPE: DataFrame

x

Column for the x axis

TYPE: str

y

Column for the y axis

TYPE: str

z

Column for the z axis (also colors the points)

TYPE: str

feasible_mask

Optional boolean Series; infeasible points greyed out

TYPE: Series | None DEFAULT: None

pareto_front

Optional DataFrame of Pareto-optimal rows to highlight

TYPE: DataFrame | None DEFAULT: None

title

Plot title

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
Any

plotly.graph_objects.Figure

Source code in src/phased_array_systems/viz/interactive.py
def trade_space_plot_interactive(
    results: pd.DataFrame,
    x: str,
    y: str,
    z: str,
    feasible_mask: pd.Series | None = None,
    pareto_front: pd.DataFrame | None = None,
    title: str | None = None,
) -> Any:
    """Interactive 3-D trade-space scatter.

    Args:
        results: DataFrame with evaluation results
        x: Column for the x axis
        y: Column for the y axis
        z: Column for the z axis (also colors the points)
        feasible_mask: Optional boolean Series; infeasible points greyed out
        pareto_front: Optional DataFrame of Pareto-optimal rows to highlight
        title: Plot title

    Returns:
        plotly.graph_objects.Figure
    """
    go = _require_plotly()

    hover_cols = [x, y, z]
    fig = go.Figure()

    if feasible_mask is not None:
        infeasible = results[~feasible_mask]
        if len(infeasible):
            fig.add_trace(
                go.Scatter3d(
                    x=infeasible[x],
                    y=infeasible[y],
                    z=infeasible[z],
                    mode="markers",
                    name="Infeasible",
                    marker={"color": "lightgray", "symbol": "x", "size": 3},
                    text=_hover_text(infeasible, hover_cols),
                    hoverinfo="text",
                )
            )
        plot_data = results[feasible_mask]
    else:
        plot_data = results

    fig.add_trace(
        go.Scatter3d(
            x=plot_data[x],
            y=plot_data[y],
            z=plot_data[z],
            mode="markers",
            name="Designs",
            marker={
                "size": 4,
                "color": plot_data[z],
                "colorscale": "Viridis",
                "colorbar": {"title": z},
                "opacity": 0.75,
            },
            text=_hover_text(plot_data, hover_cols),
            hoverinfo="text",
        )
    )

    if pareto_front is not None and len(pareto_front):
        fig.add_trace(
            go.Scatter3d(
                x=pareto_front[x],
                y=pareto_front[y],
                z=pareto_front[z],
                mode="markers",
                name="Pareto optimal",
                marker={"color": "crimson", "size": 7, "symbol": "diamond"},
                text=_hover_text(pareto_front, hover_cols),
                hoverinfo="text",
            )
        )

    fig.update_layout(
        title=title or f"Trade space: {x} / {y} / {z}",
        scene={"xaxis_title": x, "yaxis_title": y, "zaxis_title": z},
        template="plotly_white",
    )
    return fig

figure_to_html_div

figure_to_html_div(fig: Any) -> str

Render a plotly figure as a self-contained HTML fragment.

plotly.js is inlined (no CDN), so reports remain single-file and viewable offline.

Source code in src/phased_array_systems/viz/interactive.py
def figure_to_html_div(fig: Any) -> str:
    """Render a plotly figure as a self-contained HTML fragment.

    plotly.js is inlined (no CDN), so reports remain single-file and
    viewable offline.
    """
    html = fig.to_html(full_html=False, include_plotlyjs="inline")
    return str(html)