Symbolic Jacobian and Stability Analysis¶
This notebook demonstrates the symbolic-numeric bridge for dynamical
systems analysis. We use Maxima's diff() to derive Jacobian matrices
exactly, solve() to find equilibrium points, and np_eig to compute
eigenvalues numerically for stability classification.
The workflow is: symbolic differentiation -> symbolic algebra -> numeric eigenvalues -> visualization. Each step leverages the right tool for the job.
load("numerics")$
load("ax-plots")$
The Jacobian [S+N]¶
Consider a predator-prey variant:
$$\dot{x} = x(1-x) - xy \qquad \dot{y} = y(x - 1)$$
Here $x$ is the prey population (with logistic self-limitation) and $y$
is the predator. We compute the Jacobian matrix symbolically using
diff().
/* Define the system */
f1 : x*(1 - x) - x*y$
f2 : y*(x - 1)$
/* Compute the Jacobian symbolically */
J : matrix(
[diff(f1, x), diff(f1, y)],
[diff(f2, x), diff(f2, y)]
);
print("Jacobian:")$
J;
matrix([-y-2*x+1,-x],[y,x-1]) Jacobian:
Fixed Points [S+N]¶
Equilibria are where both $\dot{x} = 0$ and $\dot{y} = 0$ simultaneously.
Maxima's solve() finds them exactly.
/* Find all equilibrium points */
fps : solve([f1 = 0, f2 = 0], [x, y]);
print("Fixed points:")$
for fp in fps do print(fp)$
[[x = 0,y = 0],[x = 1,y = 0]] Fixed points: [x = 0,y = 0] [x = 1,y = 0]
Stability Classification [S+N]¶
For each fixed point, we substitute into the Jacobian and compute eigenvalues. The eigenvalues determine stability:
- Both eigenvalues have negative real part: stable (node or spiral)
- Both positive: unstable
- Mixed signs: saddle point
We use Maxima's eigenvalues() for the symbolic case and np_eig for
the numeric case.
/* Evaluate Jacobian at each fixed point and classify */
for fp in fps do block(
[J_fp, ev_list, re1, re2, cl],
J_fp : subst(fp, J),
ev_list : first(eigenvalues(J_fp)),
re1 : float(realpart(ev_list[1])),
re2 : float(realpart(ev_list[2])),
cl : if is(re1 < 0 and re2 < 0) then "STABLE"
elseif is(re1 > 0 and re2 > 0) then "UNSTABLE"
elseif is(re1 * re2 < 0) then "SADDLE"
else "MARGINAL / CENTRE",
print("--- Fixed point:", fp, "---"),
print(" J =", J_fp),
print(" Eigenvalues =", ev_list),
print(" Classification:", cl)
)$
--- Fixed point: [x = 0,y = 0] --- J = matrix([1,0],[0,-1]) Eigenvalues = [-1,1] Classification: SADDLE --- Fixed point: [x = 1,y = 0] --- J = matrix([-1,-1],[0,0]) Eigenvalues = [-1,0] Classification: MARGINAL / CENTRE
Numeric Eigenvalues with np_eig¶
For more complex systems where symbolic eigenvalues may not simplify,
we can evaluate the Jacobian numerically and use np_eig from the
numerics package (wrapping LAPACK's dgeev).
/* Numeric eigenvalue computation at (1, 0) using np_eig */
J_at_10 : float(subst([x=1, y=0], J))$
print("J at (1,0) =", J_at_10)$
A_np : ndarray(J_at_10)$
[vals, vecs] : np_eig(A_np)$
print("Eigenvalues (numeric):", np_to_list(vals))$
print("Eigenvectors:")$
np_to_matrix(vecs);
J at (1,0) = matrix([-1.0,-1.0],[0.0,0.0]) Eigenvalues (numeric): [-1.0,0.0] Eigenvectors:
Phase Portrait¶
The vector field shows the flow of the dynamical system at every point. Streamlines trace trajectories from various initial conditions, revealing the global behaviour around the fixed points.
/* Vector field of the predator-prey system */
ax_draw2d(
color="#cccccc", ngrid=12,
ax_vector_field(f1, f2, x, -0.5, 2.5, y, -0.5, 2.5),
title="Predator-Prey: Vector Field",
xlabel="x (prey)", ylabel="y (predator)",
aspect_ratio=true
)$
/* Phase portrait: vector field with streamlines */
ax_draw2d(
color="#dddddd", ngrid=12,
ax_vector_field(f1, f2, x, -0.5, 2.5, y, -0.5, 2.5),
color="#e41a1c",
initial_points=[[0.1, 0.1], [0.5, 0.5], [1.5, 0.2],
[0.2, 1.0], [2.0, 0.5], [0.8, 1.5]],
ax_streamline(f1, f2, x, -0.5, 2.5, y, -0.5, 2.5),
title="Predator-Prey: Phase Portrait",
xlabel="x (prey)", ylabel="y (predator)",
aspect_ratio=true
)$
Van der Pol Oscillator [S+N]¶
The Van der Pol oscillator is a classic nonlinear system with a limit cycle:
$$\dot{x} = y \qquad \dot{y} = \mu(1 - x^2)y - x$$
For $\mu > 0$ the origin is an unstable spiral, and trajectories converge to a stable limit cycle. We derive the Jacobian symbolically, compute eigenvalues at the origin, and visualize the limit cycle.
/* Van der Pol system with mu = 1 */
mu : 1$
g1 : y$
g2 : mu*(1 - x^2)*y - x$
/* Symbolic Jacobian */
J_vdp : matrix(
[diff(g1, x), diff(g1, y)],
[diff(g2, x), diff(g2, y)]
);
print("Van der Pol Jacobian:")$
J_vdp;
matrix([0,1],[-(2*x*y)-1,1-x^2]) Van der Pol Jacobian:
/* Evaluate at the origin — the only fixed point */
J_vdp_origin : subst([x=0, y=0], J_vdp)$
print("J at origin:", J_vdp_origin)$
/* Symbolic eigenvalues */
evals_vdp : eigenvalues(J_vdp_origin);
print("Eigenvalues:", evals_vdp[1])$
/* Numeric check via np_eig */
A_vdp : ndarray(float(J_vdp_origin))$
[vals_vdp, vecs_vdp] : np_eig(A_vdp)$
print("Eigenvalues (numeric):", np_to_list(vals_vdp))$
J at origin: matrix([0,1],[-1,1])
[[-((sqrt(3)*%i-1)/2),(sqrt(3)*%i+1)/2],[1,1]]
Eigenvalues: [-((sqrt(3)*%i-1)/2),(sqrt(3)*%i+1)/2]
Eigenvalues (numeric):
[0.8660254037844386*%i+0.49999999999999994,
0.49999999999999994-0.8660254037844386*%i]
The eigenvalues at the origin have positive real part ($\mu/2 > 0$), confirming the origin is an unstable spiral. All nearby trajectories spiral outward — but they are captured by the stable limit cycle.
/* Phase portrait of the Van der Pol oscillator */
ax_draw2d(
color="#dddddd", ngrid=12,
ax_vector_field(g1, g2, x, -4, 4, y, -4, 4),
color="#377eb8",
initial_points=[[0.1, 0.0], [3.5, 0.0], [0.0, 0.5],
[-2.0, 2.0], [1.0, -3.0]],
t_range=[0, 30],
ax_streamline(g1, g2, x, -4, 4, y, -4, 4),
title="Van der Pol Oscillator (mu=1): Limit Cycle",
xlabel="x", ylabel="y",
aspect_ratio=true
)$
Varying the Damping Parameter¶
As $\mu$ increases, the limit cycle becomes more pronounced and the oscillation becomes more "relaxation-like" (sharp transitions). We compare the Jacobian eigenvalues at the origin for different $\mu$.
/* Eigenvalues at origin for varying mu — symbolic formula */
J_vdp_general : matrix(
[0, 1],
[-1, mu_var]
)$
evals_general : eigenvalues(J_vdp_general);
print("General eigenvalues at origin:")$
print(evals_general[1])$
/* Evaluate for specific mu values */
for m in [0.1, 0.5, 1.0, 2.0, 5.0] do block(
[ev : float(subst(mu_var=m, evals_general[1]))],
print(sconcat("mu = ", m, ":"), ev)
)$
[[-((sqrt(mu_var^2-4)-mu_var)/2),(sqrt(mu_var^2-4)+mu_var)/2],[1,1]] General eigenvalues at origin: [-((sqrt(mu_var^2-4)-mu_var)/2),(sqrt(mu_var^2-4)+mu_var)/2] mu = 0.1: [-(0.5*(1.997498435543818*%i-0.1)),0.5*(1.997498435543818*%i+0.1)] mu = 0.5: [-(0.5*(1.9364916731037085*%i-0.5)),0.5*(1.9364916731037085*%i+0.5)] mu = 1.0: [-(0.5*(1.7320508075688772*%i-1.0)),0.5*(1.7320508075688772*%i+1.0)] mu = 2.0: [1.0,1.0] mu = 5.0: [0.20871215252208009,4.7912878474779195]
/* Phase portrait with mu = 3: strongly nonlinear limit cycle */
g2_strong : 3*(1 - x^2)*y - x$
ax_draw2d(
color="#dddddd", ngrid=12,
ax_vector_field(g1, g2_strong, x, -4, 4, y, -6, 6),
color="#e41a1c",
initial_points=[[0.1, 0.0], [3.5, 0.0], [-2.0, 3.0]],
t_range=[0, 40],
ax_streamline(g1, g2_strong, x, -4, 4, y, -6, 6),
title="Van der Pol (mu=3): Relaxation Oscillation",
xlabel="x", ylabel="y"
)$
Summary¶
Key insight: Maxima's symbolic differentiation gives us the exact Jacobian matrix for any smooth system. No finite-difference approximation, no numerical error in the derivatives. We then:
- Solve for equilibria symbolically with
solve() - Substitute fixed-point coordinates into the Jacobian
- Compute eigenvalues — symbolically with
eigenvalues()for simple cases, or numerically withnp_eig(LAPACK) for larger systems - Classify stability: stable / unstable / saddle / spiral
- Visualise the phase portrait with
ax_vector_fieldandax_streamline
This is the symbolic-numeric bridge at work: exact analysis where it matters (derivatives, algebra), numerical muscle where it's needed (eigenvalues, trajectory integration, plotting).