Plotting#

The plot() function provides a quick way to visualize PCFs using matplotlib.

A single PCF#

_images/gallery_single_pcf_light.png _images/gallery_single_pcf_dark.png
Show code
def plot_single_pcf():
    f = mpcf.Pcf(np.array([[0, 1], [1, 4], [3, 2], [4, 3]], dtype=np.float32))

    fig, ax = plt.subplots(figsize=(5, 2.5))
    plotpcf(f, ax=ax, max_time=6, linewidth=2)
    ax.set_xlabel("t")
    ax.set_ylabel("f(t)")
    fig.tight_layout()
    return fig

Overlaying many PCFs#

Pass a 1-D tensor to plot all elements at once. Use alpha to see overlapping regions:

_images/gallery_overlaid_light.png _images/gallery_overlaid_dark.png
Show code
def plot_overlaid():
    X = noisy_sin((20,), n_points=80)

    fig, ax = plt.subplots(figsize=(5, 2.5))
    plotpcf(X, ax=ax, alpha=0.3, linewidth=0.8)
    ax.set_xlabel("t")
    ax.set_ylabel("f(t)")
    ax.set_title("20 noisy sine functions")
    fig.tight_layout()
    return fig

PCF arithmetic#

Since PCFs support pointwise arithmetic, you can visualize the result of operations like addition:

_images/gallery_arithmetic_light.png _images/gallery_arithmetic_dark.png
Show code
def plot_arithmetic():
    f = mpcf.Pcf(np.array([[0, 1], [1, 3], [3, 1]], dtype=np.float32))
    g = mpcf.Pcf(np.array([[0, 2], [2, 0]], dtype=np.float32))

    fig, axes = plt.subplots(1, 3, figsize=(9, 2.5), sharex=True, sharey=True)
    for ax, pcf, title in [
        (axes[0], f, "f"),
        (axes[1], g, "g"),
        (axes[2], f + g, "f + g"),
    ]:
        plotpcf(pcf, ax=ax, max_time=5, linewidth=2)
        ax.set_title(title)
        ax.set_ylim(-0.3, 6)
    axes[0].set_ylabel("value")
    fig.tight_layout()
    return fig

Highlighting the mean#

A common pattern is to plot individual noisy functions in the background with their mean highlighted in the foreground:

_images/gallery_mean_highlight_light.png _images/gallery_mean_highlight_dark.png
Show code
def plot_mean_highlight(sin_color="b", cos_color="r"):
    sines = noisy_sin((15,), n_points=100)
    cosines = noisy_cos((15,), n_points=100)

    fig, ax = plt.subplots(figsize=(5, 2.5))
    plotpcf(sines, ax=ax, color=sin_color, linewidth=0.5, alpha=0.2)
    plotpcf(cosines, ax=ax, color=cos_color, linewidth=0.5, alpha=0.2)

    plotpcf(mpcf.mean(sines), ax=ax, color=sin_color, linewidth=2.5, label="mean(sin)")
    plotpcf(mpcf.mean(cosines), ax=ax, color=cos_color, linewidth=2.5, label="mean(cos)")

    ax.set_xlabel("t")
    ax.set_ylabel("f(t)")
    ax.legend()
    fig.tight_layout()
    return fig

Persistence barcodes#

Use plot_barcode() to visualize persistence barcodes as horizontal line segments. Each bar runs from its birth to its death value. Bars with infinite death are drawn as arrows extending to the right edge of the plot.

Stack multiple homology dimensions by passing y_offset:

_images/gallery_barcode_light.png _images/gallery_barcode_dark.png
Show code
def plot_barcode_example(h0_color="steelblue", h1_color="orangered"):
    from masspcf.persistence import Barcode
    from masspcf.plotting import plot_barcode

    bc_h0 = Barcode(np.array([
        [0.0, np.inf], [0.0, 1.8], [0.0, 0.6], [0.1, 1.2],
    ], dtype=np.float64))

    bc_h1 = Barcode(np.array([
        [0.5, 2.0], [0.8, 1.5], [1.0, 3.0],
    ], dtype=np.float64))

    fig, ax = plt.subplots(figsize=(5, 2.5))
    y = plot_barcode(bc_h0, ax=ax, color=h0_color, linewidth=2, label="H0")
    plot_barcode(bc_h1, ax=ax, y_offset=y + 1, color=h1_color, linewidth=2, label="H1")
    ax.set_xlabel("t")
    ax.set_yticks([])
    ax.legend()
    fig.tight_layout()
    return fig

TDA pipeline#

A complete example: generate a random point cloud, compute its persistent homology, and plot the persistence diagram (via persim) alongside the stable rank:

