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()
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()
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.
Plot helper gallery
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()
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()
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()
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()
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()
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()
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()
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()
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.