Skip to content

Plotting examples

Overview

These demos capture the plotting helpers that sit on top of dynlib's simulation stack. They show how to build time series, phase portraits, return maps, vector fields, histograms, and animations with consistent styling and the dynlib.plot API.

Time series and phase portraits

Logistic map diagnostics

from dynlib import setup
from dynlib.plot import series, export, theme, fig, return_map, cobweb

model = '''
inline:
[model]
type="map"
name="Logistic Map"

[states]
x=0.1

[params]
r=4.0

[equations.rhs]
x = "r * x * (1 - x)"
'''

# stepper="map" is default and can be omitted for map models
sim = setup(model, stepper="map", jit=True, disk_cache=True)
sim.run(N=192, transient=40)
sim.run(resume=True, N=400)
res=sim.results()

seeds = [[0.1], [0.7], [0.9]]
fps = sim.model.fixed_points(seeds=seeds)
print(fps.points)

theme.use("notebook")
theme.update(grid=False)

# Create 1x2 grid for time series and return map
ax = fig.grid(rows=1, cols=2, size=(12, 5))

# Time series plot
series.plot(
    x=res.t,
    y=res["x"],
    style="line",
    ax=ax[0, 0],
    xlabel="n",
    ylabel="$x_n$",
    ylabel_rot=0,
    title="Logistic Map (r=4)",
    ypad=10,
    xlabel_fs=13,
    ylabel_fs=13,
    title_fs=14,
    xtick_fs=9,
    ytick_fs=11,
    lw=1.0
)

# Return map: x[n] vs x[n+1]
return_map(
    x=res["x"],
    step=1,
    style="scatter",
    ax=ax[0, 1],
    ms=2,
    color="C1",
    title="Return Map: $x[n]$ vs $x[n+1]$",
    xlabel_fs=13,
    ylabel_fs=13,
    title_fs=14,
    xtick_fs=9,
    ytick_fs=11,
)

cobweb(
    f=sim.model,
    x0=0.1,
    xlim=(0, 1),
    steps=50,
    color="green",
    stair_color="orange",
    identity_color="red",
)

export.show()
Builds the builtin logistic map, runs with a transient, and then plots the time series, return map, and cobweb diagram. The script also prints the fixed points found by sim.model.fixed_points(seeds=...) so you can compare analytic predictions with the numerically discovered attractors.

Van der Pol oscillator

from dynlib import setup
from dynlib.plot import series, export, fig, phase
from dynlib.utils import Timer

stepper = "tr-bdf2a"
mu = 1000.0

sim = setup("builtin://ode/vanderpol", 
            stepper=stepper, 
            jit=True,
            disk_cache=False)

sim.assign(mu=mu)
sim.config(dt=5e-4, max_steps=6_500_000)
with Timer("run simulation"):
    sim.run(T=3000.0)
res = sim.results()

series.plot(x=res.t, y=res["x"],
            title=f"Van der Pol Oscillator (μ={mu})",
            xlabel="Time",
            ylabel="x",
            ylim=(-3, 3),
            )

phase.xy(x=res["x"], y=res["y"])
export.show()
Runs the stiff builtin://ode/vanderpol model with the tr-bdf2a stepper, times the run with dynlib.utils.Timer, and plots both the time series and phase portrait. It demonstrates how to tune dt/max_steps for long runs while keeping the figure creation simple.

Basic plotting primitives

from __future__ import annotations
"""
Demonstration of various plotting functions from dynlib.plot module.
Only the ones not used in other examples are shown here.
"""

import numpy as np
from dynlib.plot import fig, series, phase, utils, export, theme


