Chapter 23
25 min read
Section 202 of 353

Nonlinear Systems and Linearization

Systems of Differential Equations

Learning Objectives

By the end of this section you will be able to:

  1. Find every fixed point of a 2-D nonlinear system x=F(x)\mathbf{x}' = \mathbf{F}(\mathbf{x}) by solving F(x)=0\mathbf{F}(\mathbf{x}^\ast)=\mathbf{0}.
  2. Compute the Jacobian matrix J(x)=F/xxJ(\mathbf{x}^\ast) = \partial\mathbf{F}/\partial\mathbf{x}\big|_{\mathbf{x}^\ast} analytically and verify it numerically.
  3. Classify each fixed point as saddle, node, spiral, or center using the trace, determinant, and discriminant of JJ.
  4. State and apply the Hartman–Grobman theorem — the rule that says, for hyperbolic fixed points, the nonlinear flow looks topologically like the linearized flow.
  5. Identify the failure case (centers, zero eigenvalues) and know that you need a finer tool there.
  6. Implement the whole pipeline — fixed-point search, Jacobian, classification — first in NumPy with finite differences, then in PyTorch with exact autograd.

Why Linearize?

“The equations of physics are almost never linear. So why does almost every textbook chapter solve linear systems?”

Because near every interesting point, every smooth nonlinear system looks linear. Zoom in close enough on the orbit of a comet, the swing of a pendulum, the population of a forest, the voltage in a Hodgkin–Huxley neuron, and you will always see the same family of linear pictures we already mastered in sections 23.02–23.06: stable spirals, unstable nodes, saddles, centers.

Linearization is the bridge. It says: solve the linear problem once, then transplant the answer to every fixed point of every smooth nonlinear system in nature. That is the entire content of this section.

🪐 Mechanics

  • Pendulum (this section)
  • Three-body equilibrium points (Lagrange)
  • Aircraft trim states

🌿 Biology & ecology

  • Predator–prey equilibria (§ 23.08)
  • Gene-regulatory toggle switches
  • Neuron rest potentials

🎛 Control & AI

  • Robotic balance points
  • Reinforcement-learning policies near fixed strategies
  • Optimizer stability around minima

The Idea: A Microscope at Each Fixed Point

Picture the flow of a 2-D nonlinear system x=F(x)\mathbf{x}' = \mathbf{F}(\mathbf{x}) as a wind field swirling across the plane. At a fixed point x\mathbf{x}^\ast the wind dies — air is perfectly still. Drop a leaf at that point, it stays. Drop a leaf slightly off the point, and it drifts. Which way?

Zoom your microscope into a tiny disk around x\mathbf{x}^\ast. Set u=xx\mathbf{u} = \mathbf{x} - \mathbf{x}^\ast (the deviation). A first-order Taylor expansion of F\mathbf{F} around x\mathbf{x}^\ast says

F(x+u)  =  F(x)=0  +  J(x)u  +  O(u2).\mathbf{F}(\mathbf{x}^\ast + \mathbf{u}) \;=\; \underbrace{\mathbf{F}(\mathbf{x}^\ast)}_{=\,\mathbf{0}} \;+\; J(\mathbf{x}^\ast)\,\mathbf{u} \;+\; \mathcal{O}(\|\mathbf{u}\|^2).