_images/gallery_tda_pipeline_light.png _images/gallery_tda_pipeline_dark.png
Show code
def plot_tda_pipeline(h0_color="steelblue", h1_color="orangered"):
    from masspcf import persistence as mpers
    from masspcf.plotting import plot_barcode

    # 1. Noisy circle (clear H1 topology)
    rng = np.random.RandomState(10)
    theta = rng.uniform(0, 2 * np.pi, 30)
    r = 1.0 + rng.normal(0, 0.15, 30)
    points = np.column_stack([r * np.cos(theta), r * np.sin(theta)]).astype(np.float64)

    # 2. Compute persistent homology
    bcs = mpers.compute_persistent_homology(points, max_dim=1, verbose=False)
    bc_h0, bc_h1 = bcs[0], bcs[1]

    # 3. Convert to stable rank
    sranks = mpers.barcode_to_stable_rank(bcs)

    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 3),
                                         gridspec_kw={"width_ratios": [1, 1, 1.2]})

    # Left: point cloud
    ax1.scatter(points[:, 0], points[:, 1], s=15, color="grey", edgecolors="black",
                linewidths=0.5)
    ax1.set_aspect("equal")
    ax1.set_title("Point cloud")

    # Middle: persistence diagram (via persim)
    import persim
    persim.plot_diagrams(
        [np.asarray(bc_h0), np.asarray(bc_h1)],
        ax=ax2, legend=True, show=False,
    )
    legend = ax2.get_legend()
    legend.get_frame().set_alpha(0)
    fg = ax2.xaxis.label.get_color()
    for text in legend.get_texts():
        text.set_color(fg)
    for line in ax2.get_lines():
        line.set_color(fg)
    ax2.set_title("Persistence diagram")

    # Right: stable rank
    plotpcf(sranks[0], ax=ax3, max_time=2, color=h0_color, linewidth=2,
            label="H0")
    plotpcf(sranks[1], ax=ax3, max_time=2, color=h1_color, linewidth=2,
            label="H1")
    ax3.set_xlabel("t")
    ax3.set_ylabel("rank")
    ax3.set_title("Stable rank")
    leg3 = ax3.legend(fontsize=8)
    leg3.get_frame().set_alpha(0)
    for text in leg3.get_texts():
        text.set_color(fg)

    fig.tight_layout(w_pad=1.5)
    return fig

Betti curve pipeline#

The same pipeline using a barcode plot and Betti curves:

_images/gallery_betti_pipeline_light.png _images/gallery_betti_pipeline_dark.png
Show code
def plot_betti_pipeline(h0_color="steelblue", h1_color="orangered"):
    from masspcf import persistence as mpers
    from masspcf.plotting import plot_barcode

    # 1. Noisy circle
    rng = np.random.RandomState(10)
    theta = rng.uniform(0, 2 * np.pi, 30)
    r = 1.0 + rng.normal(0, 0.15, 30)
    points = np.column_stack([r * np.cos(theta), r * np.sin(theta)]).astype(np.float64)

    # 2. Compute persistent homology
    bcs = mpers.compute_persistent_homology(points, max_dim=1, verbose=False)
    bc_h0, bc_h1 = bcs[0], bcs[1]

    # 3. Convert to Betti curves
    bettis = mpers.barcode_to_betti_curve(bcs, verbose=False)

    fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 3),
                                         gridspec_kw={"width_ratios": [1, 1, 1.2]})

    # Left: point cloud
    ax1.scatter(points[:, 0], points[:, 1], s=15, color="grey",
                edgecolors="black", linewidths=0.5)
    ax1.set_aspect("equal")
    ax1.set_title("Point cloud")

    # Middle: persistence barcode
    y = plot_barcode(bc_h0, ax=ax2, color=h0_color, linewidth=2, label="H0")
    plot_barcode(bc_h1, ax=ax2, y_offset=y + 1, color=h1_color, linewidth=2,
                 label="H1")
    ax2.set_yticks([])
    ax2.set_xlabel("t")
    ax2.set_title("Persistence barcode")
    ax2.legend(fontsize=8)

    # Right: Betti curves
    plotpcf(bettis[0], ax=ax3, max_time=2, color=h0_color, linewidth=2,
            label="H0")
    plotpcf(bettis[1], ax=ax3, max_time=2, color=h1_color, linewidth=2,
            label="H1")
    ax3.set_xlabel("t")
    ax3.set_ylabel("count")
    ax3.set_title("Betti curve")
    ax3.legend(fontsize=8)

    fig.tight_layout(w_pad=1.5)
    return fig

max_time#

By default, a single PCF is plotted only up to its last breakpoint. Pass max_time to extend the final constant segment:

f = mpcf.Pcf([[0, 1], [2, 3]])
plotpcf(f, ax=ax, max_time=5)  # extends the plot to t=5

When plotting a 1-D tensor, all elements are automatically extended to the latest breakpoint across the tensor. Passing max_time overrides this with a custom value.

Styling#

The plot function accepts any keyword arguments that matplotlib’s step function does (color, linewidth, alpha, label, etc.). When plotting a 1-D tensor with auto_label=True, each PCF is automatically labeled as f0, f1, etc.