def main() -> None:
    theme.use("notebook")

    # ----- Time data -----
    t = np.linspace(0, 10, 101)
    y = np.sin(2 * np.pi * 0.5 * t)
    y2 = np.cos(2 * np.pi * 0.5 * t)
    # step-like signal
    y_step = np.floor(t) % 2
    # noisy distribution for histogram
    y_hist = y + 0.3 * np.random.randn(len(t))

    # ----- Discrete (map) data -----
    r = 3.7
    x0 = 0.2
    n_iter = 30
    xs = [x0]
    x = x0
    for k in range(n_iter):
        x = r * x * (1 - x)
        xs.append(x)
    ks = np.arange(len(xs))
    xs = np.array(xs)

    # Layout: 3 rows x 2 cols
    ax = fig.grid(rows=3, cols=2, size=(10, 12), sharex=False)

    # Row 0, Col 0: series.stem
    series.stem(
        x=t,
        y=y,
        ax=ax[0, 0],
        label="sin(t) stems",
        color="C0",
        xlabel="$t$",
        ylabel="$y$",
        title="series.stem: stem plot",
    )

    # Row 0, Col 1: series.step
    series.step(
        x=t,
        y=y_step,
        ax=ax[0, 1],
        label="step(t)",
        color="C1",
        xlabel="$t$",
        ylabel="$y$",
        title="series.step: step plot",
    )

    # Row 1, Col 0: utils.hist
    utils.hist(
        y=y_hist,
        bins=30,
        density=False,
        ax=ax[1, 0],
        color="C2",
        title="utils.hist: histogram",
        xlabel="$y$",
        ylabel="count",
    )

    # Row 1, Col 1: phase.xy with scatter style (for discrete maps)
    phase.xy(
        x=y,
        y=y2,
        style="scatter",
        ax=ax[1, 1],
        ms=6,
        color="C3",
        title="phase.xy: sin vs cos (scatter)",
        xlabel=r"$\sin$",
        ylabel=r"$\cos$",
    )

    # Row 2, Col 0: series with discrete/map style (stem-like effect)
    series.plot(
        x=ks,
        y=xs,
        style="map",
        ax=ax[2, 0],
        color="C4",
        title="series.plot: logistic iterations (map style)",
        xlabel="n",
        ylabel="$x_n$",
    )

    # Row 2, Col 1: series with mixed style (line + markers)
    series.plot(
        x=ks,
        y=xs,
        style="mixed",
        ax=ax[2, 1],
        color="C5",
        title="series.plot: logistic iterations (mixed style)",
        xlabel="n",
        ylabel="$x_n$",
    )

    # Tighten layout and show
    export.show()


if __name__ == "__main__":
    main()
Presents one figure with six subplots to showcase series.stem, series.step, utils.hist, phase.xy, and the series.plot styles map and mixed. This is a quick reference for how each helper handles line, stem, histogram, and map-style data.

Theme presets

#!/usr/bin/env python3
"""
Demonstration of all theme presets available in dynlib.plot.theme.

This script creates a sample figure for each preset and saves it as an image.
"""

import numpy as np
from dynlib.plot import fig, series, theme, export, utils


def create_sample_figure():
    """Create a sample figure with various plot elements."""
    # Generate sample data
    t = np.linspace(0, 10, 100)
    y1 = np.sin(t)
    y2 = np.cos(t)
    y3 = np.sin(t) * np.exp(-t / 10)

    # Create figure with subplots
    ax = fig.grid(rows=2, cols=2, size=(8, 6))

    # Line plot
    series.plot(x=t, y=y1, ax=ax[0, 0], label="sin(t)", xlabel="Time", ylabel="Amplitude", title="Line Plot")

    # Scatter plot
    series.plot(x=t[::5], y=y2[::5], ax=ax[0, 1], style="scatter", label="cos(t) samples", xlabel="Time", ylabel="Amplitude", title="Scatter Plot")

    # Multiple lines
    series.plot(x=t, y=y1, ax=ax[1, 0], label="sin(t)", color="C0")
    series.plot(x=t, y=y2, ax=ax[1, 0], label="cos(t)", color="C1")
    series.plot(x=t, y=y3, ax=ax[1, 0], label="damped sin(t)", color="C2")
    ax[1, 0].set_xlabel("Time")
    ax[1, 0].set_ylabel("Amplitude")
    ax[1, 0].set_title("Multiple Lines")
    ax[1, 0].legend()

    # Histogram
    data = np.random.normal(0, 1, 1000)
    utils.hist(y=data, ax=ax[1, 1], bins=30, xlabel="Value", ylabel="Frequency", title="Histogram")

    return ax[0, 0].figure


def main():
    """Demonstrate each theme preset."""
    presets = ["notebook", "paper", "talk", "dark", "mono"]

    for preset in presets:
        print(f"Creating figure with '{preset}' preset...")

        # Apply theme
        theme.use(preset)

        # Create sample figure
        fig = create_sample_figure()

        # Save figure
        export.savefig(fig, f"theme_{preset}", fmts=("png",), dpi=150)

        print(f"Saved theme_{preset}.png")


if __name__ == "__main__":
    main()
Iterates through the notebook, paper, talk, dark, and mono themes, renders a sample figure, and saves PNGs so you can inspect how each preset affects colors, gridlines, and typography.

Faceted histograms

import dynlib.plot as plot
import numpy as np

