Chris Parmer — home

Van der Pol phase portrait

Nonlinear Dynamics

Example from the compendium of canonical charts

Nonlinear Dynamics — Van der Pol phase portrait

Python Code

"""Nonlinear Dynamics — Van der Pol phase portrait."""
from pathlib import Path

# ── Palette + theme (matching the Plotly Studio gallery these charts ship in) ──
VIOLET, TEAL, GREEN, PINK, ORANGE = "#845EEE", "#52B3D0", "#55B685", "#DA5597", "#E9A23B"
PRIMARY, SECONDARY = VIOLET, TEAL
COLORWAY = [VIOLET, TEAL, GREEN, PINK, ORANGE]
BG, TEXT, GRID, MUTED = "#ffffff", "#1c2024", "#d9d9e0", "#60646c"
FONT = "Inter, -apple-system, BlinkMacSystemFont, sans-serif"
COLORSCALE = [[0, "rgba(132, 94, 238, 0.05)"], [1, "rgba(132, 94, 238, 0.9)"]]


def apply_theme(fig):
    """Light gallery theme: white background, Inter font, soft gridlines."""
    fig.update_layout(
        paper_bgcolor=BG, plot_bgcolor=BG, colorway=COLORWAY,
        font=dict(family=FONT, color=TEXT, size=12),
        legend=dict(font=dict(color=TEXT)),
        hoverlabel=dict(bgcolor="#f0f0f3", font=dict(color=TEXT, family=FONT), bordercolor=GRID),
    )
    fig.update_xaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)
    fig.update_yaxes(gridcolor=GRID, linecolor=GRID, zerolinecolor=GRID)


def fetch_csv(url, **kwargs):
    import io
    import pandas as pd
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return pd.read_csv(io.StringIO(r.text), **kwargs)


def fetch_json(url):
    import requests
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.json()

import numpy as np
import plotly.graph_objects as go
import plotly.figure_factory as ff
from scipy.integrate import solve_ivp


MU = 1.0


def vdp(t, y):
    x, v = y
    return [v, MU * (1 - x**2) * v - x]


def generate():
    ICs = [(0.1, 0), (3, 0), (-3, 0), (0.5, 2), (-2, -2)]
    colors = [VIOLET, GREEN, TEAL, PINK, TEAL]

    fig = go.Figure()

    # Background quiver field on a coarse grid
    gx = np.linspace(-4, 4, 16)
    gy = np.linspace(-4, 4, 16)
    GX, GY = np.meshgrid(gx, gy)
    GU = GY
    GV = MU * (1 - GX**2) * GY - GX

    # Normalize arrow length for display
    mag = np.sqrt(GU**2 + GV**2) + 1e-10
    scale_factor = 0.25
    GU_n = GU / mag * scale_factor
    GV_n = GV / mag * scale_factor

    quiver_fig = ff.create_quiver(
        GX, GY, GU_n, GV_n,
        scale=1.0, arrow_scale=0.3,
    )
    for trace in quiver_fig.data:
        trace.line.color = MUTED
        trace.opacity = 0.35
        trace.showlegend = False
        fig.add_trace(trace)

    # Trajectory for each IC
    t_span = (0, 30)
    t_eval = np.linspace(0, 30, 3000)

    for (x0, v0), color in zip(ICs, colors):
        sol = solve_ivp(vdp, t_span, [x0, v0], t_eval=t_eval, dense_output=True, rtol=1e-8, atol=1e-10)
        x_traj = sol.y[0]
        v_traj = sol.y[1]

        fig.add_trace(go.Scatter(
            x=x_traj, y=v_traj,
            mode="lines",
            name=f"IC=({x0},{v0})",
            line=dict(color=color, width=1.8),
            hovertemplate="x=%{x:.2f}, ẋ=%{y:.2f}<extra></extra>",
        ))

    fig.update_layout(
        xaxis=dict(
            title="x",
            range=[-4.5, 4.5],
            zeroline=True,
            zerolinecolor=GRID,
            zerolinewidth=1,
        ),
        yaxis=dict(
            title="dx/dt",
            range=[-4.5, 4.5],
            zeroline=True,
            zerolinecolor=GRID,
            zerolinewidth=1,
        ),
        legend=dict(orientation="h", y=1.08),
        margin=dict(t=40, b=60, l=70, r=40),
        height=580,
    )
    apply_theme(fig)
    return fig


fig = generate()
fig.show()

Made with Plotly