Drop the higher-order term and the flow u=F(x+u)\mathbf{u}' = \mathbf{F}(\mathbf{x}^\ast + \mathbf{u}) collapses to a linear system:

  u  =  J(x)u  \boxed{\;\mathbf{u}' \;=\; J(\mathbf{x}^\ast)\,\mathbf{u}\;}

Three sentences, three actions

  1. Find every x\mathbf{x}^\ast with F(x)=0\mathbf{F}(\mathbf{x}^\ast)=\mathbf{0}.
  2. Compute J(x)J(\mathbf{x}^\ast) (the matrix of partial derivatives of F\mathbf{F}).
  3. Classify the linear system u=Ju\mathbf{u}'=J\mathbf{u} using its eigenvalues — saddle, spiral, node, or center.

Repeat at every fixed point. Glue all the local pictures together and you have the global phase portrait of a nonlinear system.


Step 1 — Finding Fixed Points

A fixed point (a.k.a. equilibrium, critical point, rest point) is any x\mathbf{x}^\ast satisfying

F(x)  =  0.\mathbf{F}(\mathbf{x}^\ast) \;=\; \mathbf{0}.

In two dimensions this is a system of two equations in two unknowns. Methods:

MethodWhen to useHow
By inspectionPolynomial / trig terms factor easilySet each component of F to zero and solve in your head.
Substitution / eliminationAlgebraic systemsSolve one equation for one variable, substitute into the other.
Newton's methodBlack-box / no closed formIterate xₙ₊₁ = xₙ − J⁻¹ F(xₙ) from several seeds.
Numerical root finderProduction codescipy.optimize.fsolve, root_scalar, or homotopy continuation.

For the damped pendulum θ˙=ω,  ω˙=sinθbω\dot\theta=\omega,\;\dot\omega=-\sin\theta - b\omega: θ˙=0\dot\theta=0 forces ω=0\omega=0, then ω˙=0\dot\omega=0 with ω=0\omega=0 gives sinθ=0\sin\theta=0, so θ{0,±π,±2π,}\theta\in\{0,\pm\pi,\pm 2\pi,\ldots\}. The pendulum has infinitely many fixed points — but physically only two are distinct: (0,0)(0,0) (hanging straight down) and (π,0)(\pi,0) (balanced straight up).


Step 2 — The Jacobian Matrix

Write the system in components F(x,y)=(F1(x,y),  F2(x,y))\mathbf{F}(x,y) = \bigl(F_1(x,y),\;F_2(x,y)\bigr). The Jacobian at a point (x,y)(x,y) is the 2×2 matrix

J(x,y)  =  [F1xF1yF2xF2y].J(x,y) \;=\; \begin{bmatrix}\dfrac{\partial F_1}{\partial x} & \dfrac{\partial F_1}{\partial y}\\[1em]\dfrac{\partial F_2}{\partial x} & \dfrac{\partial F_2}{\partial y}\end{bmatrix}.

It is the multivariable generalization of the ordinary derivative f(x)f'(x). Its columns tell you what happens to the unit vectors e1,e2\mathbf{e}_1, \mathbf{e}_2 under F\mathbf{F}; equivalently, its action on any tiny displacement u\mathbf{u} is the best linear approximation to F(x+u)F(x)\mathbf{F}(\mathbf{x}^\ast+\mathbf{u}) - \mathbf{F}(\mathbf{x}^\ast).

Pendulum example

With F1=ωF_1 = \omega and F2=sinθbωF_2 = -\sin\theta - b\,\omega the partials are

F1θ=0,      F1ω=1,      F2θ=cosθ,      F2ω=b.\frac{\partial F_1}{\partial\theta}=0,\;\;\;\frac{\partial F_1}{\partial\omega}=1,\;\;\;\frac{\partial F_2}{\partial\theta}=-\cos\theta,\;\;\;\frac{\partial F_2}{\partial\omega}=-b.

So the Jacobian is

J(θ,ω)  =  [01cosθb].J(\theta,\omega) \;=\; \begin{bmatrix}0 & 1\\ -\cos\theta & -b\end{bmatrix}.

Notice JJ depends only on θ\theta (and the constant bb). Evaluated at the two physical fixed points:

J(0,0)=[011b],J(π,0)=[01+1b].J(0,0) = \begin{bmatrix}0 & 1\\ -1 & -b\end{bmatrix},\qquad J(\pi,0) = \begin{bmatrix}0 & 1\\ +1 & -b\end{bmatrix}.

The sign of the off-diagonal entry flips because cos(π)=1\cos(\pi) = -1. That single sign change causes the entire qualitative difference between “pendulum hanging down” and “pendulum balanced up”.


Step 3 — The Linearization Theorem

Once we have J(x)J(\mathbf{x}^\ast), the small deviation u=xx\mathbf{u} = \mathbf{x}-\mathbf{x}^\ast satisfies the linear system

u  =  J(x)u.\mathbf{u}' \;=\; J(\mathbf{x}^\ast)\,\mathbf{u}.

Hartman–Grobman theorem (informal)

If every eigenvalue of J(x)J(\mathbf{x}^\ast) has nonzero real part (the fixed point is called hyperbolic), then there is a continuous, invertible change of coordinates near x\mathbf{x}^\ast that maps the nonlinear orbits onto the linear orbits of u=Ju\mathbf{u}' = J\mathbf{u}. The two flows look the same up to a smooth deformation.

In plain English: the linearization tells the truth about every hyperbolic fixed point. The only loophole is when some eigenvalue has zero real part — pure imaginary (a center) or exactly zero (a degenerate direction). In those cases the missing higher-order terms decide the actual behavior.

Why this is so powerful

We do not need to solve the nonlinear system to know its local geometry. Two partial derivatives and two eigenvalues per fixed point are enough to predict whether the system will settle, oscillate, blow up, or do a quarter-cycle saddle pass. The pendulum's entire qualitative story comes from four numbers: ±1\pm 1 (the off-diagonal entry at each fixed point) and the damping ±b\pm b.


Classifying Fixed Points by Trace and Determinant

For a 2×2 matrix JJ with trace τ=trJ\tau = \mathrm{tr}\,J and determinant Δ=detJ\Delta = \det J, the characteristic polynomial is

λ2τλ+Δ=0,\lambda^2 - \tau\lambda + \Delta = 0,

so the eigenvalues are

λ1,2  =  τ±τ24Δ2.\lambda_{1,2} \;=\; \frac{\tau \pm \sqrt{\tau^2 - 4\Delta}}{2}.

Three numbers — τ,Δ,  τ24Δ\tau,\,\Delta,\;\tau^2-4\Delta — completely determine the fixed-point type. The full chart:

Sign of ΔSign of τ²−4ΔSign of τEigenvaluesType
Δ < 0— (always > 0)Real, opposite signsSaddle (always unstable)
Δ > 0> 0τ < 0Real, both negativeStable node
Δ > 0> 0τ > 0Real, both positiveUnstable node
Δ > 0< 0τ < 0Complex, Re < 0Stable spiral
Δ > 0< 0τ > 0Complex, Re > 0Unstable spiral
Δ > 0< 0τ = 0Pure imaginaryCenter (non-hyperbolic)
Δ > 0= 0Real, repeatedDegenerate / star node
Δ = 0One zero eigenvalueNon-isolated / non-hyperbolic

The trace-determinant plane

Plot every 2×2 matrix as a point (τ,Δ)(\tau, \Delta) in the plane. The parabola τ2=4Δ\tau^2 = 4\Delta separates real from complex eigenvalues; the horizontal line Δ=0\Delta=0 separates saddles from nodes/spirals; the vertical line τ=0\tau=0 separates stable from unstable. Every region of the plane corresponds to one fixed-point type. The linear explorer below lets you wander that plane with sliders.


Interactive: Linear Phase-Plane Classifier

Before we attack the nonlinear pendulum, build muscle memory for what each linear type LOOKS like. Drag the four sliders to change the entries of AA; the panel on the right shows which region of the trace-determinant plane you have landed in. Try the preset buttons — saddle, stable spiral, center — and notice how the field flips between in/out, rotation, decay.

Loading linear phase portrait explorer…

The Damped Pendulum, End to End

Now put the three steps together on the model nonlinear system of this section. Take the damped pendulum

θ˙=ω,ω˙=sinθbω,b=0.3.\dot{\theta} = \omega,\qquad \dot{\omega} = -\sin\theta - b\,\omega,\qquad b = 0.3.

Fixed points

From θ˙=0\dot\theta=0: ω=0\omega=0. From ω˙=0\dot\omega=0: sinθ=0\sin\theta=0. Physical solutions (mod 2π2\pi):

x1=(0,0)(down),x2=(π,0)(up).\mathbf{x}^\ast_1 = (0,\,0)\quad\text{(down)},\qquad \mathbf{x}^\ast_2 = (\pi,\,0)\quad\text{(up)}.

Jacobian

J(θ,ω)=[01cosθ0.3].J(\theta,\omega) = \begin{bmatrix}0 & 1\\ -\cos\theta & -0.3\end{bmatrix}.

Linearization at the “down” fixed point

J(0,0)=[0110.3].J(0,0) = \begin{bmatrix}0 & 1\\ -1 & -0.3\end{bmatrix}. Trace τ=0.3\tau = -0.3, determinant Δ=1\Delta = 1, discriminant τ24Δ=0.094=3.91<0\tau^2-4\Delta = 0.09 - 4 = -3.91 < 0. Eigenvalues:

λ=0.3±3.912=0.15±0.989i.\lambda = \frac{-0.3 \pm \sqrt{-3.91}}{2} = -0.15 \pm 0.989\,i.

Complex pair, negative real part ⇒ stable spiral. The pendulum, released from any small angle, will oscillate while losing amplitude. Period of one oscillation: T=2π/0.9896.35T = 2\pi / 0.989 \approx 6.35 time units (matches the period 2π/ω02\pi/\omega_0 we know from the small-angle approximation).

Linearization at the “up” fixed point

J(π,0)=[01+10.3].J(\pi,0) = \begin{bmatrix}0 & 1\\ +1 & -0.3\end{bmatrix}. Trace τ=0.3\tau = -0.3, determinant Δ=1<0\Delta = -1 < 0. Δ negative is the signature of a saddle — no further work needed. Eigenvalues:

λ1,2=0.3±0.09+42=0.3±2.0222=+0.861,  1.161.\lambda_{1,2} = \frac{-0.3 \pm \sqrt{0.09 + 4}}{2} = \frac{-0.3 \pm 2.022}{2} = +0.861,\;-1.161.

One positive eigenvalue (unstable direction) and one negative eigenvalue (stable direction). The unstable manifold direction is the eigenvector for λ=+0.861\lambda = +0.861: v1(0.758,0.653)\mathbf{v}_1 \approx (0.758,\,0.653). The stable manifold direction: v2(0.653,0.758)\mathbf{v}_2 \approx (-0.653,\,0.758). These are the two dashed lines you will see in the interactive picture below.

Glue the two local pictures together: orbits spiral into the down position, except those landing on the stable manifold of the up position, which slide in along v2\mathbf{v}_2; any orbit slightly off the stable manifold gets ejected along v1\mathbf{v}_1 and eventually spirals into the next down position one full revolution away. That entire global story comes from two 2×2 matrices.


Interactive: Pendulum vs Its Linearization

Below: the full nonlinear pendulum field as faint blue arrows; the Jacobian linearization around the highlighted fixed point as bright amber arrows inside a dashed circle. Click anywhere to drop a starting state. The solid bright curve is the TRUE pendulum trajectory; the dashed curve is what the LINEARIZATION predicts from the same starting state. Near the chosen fixed point the two are indistinguishable. Far away — especially across θ=π/2\theta = \pi/2 — they diverge dramatically.

Loading pendulum phase-plane explorer…

Things to try

  • With the default b=0.3b = 0.3 and the origin selected, click on (0.3,0)(0.3, 0). The solid & dashed curves sit on top of each other — the small-angle approximation IS the linearization.
  • Now click on (2.5,0)(2.5, 0) (a large angle far from the origin). The linear prediction spirals inward as usual; the true trajectory swings over the top of the pendulum and keeps going. The linearization broke down.
  • Switch to the saddle at (π,0)(\pi, 0). Click points near (π,0)(\pi, 0) on each side of the dashed violet (stable) line — orbits jump apart along the amber (unstable) line. That is the saddle separatrix at work.
  • Slide bb to 00. The origin becomes a CENTER. The linearization predicts perfect ellipses, but the true pendulum's orbits are slightly deformed by the missing higher-order terms — Hartman–Grobman is silent at centers, and it shows.

Worked Example (Step-by-Step)

Let's do every step by hand, exactly as the computer would, for the system in the simulator (b=0.3b = 0.3).

Click to expand the full hand calculation

Step 1 — Write the system in standard form

F1(θ,ω)=ωF_1(\theta,\omega) = \omega, F2(θ,ω)=sinθ0.3ωF_2(\theta,\omega) = -\sin\theta - 0.3\,\omega. State vector x=(θ,ω)T\mathbf{x} = (\theta,\omega)^T.

Step 2 — Solve F = 0 for fixed points

F1=0ω=0F_1 = 0 \Rightarrow \omega = 0. Substitute into F2=0F_2 = 0: sinθ0.30=0sinθ=0-\sin\theta - 0.3 \cdot 0 = 0 \Rightarrow \sin\theta = 0. So θ=nπ\theta = n\pi for any integer nn. Two physically distinct fixed points: (0,0)(0, 0) (down) and (π,0)(\pi, 0) (up).

Step 3 — Compute the Jacobian as a function of (θ, ω)

J(θ,ω)=[F1/θF1/ωF2/θF2/ω]=[01cosθ0.3].J(\theta,\omega) = \begin{bmatrix}\partial F_1/\partial\theta & \partial F_1/\partial\omega\\ \partial F_2/\partial\theta & \partial F_2/\partial\omega\end{bmatrix} = \begin{bmatrix}0 & 1\\ -\cos\theta & -0.3\end{bmatrix}.

Step 4 — Evaluate at the “down” fixed point (0, 0)

J(0,0)=[0110.3].J(0,0) = \begin{bmatrix}0 & 1\\ -1 & -0.3\end{bmatrix}.

τ=0+(0.3)=0.3\tau = 0 + (-0.3) = -0.3, Δ=0(0.3)1(1)=1\Delta = 0\cdot(-0.3) - 1\cdot(-1) = 1, τ24Δ=0.094=3.91<0\tau^2-4\Delta = 0.09 - 4 = -3.91 < 0 ⇒ complex eigenvalues, negative real part ⇒ stable spiral.

λ=0.3±i3.9120.150±0.989i\lambda = \frac{-0.3 \pm i\sqrt{3.91}}{2} \approx -0.150 \pm 0.989\,i. Period of oscillation T=2π/Imλ6.353T = 2\pi/|\mathrm{Im}\,\lambda| \approx 6.353 s. Decay envelope e0.15te^{-0.15\,t} — amplitude drops to e1.50.22e^{-1.5} \approx 0.22 after one swing.

Step 5 — Evaluate at the “up” fixed point (π, 0)

J(π,0)=[01+10.3](because cosπ=1).J(\pi,0) = \begin{bmatrix}0 & 1\\ +1 & -0.3\end{bmatrix}\quad\text{(because }\cos\pi = -1\text{)}.

τ=0.3,  Δ=1\tau = -0.3,\;\Delta = -1. Δ negative is the saddle signature — we already know the answer. For the record: τ24Δ=0.09+4=4.09\tau^2-4\Delta = 0.09 + 4 = 4.09, 4.092.022\sqrt{4.09} \approx 2.022, so

λ1=0.3+2.0222+0.861,λ2=0.32.02221.161.\lambda_1 = \frac{-0.3 + 2.022}{2} \approx +0.861,\qquad \lambda_2 = \frac{-0.3 - 2.022}{2} \approx -1.161.

Saddle. Unstable direction grows like e0.861te^{0.861\,t}; stable direction decays like e1.161te^{-1.161\,t}. Time to double in the unstable direction: ln2/0.8610.805\ln 2 / 0.861 \approx 0.805 s.

Step 6 — Eigenvectors of the saddle (the manifolds)

For λ1=0.861\lambda_1 = 0.861: solve (J0.861I)v=0(J - 0.861\,I)\mathbf{v} = 0:

[0.861111.161] ⁣[v1v2]=0    v2=0.861v1.\begin{bmatrix}-0.861 & 1\\ 1 & -1.161\end{bmatrix}\!\begin{bmatrix}v_1\\v_2\end{bmatrix} = \mathbf{0} \;\Longrightarrow\; v_2 = 0.861\,v_1.

Pick v1=1,v2=0.861v_1 = 1, v_2 = 0.861; normalize to (0.758,0.653)(0.758,\,0.653). Same procedure for λ2=1.161\lambda_2 = -1.161 gives the orthogonal-looking pair (0.653,0.758)(-0.653,\,0.758). These are the two dashed lines emanating from the saddle in the interactive plot.

Step 7 — Glue the two pictures into a global phase portrait

Almost every orbit ends up in one of the basins of attraction of (2nπ,0)(2n\pi, 0). The boundary between consecutive basins is the stable manifold of the saddle ((2n+1)π,0)((2n+1)\pi, 0). Tip the pendulum just past one of those saddles and it tumbles into the NEXT basin — a literal “separatrix”.

Step 8 — Sanity check the numbers in the explorer

  • Origin selected, eigenvalue panel shows λ=0.150±0.989i\lambda = -0.150 \pm 0.989\,i. ✅
  • Saddle selected, panel shows λ1=+0.861,  λ2=1.161\lambda_1 = +0.861,\;\lambda_2 = -1.161.
  • Slide b0b \to 0: origin readout flips to Center — linearization fails here. ✅
  • Slide b>2b > 2: origin readout flips to Stable node (over-damped). ✅

Computation in Python

Two helpers do all the work: a numerical Jacobian by central differences, and a classifier that reads three numbers off any 2×2 matrix. We loop over the three fixed points of the pendulum and print a one-line verdict for each.

Find fixed points → Jacobian → classification
🐍pendulum_classify.py
1Import NumPy

We need three NumPy ingredients: array math (for the state vector), np.linalg.det / np.linalg.eigvals (for the Jacobian analysis), and np.sin / np.cos (for the pendulum's nonlinearity). The whole 'classify a fixed point' algorithm is about 20 lines because these primitives do all the heavy lifting.

5F(state, b) — the vector field

F takes a 2-vector state = (θ, ω) and a scalar damping b, and returns the time-derivative (θ′, ω′). For the pendulum θ′ = ω (definition of angular velocity) and ω′ = −sin(θ) − b·ω (Newton's law: torque from gravity minus viscous drag). Writing the system in this shape is what lets every downstream step — finding fixed points, computing the Jacobian, integrating — share the SAME function.

9find_fixed_points() — solve F(state) = 0

Fixed points are the states where the derivative vanishes — points that, once you reach them, you stay at forever. Setting θ′ = 0 forces ω = 0; then setting ω′ = 0 forces sin(θ) = 0, i.e. θ ∈ {0, ±π, ±2π, …}. Inside [-π, π] there are exactly three. For more complicated systems we'd use a root-finder (scipy.optimize.fsolve, Newton's method) — for the pendulum the answer is by inspection.

14jacobian(state, b) — header

The Jacobian J is the 2×2 matrix of partial derivatives ∂Fᵢ/∂stateⱼ evaluated at a point. It plays the same role for vector-valued F as the ordinary derivative f′(x) plays for scalar f: it's the LINEAR map that best approximates F near the point. We will use J to classify each fixed point.

17Central-difference loop

For each input axis j we perturb state by ±ε along that axis, evaluate F at both perturbed states, and take the symmetric difference (F(s + ε·eⱼ) − F(s − ε·eⱼ)) / (2ε). That gives the j-th COLUMN of the Jacobian. Central differences are O(ε²) accurate — much better than the one-sided (F(s + ε) − F(s)) / ε which is only O(ε). For the pendulum at ε = 1e−6 we recover J to about 13 digits.

24classify(J) — header

Three numbers completely determine the topological type of a 2D fixed point: the trace τ = tr J, the determinant Δ = det J, and the discriminant τ² − 4Δ. We compute all three, then walk the decision tree below.

25Trace, determinant, discriminant

These are exactly the coefficients of the characteristic polynomial of a 2×2 matrix: λ² − τλ + Δ = 0. Its two roots are the eigenvalues λ = (τ ± √(τ² − 4Δ))/2. Whether the discriminant is positive, negative, or zero tells you whether the eigenvalues are real distinct, complex conjugate, or repeated — and that's the entire classification scheme.

28np.linalg.eigvals — exact eigenvalues

Built-in eigenvalue solver. Returns a complex array even when the eigenvalues are real (imaginary parts will be ~0). We use it to PRINT the eigenvalues for inspection; the classification itself uses only the cheap trace/det/disc combination so the algorithm scales to large systems where eigendecomposition is expensive.

30disc > 0: two real eigenvalues

Real, distinct eigenvalues. The flow stretches along one eigenvector and contracts along the other (or both stretch / both contract). Three sub-cases by signs: det < 0 means the eigenvalues have OPPOSITE signs ⇒ saddle (one in, one out). det > 0 means SAME signs; then trace tells you whether both are negative (stable) or positive (unstable).

34disc < 0: complex conjugate pair

Eigenvalues are α ± iβ with β > 0. The imaginary part forces rotation; the real part α controls growth (α > 0) or decay (α < 0). Special borderline case: α = 0 (pure imaginary eigenvalues) gives a CENTER — closed orbits. Centers are the dangerous case where linearization can lie about the true nonlinear behavior, so we flag it.

39disc ≈ 0: repeated eigenvalue (degenerate)

Measure-zero boundary case: τ² = 4Δ. The two eigenvalues collide. The flow can be a degenerate node, a 'star' (if J is already a scalar multiple of the identity), or a generalized eigenvector situation. Numerically rare for real-world systems but the classifier handles it cleanly.

43Loop over the three fixed points

For the damping b = 0.3 we compute J at (0,0), (π,0), and (−π,0) and print the verdict. Expected output: origin is a STABLE SPIRAL (eigenvalues −0.15 ± 0.989 i), and BOTH ±π saddles share eigenvalues (+0.861, −1.161). Saddles always have one positive and one negative real eigenvalue — that mismatch in sign is what creates the 'one direction in, one direction out' geometry.

34 lines without explanation
1import numpy as np
2
3# ----------------------------------------------------------------------
4# Damped pendulum:  theta' = omega,  omega' = -sin(theta) - b * omega
5# ----------------------------------------------------------------------
6def F(state, b):
7    theta, omega = state
8    return np.array([omega, -np.sin(theta) - b * omega])
9
10def find_fixed_points():
11    """Return all (theta*, omega*) where F = 0 inside [-pi, pi]."""
12    # omega* = 0  forces  sin(theta*) = 0   ->   theta* in {0, pi, -pi}
13    return [(0.0, 0.0), (np.pi, 0.0), (-np.pi, 0.0)]
14
15def jacobian(state, b, eps=1e-6):
16    """Numerical Jacobian by central differences."""
17    n = len(state)
18    J = np.zeros((n, n))
19    for j in range(n):
20        e = np.zeros(n); e[j] = eps
21        J[:, j] = (F(state + e, b) - F(state - e, b)) / (2 * eps)
22    return J
23
24def classify(J):
25    tr  = np.trace(J)
26    det = np.linalg.det(J)
27    disc = tr**2 - 4*det
28    eigs = np.linalg.eigvals(J)
29    if disc > 0:
30        if det < 0:        name = "saddle"
31        elif tr < 0:       name = "stable node"
32        else:              name = "unstable node"
33    elif disc < 0:
34        if abs(tr) < 1e-9: name = "center (linearization fails)"
35        elif tr < 0:       name = "stable spiral"
36        else:              name = "unstable spiral"
37    else:                  name = "degenerate"
38    return name, eigs
39
40b = 0.3
41for fp in find_fixed_points():
42    J = jacobian(np.array(fp), b)
43    name, eigs = classify(J)
44    print(f"fp = ({fp[0]:+.4f}, {fp[1]:+.4f})  ->  {name}")
45    print(f"   J = {J.tolist()}")
46    print(f"   eigenvalues = {np.array2string(eigs, precision=4)}")

Expected output:

fp = (+0.0000, +0.0000)  ->  stable spiral
   J = [[0.0, 1.0], [-1.0, -0.3]]
   eigenvalues = [-0.15+0.9887j -0.15-0.9887j]
fp = (+3.1416, +0.0000)  ->  saddle
   J = [[0.0, 1.0], [1.0, -0.3]]
   eigenvalues = [ 0.8612 -1.1612]
fp = (-3.1416, +0.0000)  ->  saddle
   J = [[0.0, 1.0], [1.0, -0.3]]
   eigenvalues = [ 0.8612 -1.1612]

PyTorch: Autograd Jacobian and Stability

Finite differences are great for sanity-checking, but they introduce truncation error and require N+1 function evaluations per Jacobian column. PyTorch's autograd\text{autograd} gives the exact Jacobian with a single backward pass — and, crucially, it keeps every quantity inside a differentiable graph. That means we can later optimize stability itself: pick the damping bb that puts the pendulum's eigenvalues exactly where we want them.

Autograd Jacobian → eigenvalues → classification
🐍pendulum_classify_torch.py
1Import PyTorch

PyTorch's killer feature here is torch.autograd.functional.jacobian: it computes the EXACT Jacobian by automatic differentiation — no finite-difference epsilon, no truncation error. The same machinery used to train neural networks now hands us the linearization matrix of any dynamical system.

4F(state, b) — vector field that's batch-friendly

Same pendulum F as before, but written with state[..., 0] and state[..., 1] so it broadcasts over arbitrary leading batch dimensions. Passing a single state of shape (2,) gives a single derivative of shape (2,); passing a batch of N states of shape (N, 2) gives derivatives of shape (N, 2). One function, zero loops.

8jacobian_autograd — header

Wraps PyTorch's autograd Jacobian. We pass a lambda that closes over b so the argument signature matches what torch expects: a function of ONE tensor input. The returned tensor has shape (output_dim, input_dim) = (2, 2) for a single state.

10torch.autograd.functional.jacobian — the autograd magic

Internally PyTorch records the computational graph for F(s), then evaluates ∂Fᵢ/∂sⱼ for every (i, j) pair using reverse-mode autodiff. No finite differences — the result is exact to floating-point precision. For the pendulum at the origin this returns tensor([[0., 1.], [-1., -0.3]]), exactly matching the analytical Jacobian.

12classify(J) — header

Same classification logic as the NumPy version, but expressed in PyTorch primitives so we can keep the entire pipeline (vector field → Jacobian → eigenvalues → stability label) inside the autograd graph. That matters when we later want gradients of the eigenvalues with respect to parameters.

13torch.linalg.eigvals(J)

Eigenvalues come back as complex tensors. .real and .imag pull out the components. PyTorch's eigvals supports batched input, so we could classify many fixed points in parallel by stacking their Jacobians into one (N, 2, 2) tensor and calling eigvals once — a useful trick when scanning over a parameter sweep.

16Stable-spiral / node branch

Both eigenvalues have negative real part ⇒ every direction decays. If any imaginary part is non-negligible, it's a SPIRAL (rotation + decay). If imaginary parts vanish, it's a NODE (pure decay). The 1e−9 threshold protects against floating-point noise — torch sometimes returns a tiny non-zero imaginary part for real eigenvalues.

19Unstable-spiral / node branch

Mirror image: both real parts positive ⇒ every direction grows. Real eigenvalues give an unstable node, complex give an unstable spiral.

22Saddle branch

Real parts have OPPOSITE signs (their product is negative). One direction grows, the other decays — the hallmark hyperbolic geometry. The .item() pulls the boolean out of the 0-d tensor so the Python 'if' can use it.

24Center / degenerate fall-through

Anything else — pure imaginary eigenvalues (center), repeated real eigenvalues, or zero eigenvalues — is exactly the case where linearization can NOT promise topological equivalence with the nonlinear flow. We label it 'center / degenerate' so the user knows to investigate further (Lyapunov functions, normal forms, …).

26Build the test fixed points

Two tensor states: origin and (π, 0). torch.pi is a Python float; wrapping it in torch.tensor and then .item() makes the code explicit about the type conversion. For a real workflow you'd discover fixed points by stacking several initial guesses and running a vectorized Newton solver — see the predator-prey notebook in section 23.08.

33Loop and print

For each fixed point we get J (a 2×2 tensor), the eigenvalues (a complex 1-D tensor), and the classification string. Expected output matches Python exactly: origin → stable spiral with eigenvalues −0.15 ± 0.989 i, (π, 0) → saddle with eigenvalues +0.861 and −1.161. Cross-checking two implementations is the cheapest insurance against algebra bugs.

26 lines without explanation
1import torch
2
3# Damped pendulum vector field, batched over states.
4def F(state, b=0.3):
5    theta, omega = state[..., 0], state[..., 1]
6    return torch.stack([omega, -torch.sin(theta) - b * omega], dim=-1)
7
8def jacobian_autograd(state, b=0.3):
9    """Exact Jacobian via torch.autograd.functional.jacobian — no eps, no error."""
10    return torch.autograd.functional.jacobian(lambda s: F(s, b), state)
11
12def classify(J):
13    eigs = torch.linalg.eigvals(J)
14    real = eigs.real
15    imag = eigs.imag
16    if torch.all(real < 0):
17        return ("stable spiral" if torch.any(imag.abs() > 1e-9)
18                else "stable node")
19    if torch.all(real > 0):
20        return ("unstable spiral" if torch.any(imag.abs() > 1e-9)
21                else "unstable node")
22    if (real[0] * real[1] < 0).item():
23        return "saddle"
24    return "center / degenerate"
25
26pi  = torch.tensor(torch.pi)
27fps = [
28    torch.tensor([0.0,       0.0]),  # spiral
29    torch.tensor([pi.item(), 0.0]),  # saddle
30]
31
32for fp in fps:
33    J     = jacobian_autograd(fp, b=0.3)
34    eigs  = torch.linalg.eigvals(J)
35    name  = classify(J)
36    print(f"fp = ({fp[0]:+.4f}, {fp[1]:+.4f})  ->  {name}")
37    print(f"   J =\n{J}")
38    print(f"   eigenvalues = {eigs}")

Expected output:

fp = (+0.0000, +0.0000)  ->  stable spiral
   J =
tensor([[ 0.0000,  1.0000],
        [-1.0000, -0.3000]])
   eigenvalues = tensor([-0.1500+0.9887j, -0.1500-0.9887j])
fp = (+3.1416, +0.0000)  ->  saddle
   J =
tensor([[ 0.0000,  1.0000],
        [ 1.0000, -0.3000]])
   eigenvalues = tensor([ 0.8612+0.j, -1.1612+0.j])

Why bother with PyTorch when NumPy already works?

For this 2-D toy example, NumPy is just as fast and clearer. PyTorch earns its keep the moment you want to differentiate through the stability analysis — for example, tune the damping bb so that the dominant eigenvalue has a target real part: b=argminb(Reλmax(J(b))+0.5)2b^\ast = \arg\min_b\,\bigl(\mathrm{Re}\,\lambda_{\max}(J(b)) + 0.5\bigr)^2. With autograd, that's a 5-line gradient-descent loop because λ/b\partial\lambda/\partial b flows for free. The same trick underlies modern data-driven control, neural-ODE stabilization, and physics-informed learning.


When Linearization Fails — The Center Case

Hartman–Grobman only protects hyperbolic fixed points (every eigenvalue has nonzero real part). The undamped pendulum b=0b = 0 at the origin has Jacobian

J(0,0)=[0110]J(0,0) = \begin{bmatrix}0 & 1\\ -1 & 0\end{bmatrix}

with eigenvalues λ=±i\lambda = \pm i — pure imaginary. Linearization predicts a center (perfect circular orbits around the origin). What does the true nonlinear pendulum do? It conserves the energy E=12ω2+(1cosθ)E = \tfrac{1}{2}\omega^2 + (1-\cos\theta), and the level curves of EE are not circles — they bulge as θ\theta approaches ±π\pm\pi and connect through the two saddle points to form the famous pendulum separatrix. So the linearization is qualitatively right (closed orbits) but quantitatively wrong (shape), and it cannot say anything at all about the global heteroclinic loop.

Switch on a tiny b>0b > 0 and the eigenvalues instantly pick up a small negative real part — the center becomes a very-slowly decaying spiral. Hartman–Grobman is restored, but the warning stands: at the boundary, higher-order terms decide. Tools you meet later — Lyapunov functions, normal forms, center-manifold theory — exist precisely to plug this gap.

Three borderline cases to remember

  1. Reλ=0\mathrm{Re}\,\lambda = 0 (center): higher-order terms decide. Could be stable, unstable, or a tiny limit cycle.
  2. λ=0\lambda = 0 (zero eigenvalue): a whole line of equilibria, not an isolated point. Classification table doesn't apply.
  3. Repeated eigenvalue λ1=λ2\lambda_1 = \lambda_2 with only one eigenvector: degenerate node / improper node. Use generalized eigenvectors.

Where Linearization Shows Up

FieldSystemLinearization tells you…
MechanicsLagrange L4/L5 pointsTrojan asteroids are stable spirals (in the rotating frame).
EcologyLotka–Volterra near coexistenceCenter predicts the famous predator–prey cycle.
NeuroscienceFitzHugh–Nagumo rest stateHopf bifurcation: spiral changes stability ⇒ tonic firing onset.
ChemistryBrusselator near steady stateSaddle-node bifurcation gives bistability of chemical states.
ControlQuadcopter hoverLinearize → LQR controller designed in 5 minutes vs months for the full model.
Machine learningGradient flow near a minimumJacobian = Hessian of loss; spectrum predicts convergence rate.
ClimateStommel two-box ocean modelSaddle node ⇒ abrupt thermohaline-circulation shutdown.

Every entry is the same three steps: write F\mathbf{F}, find x\mathbf{x}^\ast, eigenvalue-classify J(x)J(\mathbf{x}^\ast). The same 2×2 characteristic polynomial decides whether a quadcopter hovers, a forest persists, or an ice age begins.


Common Pitfalls

Confusing 'fixed point' with 'equilibrium of the homogeneous problem'

For a non-autonomous system x˙=F(x,t)\dot{\mathbf{x}} = \mathbf{F}(\mathbf{x},\,t) the time variable wrecks the “set F to zero” trick. Linearization in that setting requires a known particular solution to expand around, then a time-varying Jacobian. Don't pretend an explicitly time-dependent system has fixed points just because the autonomous version did.

Forgetting to translate to the deviation u = x − x*

The linearization u=Ju\mathbf{u}' = J\mathbf{u} is in deviation coordinates centered on x\mathbf{x}^\ast. A common bug is to plot the linear flow on the ORIGINAL (θ,ω)(\theta,\omega) plane as if the fixed point were at the origin — getting a picture that's correct in shape but offset. Always shift back: x(t)x+u(t)\mathbf{x}(t) \approx \mathbf{x}^\ast + \mathbf{u}(t).

Trusting linearization at a center

Pure imaginary eigenvalues mean the linear system has closed orbits. The nonlinear system might have a stable spiral, an unstable spiral, OR closed orbits — only the higher-order terms know. Reach for Lyapunov functions, energy methods, or numerical simulation, not Hartman–Grobman.

Mixing up trace, determinant, discriminant signs

Memorize the two critical signs: Δ<0    saddle\Delta < 0 \;\Rightarrow\; \text{saddle} (irrespective of τ\tau), and τ24Δ<0    rotation\tau^2 - 4\Delta < 0 \;\Rightarrow\; \text{rotation} (spiral or center). The rest is just whether τ\tau is positive or negative.


Summary

ConceptFormulaMeaning
Fixed pointF(x*) = 0Point where the flow is zero. Everything starts here.
JacobianJ_{ij} = ∂F_i / ∂x_jBest linear approximation of F near a point.
Linearized systemu' = J(x*) u with u = x - x*What the dynamics look like in a microscope at x*.
Hartman–GrobmanRe λ ≠ 0 for every eigenvalueNonlinear ≈ linear (topologically) near a hyperbolic fixed point.
τ, Δ, τ²−4Δtrace, det, discriminantThree numbers classify every 2-D linear fixed point.
Δ < 0Saddle (always unstable, always one-in-one-out).
Δ > 0, τ²−4Δ < 0complex eigenvaluesSpiral if τ ≠ 0; center if τ = 0.
Δ > 0, τ²−4Δ > 0real eigenvaluesNode, stable if τ < 0, unstable if τ > 0.
Failure caseRe λ = 0 for some eigenvalueHigher-order terms decide — Hartman–Grobman silent.

The one-line takeaway

Solve F(x)=0\mathbf{F}(\mathbf{x}^\ast) = \mathbf{0} to find every fixed point; compute J(x)J(\mathbf{x}^\ast); read the topological type off the trace, determinant, and discriminant of the resulting 2×2 matrix. Do this at every equilibrium and you have mapped the local geometry of an arbitrary nonlinear system — from a pendulum, to a predator–prey ecosystem, to the rest state of a neuron — using nothing more than two partial derivatives and one quadratic equation.

Loading comments...