# Sample data: a dictionary where keys are categories and values are data arrays
data = {
    'Category A': np.random.randn(100),
    'Category B': np.random.randn(100) + 1,
    'Category C': np.random.randn(100) - 1,
}

# Create facets: 2 columns, with a title
for ax, key in plot.facet.wrap(data.keys(), cols=2, title='Data by Category'):
    values = data[key]
    ax.hist(values, bins=20, alpha=0.7)
    ax.set_title(f'Histogram for {key}')
    ax.set_xlabel('Value')
    ax.set_ylabel('Frequency')

# Display the plot
plot.export.show()
Uses plot.facet.wrap to build a grid of histogram panels for multiple categories. Each axis receives its own data slice plus titles/labels so you can explore the data-distribution helper without manually creating plt.subplots.

Vector field helper

from __future__ import annotations

"""
Demonstration of the dynlib.plot.vectorfield helper.
"""

import numpy as np

from dynlib import build, plot


def _make_model():
    # Simple spiral system with tunable linear terms
    model_uri = """
inline:
[model]
type = "ode"
name = "spiral"

[sim]
t0 = 0.0
dt = 0.1

[states]
x = 0.0
y = 0.0

[params]
a = 0.8
b = 0.2

[equations.rhs]
x = "a * x - y"
y = "x + b * y"
"""
    return build(model_uri, jit=False, disk_cache=False)


def main() -> None:
    plot.theme.use("notebook")
    model = _make_model()

    ax = plot.fig.single(title="Vector field demo")

    handle = plot.vectorfield(
        model,
        ax=ax,
        xlim=(-2, 2),
        ylim=(-2, 2),
        grid=(25, 25),
        normalize=True,
        speed_color=True,
        speed_cmap="plasma",
        nullclines=True,
        nullcline_style={"colors": ["#333333"], "linewidths": 1.2, "alpha": 0.6},
    )

    # Update parameters and redraw to illustrate handle.update()
    handle.update(params={"a": 1.2, "b": -0.1})

    plot.export.show()


if __name__ == "__main__":
    main()
Shows plot.vectorfield with a spiral model, getter handles that update parameters (a, b), nullclines, and a custom color mapping for speed. It demonstrates how the returned handle can redraw the vector field when you tweak parameters interactively.

Animated & swept vector fields

Vector field animation demo

from __future__ import annotations

"""Demonstration of plot.vectorfield_animate for a simple spiral system."""

from dynlib import build, plot


def _make_model():
    model_uri = """
inline:
[model]
type = "ode"
name = "spiral"

[sim]
t0 = 0.0
dt = 0.05

[states]
x = 0.0
y = 0.0

[params]
a = -0.4
b = 0.25

[equations.rhs]
x = "a * x - y"
y = "x + b * y"
"""
    return build(model_uri, jit=False, disk_cache=False)


def main() -> None:
    plot.theme.use("notebook")
    model = _make_model()

    values = [v for v in (-1.0, -0.4, 0.0, 0.6, 1.0, 1.4)]
    # You have to define anim (or any other name) even if you don't use it. 
    # Otherwise it gets garbage collected.
    anim = plot.vectorfield_animate(
        model,
        param="a",
        values=values,
        xlim=(-2.5, 2.5),
        ylim=(-2.5, 2.5),
        grid=(24, 24),
        normalize=True,
        speed_color=True,
        speed_cmap="plasma",
        title_func=lambda v, idx: f"Vector field: a={float(v):.2f}",
        nullclines=True,
        nullcline_style={"colors": ["#333333"], "linewidths": 1.0, "alpha": 0.6},
        interactive=False,
        fps=3,
        blit=False,
    )

    # Preview the animation in notebook/backends that support it, or save via anim.animation.save(...)
    plot.export.show()


if __name__ == "__main__":
    main()
Uses plot.vectorfield_animate to step the a parameter through several profiles while normalizing and color-mapping the speed. The animation can preview in notebooks or be saved later with Matplotlib writers.

Dense vector field animation

# An interesting animation that I like.

import numpy as np
from dynlib.plot import export, vectorfield_animate


DSL = """
inline:
[model]
type="ode"

[states]
x=0.0
y=0.0

[params]
k=0.0

[equations.rhs]
x = "sin(k*y)"
y = "sin(k*x)"
"""

# You have to define anim (or any other name) even if you don't use it. 
# Otherwise it gets garbage collected.
anim=vectorfield_animate(DSL, 
                    param="k", 
                    values=np.linspace(0.1,10,300), 
                    xlim=(-10, 10), 
                    ylim=(-10, 10), 
                    grid=(24, 24), 
                    interval=130,
                    normalize=True,
                    title_func=lambda v, idx: f"Vector field: k={float(v):.2f}",
                    )

# Save using writer of your choice, e.g., "ffmpeg", "pillow", etc.
# anim.save("vectorfield_animation.gif", writer="pillow", dpi=150)

export.show()
Builds a sin/cos-based vector field and sweeps the frequency k across 300 frames, giving you an extensive example of how to keep the anim handle alive so it doesn't get garbage-collected before you save.

High-dimensional slices

from __future__ import annotations

"""
Demonstration of projecting a higher-dimensional vector field onto chosen 2D planes.

We use the 3D Lorenz system and visualize two slices:
- x/y plane with z fixed (and then updated to a new z to show handle.update)
- y/z plane with x fixed

Click on either panel to launch a short trajectory through that slice.
"""

from dynlib import build, plot


def _lorenz_model():
    model_uri = """
inline:
[model]
type = "ode"
name = "lorenz-3d"
stepper = "rk4"

[sim]
t0 = 0.0
dt = 0.01
t_end = 8.0

[states]
x = 1.0
y = 1.0
z = 1.0

[params]
sigma = 10.0
rho = 28.0
beta = 2.6666666666666665

[equations.rhs]
x = "sigma * (y - x)"
y = "x * (rho - z) - y"
z = "x * y - beta * z"
"""
    return build(model_uri, jit=False, disk_cache=False)


def main() -> None:
    plot.theme.use("notebook")
    model = _lorenz_model()

    ax = plot.fig.grid(rows=1, cols=2, size=(12, 5), sharex=False, sharey=False)

    handle_xy = plot.vectorfield(
        model,
        ax=ax[0, 0],
        vars=("x", "y"),
        fixed={"z": 5.0},
        xlim=(-20, 20),
        ylim=(-30, 30),
        grid=(25, 25),
        normalize=False,
        nullclines=False,
        T=6.0,
        dt=0.01,
        trajectory_style={"color": "C0"},
    )
    ax[0, 0].set_title("x-y slice with z fixed (click to trace)", fontsize=11)

    handle_yz = plot.vectorfield(
        model,
        ax=ax[0, 1],
        vars=("y", "z"),
        fixed={"x": 0.0},
        xlim=(-30, 30),
        ylim=(0, 50),
        grid=(25, 25),
        normalize=False,
        nullclines=False,
        T=6.0,
        dt=0.01,
        trajectory_style={"color": "C1"},
    )
    ax[0, 1].set_title("y-z slice with x fixed (click to trace)", fontsize=11)

    # Show that fixed values can be updated without rebuilding the figure.
    handle_xy.update(fixed={"z": 15.0}, redraw=True)
    handle_xy.ax.set_title("x-y slice with z updated to 15.0", fontsize=11)

    plot.export.show()


if __name__ == "__main__":
    main()
Projects the 3D Lorenz vector field onto two different 2D slices (x/y and y/z) with adjustable fixed-state values. The demo lets you click either panel to trigger a trajectory trace and shows how to reuse the returned handle to update the slice without recreating axes.

Vector field sweeps

from __future__ import annotations

"""Demonstration of plot.vectorfield_sweep for a simple 2D system."""

from dynlib import build, plot


def _make_model():
    model_uri = """
inline:
[model]
type = "ode"
name = "spiral"

[sim]
t0 = 0.0
dt = 0.05

[states]
x = 0.0
y = 0.0

[params]
a = 0.5
b = -0.2

[equations.rhs]
x = "a * x - y"
y = "x + b * y"
"""
    return build(model_uri, jit=False, disk_cache=False)


def main() -> None:
    plot.theme.use("notebook")
    model = _make_model()

    plot.vectorfield_sweep(
        model,
        param="a",
        values=[-0.6, 0.0, 0.6, 1.2],
        xlim=(-2.5, 2.5),
        ylim=(-2.5, 2.5),
        grid=(22, 22),
        normalize=True,
        speed_color=True,
        speed_cmap="plasma",
        cols=2,
        facet_titles="a={value:.2f}",
        title="Vector field sweep over parameter 'a'",
        nullclines=True,
        nullcline_style={"colors": ["#333333"], "linewidths": 1.0, "alpha": 0.6},
        interactive=False,
        size=(8,6)
    )

    plot.export.show()


if __name__ == "__main__":
    main()
Sweeps the spiral model over a list of a values, arranges the resulting fields in a grid, and shows how plot.vectorfield_sweep automatically handles layout, nullclines, and annotations so you can compare parameter regimes at a glance.