Chapter 22
22 min read
Section 186 of 353

Complex Roots and Oscillations

Second-Order Differential Equations

Learning Objectives

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

  1. Recognize when a homogeneous linear ODE has complex conjugate roots just by checking the sign of Δ=b24ac\Delta = b^2 - 4ac.
  2. Derive the real-valued general solution y(t)=eαt(C1cos(βt)+C2sin(βt))y(t) = e^{\alpha t}\bigl(C_1\cos(\beta t) + C_2\sin(\beta t)\bigr) from the complex form using Euler's formula.
  3. Interpret the real part α\alpha as decay/growth rate and the imaginary part β\beta as angular frequency.
  4. Rewrite the solution in amplitude-phase form y(t)=Aeαtcos(βtφ)y(t) = A\,e^{\alpha t}\cos(\beta t - \varphi) and read off period, half-life, and phase.
  5. Translate between coefficient form (a,b,c)(a,b,c) and physical form (ωn,ζ)(\omega_n,\zeta), then classify damping regimes.
  6. Implement a complex-root solver in plain Python and cross-check it against PyTorch's matrix_exp\mathrm{matrix\_exp}.

Where We Pick Up: A New Kind of Root

In section 22.1 we tried y=erty = e^{rt} in ay+by+cy=0ay'' + by' + cy = 0 and ended up with the characteristic equation:

ar2+br+c=0a r^2 + b r + c = 0

The quadratic formula gave us three branches depending on Δ=b24ac\Delta = b^2 - 4ac. We handled the two easy cases — distinct real roots and the repeated real root. This section is the third and richest one:

Δ<0r=α±iβ, β>0.\Delta < 0 \quad\Longrightarrow\quad r = \alpha \pm i\beta,\ \beta > 0.
The promise: negative discriminants do not break the method — they turn the boring exponential growth/decay into something far more interesting: oscillation. Every spring, every pendulum, every RLC circuit, every vibrating molecule lives here.

Two questions are about to dominate the whole section:

  • We started in R\mathbb{R}. How did i=1i = \sqrt{-1} sneak in, and what do we do with it?
  • Why does an imaginary root manifest as a real oscillation? What is the bridge?

The bridge is Euler's formula. Once it is in place, the rest of the section is just careful bookkeeping.


The Imaginary Wall

Take the cleanest possible example: y+y=0y'' + y = 0. The characteristic equation is r2+1=0r^2 + 1 = 0, giving r=±ir = \pm i.

Naively plug into the "general solution" from section 22.1:

y(t)=C1eit+C2eit.y(t) = C_1 e^{i t} + C_2 e^{-i t}.

This looks complex-valued. But we are modelling a real physical thing — a frictionless mass on a spring, or an LC circuit. Voltages and positions are real numbers. We need a way to express eite^{it} in real terms.

The discomfort is the point

The complex form is not wrong. It is simply a coordinate system that is awkward for a real-valued physical observable. The job of this section is to translate the complex coordinates into a familiar real basis — sines and cosines — without losing any information.


Euler's Bridge — From eiβte^{i\beta t} to Sines and Cosines

The single most important identity in this whole chapter is Euler's formula:

eiθ=cosθ+isinθ.e^{i\theta} = \cos\theta + i\sin\theta.

Why does this hold?

Expand the Taylor series. The series for eiθe^{i\theta} can be regrouped into the series for cosθ\cos\theta and isinθi\sin\theta:

eiθ=n=0(iθ)nn!=1θ22!+θ44!cosθ+i(θθ33!+θ55!)sinθe^{i\theta} = \sum_{n=0}^{\infty} \frac{(i\theta)^n}{n!} = \underbrace{1 - \frac{\theta^2}{2!} + \frac{\theta^4}{4!} - \cdots}_{\cos\theta} + i\underbrace{\Bigl(\theta - \frac{\theta^3}{3!} + \frac{\theta^5}{5!} - \cdots\Bigr)}_{\sin\theta}

The trick is that i2=1i^2 = -1, so even powers of ii alternate sign — exactly matching the alternating signs in the cosine and sine series.

Apply it to θ=βt\theta = \beta t:

eiβt=cos(βt)+isin(βt),eiβt=cos(βt)isin(βt).e^{i\beta t} = \cos(\beta t) + i\sin(\beta t),\qquad e^{-i\beta t} = \cos(\beta t) - i\sin(\beta t).

Now stitch the decaying envelope back in. For a root r=α+iβr = \alpha + i\beta:

ert=e(α+iβ)t=eαt(cos(βt)+isin(βt)).e^{rt} = e^{(\alpha + i\beta)t} = e^{\alpha t}\bigl(\cos(\beta t) + i\sin(\beta t)\bigr).

The exponential erte^{rt} has two parts: a real exponential eαte^{\alpha t} that decays (α<0\alpha < 0) or grows (α>0\alpha > 0), and a complex rotation eiβte^{i\beta t} that spins around the unit circle at angular speed β\beta. Multiplied together they trace out a spiral in the complex plane.


The Real-Valued General Solution

Our complex pair is r1=α+iβr_1 = \alpha + i\beta, r2=αiβr_2 = \alpha - i\beta. By linearity,

y(t)=K1e(α+iβ)t+K2e(αiβ)ty(t) = K_1 e^{(\alpha + i\beta)t} + K_2 e^{(\alpha - i\beta)t}

solves the ODE for any complex K1,K2K_1, K_2. To make y(t)y(t) real for all tt, we choose K2=K1K_2 = \overline{K_1} (complex conjugate). With K1=12(C1iC2)K_1 = \tfrac{1}{2}(C_1 - iC_2) and K2=12(C1+iC2)K_2 = \tfrac{1}{2}(C_1 + iC_2), the imaginary parts cancel and you are left with this clean real form:

General solution — complex-roots case

y(t)=eαt(C1cos(βt)+C2sin(βt))\boxed{\,y(t) = e^{\alpha t}\bigl(C_1\cos(\beta t) + C_2\sin(\beta t)\bigr)\,}

where α=b2a\alpha = -\tfrac{b}{2a}, β=Δ2a\beta = \tfrac{\sqrt{-\Delta}}{2a}, and C1,C2RC_1, C_2 \in \mathbb{R} are determined by the initial conditions.

Reading the formula

The eαte^{\alpha t} in front is the envelope. Whatever happens inside the parentheses, its amplitude is bounded by AeαtA\,e^{\alpha t}. The inside is a weighted sum of a cosine and a sine, both with the SAME angular frequency β\beta.

Why two real constants, not four?

A second-order ODE has a two-dimensional solution space. cos(βt)\cos(\beta t) and sin(βt)\sin(\beta t) are linearly independent and span that space (after multiplying by the envelope). Two constants C1,C2C_1, C_2 are exactly enough to match the two initial conditions y(0)y(0) and y(0)y'(0).

Applying the initial conditions

At t=0t = 0:

  • y(0)=e0(C11+C20)=C1y(0) = e^{0}(C_1\cdot 1 + C_2\cdot 0) = C_1, so C1=y(0)C_1 = y(0).
  • y(t)=eαt[(αC1+βC2)cos(βt)+(αC2βC1)sin(βt)]y'(t) = e^{\alpha t}\bigl[(\alpha C_1 + \beta C_2)\cos(\beta t) + (\alpha C_2 - \beta C_1)\sin(\beta t)\bigr], so y(0)=αC1+βC2y'(0) = \alpha C_1 + \beta C_2.

Solving for C2C_2:

C1=y(0),C2=y(0)αy(0)β.C_1 = y(0),\qquad C_2 = \frac{y'(0) - \alpha\,y(0)}{\beta}.

The denominator β\beta is non-zero exactly because we are in the complex-roots branch.


Anatomy of α\alpha and β\beta

These two numbers are everything. They live in different directions of the complex plane and they tell different physical stories.

QuantityFormulaPhysical meaningGeometric meaning
α (real part)−b / (2a)Decay rate of the envelope. α < 0 ⇒ amplitude shrinks; α > 0 ⇒ amplitude grows.Horizontal position of the root in the complex plane. Left half-plane = stable.
β (imag part)√(−Δ) / (2a)Angular frequency in rad/s. Period T = 2π/β, frequency f = β/(2π).Distance of the root from the real axis. Far from the axis = fast wiggle.
α² + β²c / aDistance² of the root from the origin (in the complex plane).For a spring this equals ωₙ², the natural frequency squared.
−b / aSum of the two roots (Vieta's formula).Twice the real part — used as the trace of the system matrix.

A two-knob mental model

Imagine you are designing an instrument. The knob α\alpha controls how quickly the note fades; the knob β\beta controls the pitch. Set α=0\alpha = 0 and the note rings forever. Make α\alpha more negative and the note dies sooner. Change β\beta and the same envelope holds a different musical pitch.


Amplitude–Phase Form: One Tilted Cosine

The form C1cos(βt)+C2sin(βt)C_1\cos(\beta t) + C_2\sin(\beta t) is algebraically convenient but visually awkward — it's a SUM of two wiggles. There is a beautiful alternative.

Use the identity C1cosθ+C2sinθ=Acos(θφ)C_1\cos\theta + C_2\sin\theta = A\cos(\theta - \varphi) with

A=C12+C22,φ=atan2(C2,C1).A = \sqrt{C_1^2 + C_2^2},\qquad \varphi = \mathrm{atan2}(C_2,\, C_1).

Then the whole solution collapses to a single damped cosine:

y(t)=Aeαtcos(βtφ)\boxed{\,y(t) = A\,e^{\alpha t}\cos(\beta t - \varphi)\,}

Reading this off a measured oscilloscope trace is now trivial:

  • Amplitude AA = the initial peak (before any decay).
  • Envelope AeαtA\,e^{\alpha t} = the dashed curve touching every peak.
  • Frequency β\beta = how fast it wiggles (rad/s).
  • Phase φ\varphi = how far the first peak is shifted from t=0t = 0.

Why atan2, not atan?

atan2(y,x)\mathrm{atan2}(y, x) uses the signs of both arguments to return an angle in (π,π](-\pi, \pi], distinguishing all four quadrants. Plain arctan(C2/C1)\arctan(C_2/C_1) would collapse the second and fourth quadrants together (since dividing two negatives kills the sign info), giving you the wrong phase half the time.


Worked Example (Step-by-Step)

Take this concrete IVP:

y+2y+5y=0,y(0)=1,y(0)=0.y'' + 2y' + 5y = 0,\qquad y(0) = 1,\quad y'(0) = 0.

We will derive every number on paper, then verify with the explorer and the Python solver below.

▸ Show full hand-derivation (recommended on first read)

Step 1 — Characteristic equation

Substitute y=erty = e^{rt}:

r2+2r+5=0.r^2 + 2r + 5 = 0.

Step 2 — Discriminant

Δ=224(1)(5)=420=16<0.\Delta = 2^2 - 4(1)(5) = 4 - 20 = -16 < 0. So we are in the complex-roots branch.

Step 3 — Roots

r=2±162=2±4i2=1±2i.r = \dfrac{-2 \pm \sqrt{-16}}{2} = \dfrac{-2 \pm 4i}{2} = -1 \pm 2i.

So α=1\alpha = -1 and β=2\beta = 2.

Step 4 — General solution

y(t)=et(C1cos2t+C2sin2t).y(t) = e^{-t}\bigl(C_1\cos 2t + C_2\sin 2t\bigr).

Step 5 — Apply y(0)=1y(0) = 1

Plug t=0t = 0: y(0)=1(C11+C20)=C1y(0) = 1\cdot(C_1\cdot 1 + C_2\cdot 0) = C_1. So C1=1C_1 = 1.

Step 6 — Apply y(0)=0y'(0) = 0

From C2=(y(0)αy(0))/βC_2 = (y'(0) - \alpha\,y(0))/\beta:

C2=0(1)(1)2=12.C_2 = \dfrac{0 - (-1)(1)}{2} = \dfrac{1}{2}.

Step 7 — Final closed form

y(t)=et(cos2t+12sin2t).y(t) = e^{-t}\bigl(\cos 2t + \tfrac{1}{2}\sin 2t\bigr).

Step 8 — Amplitude-phase form

A=12+(1/2)2=5/21.1180.A = \sqrt{1^2 + (1/2)^2} = \sqrt{5}/2 \approx 1.1180.
φ=atan2(1/2,1)0.4636rad26.57.\varphi = \mathrm{atan2}(1/2,\, 1) \approx 0.4636\,\text{rad} \approx 26.57^\circ.

y(t)=52etcos(2t0.4636).y(t) = \tfrac{\sqrt{5}}{2}\,e^{-t}\cos(2t - 0.4636).

Step 9 — Spot-check at four times

ty(t) by handenvelope ±A·e^(αt)
01.000000±1.118034
π/4 ≈ 0.7854e^(−π/4)·(0 + 0.5) ≈ 0.2280±0.5098
π/2 ≈ 1.5708e^(−π/2)·(−1 + 0) ≈ −0.2079±0.2324
π ≈ 3.1416e^(−π)·(1 + 0) ≈ 0.04321±0.04832

Every value of y(t)y(t) lies inside its envelope, as it must.

Step 10 — Read off the physics

  • Period: T=2π/β=π3.1416sT = 2\pi/\beta = \pi \approx 3.1416\,\text{s}.
  • Half-life of the envelope: t1/2=ln2/α=ln20.693st_{1/2} = \ln 2 / |\alpha| = \ln 2 \approx 0.693\,\text{s}.
  • How many oscillations before the envelope falls to 1% of its initial value? t=ln1004.605t = \ln 100 \approx 4.605; in that time the system completes 4.605/π1.474.605/\pi \approx 1.47 cycles. The response dies out fast.

Interactive: Roots in the Complex Plane

Drag the upper red root anywhere in the complex plane and watch what happens to the time-domain plot and the phase-plane spiral. The envelope ±Aeαt\pm A\,e^{\alpha t} is drawn in dashed amber so you can see exactly which curve the oscillation lives inside.

Loading complex-roots explorer…

Three experiments worth doing

  1. Pull the root all the way to the imaginary axis (α=0\alpha = 0). The envelope flattens to constant ±A, the phase-plane orbit closes into an ellipse. This is the undamped regime — perpetual motion.
  2. Cross into the right half-plane (α>0\alpha > 0). The envelope flips to a growing exponential, the spiral flies outward. This is an unstable system — think inverted pendulum, or a poorly tuned PID loop that oscillates more violently with every cycle.
  3. Increase β\beta with α\alpha fixed. The envelope is unchanged but the wiggles get faster. The spiral makes more turns per inward step — high-frequency, low-damping.

Damping in Disguise: ζ\zeta and ωn\omega_n

Engineers rarely speak in terms of (a,b,c)(a, b, c). They use two physically meaningful parameters: the natural frequency ωn\omega_n and the damping ratio ζ\zeta. They rewrite the ODE in the canonical form

y+2ζωny+ωn2y=0,y'' + 2\zeta\,\omega_n\,y' + \omega_n^2\,y = 0,

which matches ay+by+cy=0ay'' + by' + cy = 0 with ωn=c/a\omega_n = \sqrt{c/a} and ζ=b/(2ac)\zeta = b / (2\sqrt{ac}).

Computing the discriminant in this notation gives Δ=4ωn2(ζ21)\Delta = 4\omega_n^2(\zeta^2 - 1). So the three cases reorganise themselves into a single dimensionless story:

ζ valueΔ signRegimeα (decay rate)β (oscillation freq)
ζ = 0< 0Undamped0ωₙ
0 < ζ < 1< 0Underdamped (complex roots)−ζ·ωₙωₙ·√(1 − ζ²) ≡ ωd
ζ = 1= 0Critically damped−ωₙ0 (no oscillation)
ζ > 1> 0Overdampedtwo real roots0 (no oscillation)

The damped frequency ωd

When you measure a real oscillation you see ωd=ωn1ζ2\omega_d = \omega_n\sqrt{1 - \zeta^2}, NOT the natural frequency ωn\omega_n. Damping slows the wiggle. For ζ=0.1\zeta = 0.1 the difference is only 0.5%; for ζ=0.5\zeta = 0.5 it's about 13%; at ζ=1\zeta = 1 the wiggle disappears entirely.

Engineer's rule of thumb

ζ0.7\zeta \approx 0.7 is the industry-favourite damping ratio for control systems. It gives a fast response (small ζ\zeta means snappy) with only a single tiny overshoot (large ζ\zeta means well-behaved). The trade-off between speed and overshoot is exactly what ζ\zeta parameterises.


Interactive: Four Regimes from One Spring

Same mass-spring system, four different dampers. The four ghost traces stay visible so you can compare them against the active curve. Move ζ\zeta continuously through 0, 1, and above 1 and watch the regime change in real time.

Loading damping comparator…

Python: Closed-Form Solver from Scratch

We will build a single function that takes (a,b,c,y0,y0,t)(a, b, c, y_0, y'_0, t) and returns y(t)y(t) along with the physical diagnostics — α,β,A,φ\alpha, \beta, A, \varphi, period, half-life. Plain NumPy, no special-case libraries.

closed-form complex-roots solver
🐍python
1Import NumPy

NumPy lets us evaluate exp, cos, sin, sqrt on a whole time grid in one expression. Every formula below is written so it works on a single scalar t OR a NumPy array of times.

4Function signature

We accept the three coefficients a, b, c, the two initial conditions y0 = y(0) and yp0 = y'(0), and a NumPy array t of time points where we want the solution. Two initial conditions are required because a second-order ODE has two free constants C1, C2 in its general solution.

11Compute the discriminant

Δ = b² − 4ac is exactly the same discriminant you saw in section 22.1. Its sign decides which solution family applies. Here we assert Δ < 0 so we only use the complex-roots branch.

15Real part α of the root

α = −b / (2a) is the common real part of the conjugate pair r = α ± iβ. Physically α is the EXPONENTIAL DECAY RATE of the oscillation's envelope. Negative α ⇒ amplitude shrinks; positive α ⇒ amplitude grows.

16Imaginary part β of the root

β = √(−Δ) / (2a) is the magnitude of the imaginary part. β is the ANGULAR FREQUENCY of the oscillation in radians per second. We take √(−Δ) because Δ is negative in this branch, so −Δ is positive.

23Apply initial conditions — pin C1

Plug t=0 into y(t) = e^(αt)(C₁cos(βt) + C₂sin(βt)). At t=0, e^0=1, cos(0)=1, sin(0)=0, so y(0) = C₁ directly. No algebra needed.

24Apply initial conditions — pin C2

Differentiate y and plug in t=0. You get y'(0) = αC₁ + βC₂. Solving for C₂ gives (y'(0) − αy(0))/β. The β in the denominator is non-zero because we are in the complex-roots branch (β > 0).

27Build the real solution

y(t) = e^(αt)(C₁cos(βt) + C₂sin(βt)). NumPy evaluates this whole expression elementwise on the time grid. The result is a 1-D array of length len(t). The e^(αt) factor is the envelope; the cos+sin combination is the oscillation inside it.

30Compute the amplitude A

A = √(C₁² + C₂²) is the radius of the cosine–sine combination. It is the peak height of the dimensionless oscillation (before the envelope is multiplied in). This single number summarizes the initial 'energy' of the response.

31Compute the phase φ

φ = atan2(C₂, C₁) shifts the cosine. With A and φ the solution can be rewritten as y(t) = A·e^(αt)·cos(βt − φ), which is a SINGLE damped cosine. atan2 (not atan) is used to land in the correct quadrant regardless of signs.

35Return diagnostics

We return α, β, the constants, the amplitude, the phase, the period 2π/β, and the half-life ln(2)/(−α) (for a stable decay). These quantities are what an engineer actually reads off a measured time trace.

44Run the worked example

We call the solver with a=1, b=2, c=5, y(0)=1, y'(0)=0. The characteristic equation is r² + 2r + 5 = 0, giving r = −1 ± 2i. So α = −1 (decay), β = 2 (angular frequency). Plugging the initial conditions gives C₁ = 1, C₂ = 0.5, and A = √1.25 ≈ 1.118.

50Print a sanity check

We print y at t = π/4. By hand: e^(−π/4)(cos(π/2) + 0.5·sin(π/2)) = 0.4559 · (0 + 0.5) ≈ 0.2280. NumPy returns 0.2280 — confirming the code matches the derivation.

38 lines without explanation
1import numpy as np
2import matplotlib.pyplot as plt
3
4def solve_complex_roots(a, b, c, y0, yp0, t):
5    """
6    Solve  a*y'' + b*y' + c*y = 0  in the COMPLEX-ROOTS case
7    (i.e. when the discriminant b^2 - 4ac is negative).
8
9    Returns y(t) and a dict of the physical quantities.
10    """
11    disc = b**2 - 4*a*c
12    assert disc < 0, "complex roots only — got disc >= 0"
13
14    # Roots:  r = alpha ± i beta
15    alpha = -b / (2*a)
16    beta  = np.sqrt(-disc) / (2*a)
17
18    # Apply initial conditions  y(0)=y0,  y'(0)=yp0
19    #   y(0)  = C1                 -> C1 = y0
20    #   y'(0) = alpha*C1 + beta*C2 -> C2 = (yp0 - alpha*y0)/beta
21    C1 = y0
22    C2 = (yp0 - alpha*y0) / beta
23
24    # Real-valued solution
25    y = np.exp(alpha*t) * (C1*np.cos(beta*t) + C2*np.sin(beta*t))
26
27    # Amplitude-phase form  y(t) = A * e^(alpha t) * cos(beta t - phi)
28    A   = np.sqrt(C1**2 + C2**2)
29    phi = np.arctan2(C2, C1)
30
31    return y, {
32        "alpha": alpha,         # decay rate  (real part of the root)
33        "beta":  beta,          # angular freq (imag part of the root)
34        "C1": C1, "C2": C2,
35        "amplitude": A,
36        "phase": phi,
37        "period": 2*np.pi / beta,
38        "half_life": np.log(2)/-alpha if alpha < 0 else float("inf"),
39    }
40
41
42# ---- worked example: y'' + 2 y' + 5 y = 0,  y(0)=1,  y'(0)=0 ----
43t = np.linspace(0, 6, 400)
44y, info = solve_complex_roots(a=1, b=2, c=5, y0=1.0, yp0=0.0, t=t)
45
46print("alpha    =", info["alpha"])
47print("beta     =", info["beta"])
48print("C1, C2   =", info["C1"], info["C2"])
49print("A, phi   =", info["amplitude"], info["phase"])
50print("period   =", info["period"])
51print("y(pi/4)  =", y[int(400*(np.pi/4)/6)])

Why hand-derive when scipy.integrate.solve_ivp exists?

Closed-form solutions let you reason about a system before you simulate it. You can read off the period, the half-life, the steady-state behaviour, all without running a single step. A numerical solver tells you what happens; the closed form tells you why.


PyTorch: The Matrix-Exponential Picture

There is one routine that solves ALL three cases (real distinct, repeated, complex) without any branching on the discriminant: the matrix exponential. Convert the 2nd-order ODE into a first-order linear system Y=AYY' = A\,Y with Y=(y,y)TY = (y, y')^T, and the solution is just Y(t)=eAtY(0)Y(t) = e^{At}\,Y(0).

The eigenvalues of AA are exactly the roots of the characteristic equation. For our worked example PyTorch will print (1+2i, 12i)(-1 + 2i,\ -1 - 2i) — the same pair we derived by hand.

matrix-exponential solver via torch.linalg.matrix_exp
🐍python
1Import PyTorch

We use torch.linalg.matrix_exp, the matrix exponential. For a linear constant-coefficient ODE Y' = A·Y, the solution is literally Y(t) = e^(At)·Y(0) — the matrix exponential IS the propagator. PyTorch ships this as a single call.

11Build the system matrix A

Top row [0, 1] encodes y' = v. Bottom row [-c/a, -b/a] encodes v' = -(c/a)y - (b/a)v. Stacking these turns the 2nd-order equation into a 2-D first-order linear system Y' = A·Y with Y = [y, v].

13Stack the initial conditions

Y(0) = [y(0), y'(0)] — exactly the two initial conditions the 2nd-order ODE requires, now living in a single 2-vector.

16Eigenvalues == characteristic roots

Crucial connection: the eigenvalues of A are exactly the roots r of the characteristic equation a·r² + b·r + c = 0. For our example a=1, b=2, c=5, NumPy/PyTorch prints (-1+2i) and (-1-2i) — the complex conjugate pair we derived by hand. This is why eigenvalue language and root language are interchangeable for linear ODEs.

20Compute the propagator for every t

For each time t_i we form A·t_i (a 2×2 matrix), take its matrix exponential (also 2×2), and multiply by Y(0). The result is a 2-vector [y(t_i), y'(t_i)]. We stack all of them into a (T, 2) tensor.

24Take the y component

Slice column 0 to extract y(t) and drop the velocity. (For phase-plane plots you'd keep both columns.)

30Cross-check against the closed form

We compute y(t) = e^(-t)(cos(2t) + 0.5·sin(2t)) directly and compare. The matrix-exponential method should agree to ~1e-14 in float64 — the two routes give literally the same number. This is the strongest possible sanity check.

34Why this matters

matrix_exp is a single API that solves all three cases (real distinct, repeated, complex) uniformly — no if/else on the discriminant, no special-case formulas. It is also exactly the routine inside torchdiffeq and the linear flow of every Neural ODE. Understanding the 2×2 case scales to N×N coupled oscillators (modal analysis) line-for-line.

31 lines without explanation
1import torch
2
3def solve_via_matrix_exp(a, b, c, y0, yp0, t):
4    """
5    Solve  a*y'' + b*y' + c*y = 0  exactly using the MATRIX exponential.
6
7    Convert to first-order system Y' = A Y with Y = [y, y'].
8    Then  Y(t) = exp(A * t) @ Y(0)  for any t.
9
10    This works for ALL three cases (distinct, repeated, complex) without
11    branching on the discriminant.
12    """
13    A = torch.tensor([[0.0,         1.0],
14                      [-c/a,        -b/a]], dtype=torch.float64)
15    Y0 = torch.tensor([y0, yp0], dtype=torch.float64)
16
17    # eigenvalues of A == roots of the characteristic equation
18    eigvals = torch.linalg.eigvals(A)
19    print("eigenvalues of A =", eigvals)
20
21    # Vectorised: matrix_exp(A * t_i) for every t_i in the grid
22    Y = torch.stack([
23        torch.linalg.matrix_exp(A * ti) @ Y0
24        for ti in t
25    ])
26    return Y[:, 0]   # take only the y component, drop y'
27
28
29# Same worked example: y'' + 2 y' + 5 y = 0
30t = torch.linspace(0.0, 6.0, 400, dtype=torch.float64)
31y_pt = solve_via_matrix_exp(1.0, 2.0, 5.0, 1.0, 0.0, t)
32
33# Hand-derived closed form for cross-check
34alpha, beta = -1.0, 2.0
35C1, C2 = 1.0, 0.5
36y_exact = torch.exp(alpha*t) * (C1*torch.cos(beta*t) + C2*torch.sin(beta*t))
37
38err = torch.max(torch.abs(y_pt - y_exact)).item()
39print("max abs error vs closed-form:", err)

Eigenvalues = roots: the unifying view

The characteristic equation ar2+br+c=0a r^2 + b r + c = 0 is, up to sign, exactly the characteristic polynomial of the 2×2 system matrix A=(01c/ab/a)A = \begin{pmatrix} 0 & 1 \\ -c/a & -b/a \end{pmatrix}. So "find the roots of the characteristic equation" and "find the eigenvalues of AA" are the same computation. This connection scales perfectly: an nn-th order ODE becomes an n×nn \times n first-order system, and the eigenvalues still classify the dynamics.


Where Complex Roots Show Up in the Real World

🪨 Car suspension

A shock absorber over a bump: mx¨+cx˙+kx=0m\ddot{x} + c\dot{x} + kx = 0. Designers pick cc so that ζ0.7\zeta \approx 0.7 — fast settle with only a barely-perceptible overshoot.

⚡ RLC circuit

Lq¨+Rq˙+q/C=0L\ddot{q} + R\dot{q} + q/C = 0. Same equation. RR plays the role of damping; a low-R resonator (e.g. a quartz crystal) has tiny α\alpha and rings for millions of cycles.

🧬 Molecular vibrations

A diatomic molecule's stretching mode is a damped oscillator whose β\beta tells you the infrared-absorption frequency. Spectroscopy = measuring β\beta.

🤖 Gradient descent with momentum

Heavy-ball / momentum SGD near a quadratic minimum behaves exactly like an underdamped oscillator. Picking the momentum coefficient badly puts you in the underdamped regime — you see the loss oscillate. Pick it as if you were tuning ζ1\zeta \approx 1 for critical damping.

🏙️ Building sway in wind

Skyscrapers have tuned mass dampers (huge pendulums in the penthouse) sized to push the building's ζ\zeta up to a comfortable level. Without them, residents feel underdamped sway for tens of seconds after a gust.

🎛️ Audio synthesizers

A plucked-string sound is a damped oscillator chosen to give a long ringing tail. The decay rate α|\alpha| sets "sustain"; β\beta sets pitch.


Common Pitfalls

1. Confusing β with f (Hz)

β\beta is in radians per second. The ordinary frequency in Hz is f=β/(2π)f = \beta/(2\pi). Period is T=2π/β=1/fT = 2\pi/\beta = 1/f. Mixing these up by a factor of 2π2\pi is the most common numerical bug in textbook problems.

2. Forgetting that β is √(−Δ)/(2a), not √Δ/(2a)

In the complex branch Δ<0\Delta < 0, so Δ\sqrt{\Delta} is not real. The correct imaginary part uses Δ\sqrt{-\Delta}. If you blindly type sqrt(disc) into Python and it returns a NaN, you have just rediscovered this trap.

3. Treating the envelope as the solution

AeαtA\,e^{\alpha t} bounds the solution but is not itself a solution. The actual y(t)y(t) touches the envelope only at the peaks of the cosine. Average behaviour: zero crossings every π/β\pi/\beta.

4. atan vs atan2 for the phase

arctan(C2/C1)\arctan(C_2/C_1) alone can't tell (C1,C2)=(1,1)(C_1, C_2) = (1, 1) from (1,1)(-1, -1) — both give ratio 1. Always use atan2(C2,C1)\mathrm{atan2}(C_2, C_1) when computing phase, exactly as the Python code above does.

5. Numerical issues near ζ = 1

Right at the critical damping boundary the formulas split. Code that branches on Δ<0\Delta < 0 vs Δ=0\Delta = 0 needs a tolerance — pure equality almost never triggers. The matrix-exponential approach sidesteps this completely since it has no branches.


Summary

What to take away

  1. Complex roots happen iff Δ=b24ac<0\Delta = b^2 - 4ac < 0.
  2. The roots are r=α±iβr = \alpha \pm i\beta with α=b/(2a)\alpha = -b/(2a) and β=Δ/(2a)\beta = \sqrt{-\Delta}/(2a).
  3. Euler's formula eiθ=cosθ+isinθe^{i\theta} = \cos\theta + i\sin\theta turns the complex exponential into a sum of sines and cosines.
  4. The real general solution is y(t)=eαt(C1cosβt+C2sinβt)y(t) = e^{\alpha t}\bigl(C_1\cos\beta t + C_2\sin\beta t\bigr), equivalently y(t)=Aeαtcos(βtφ)y(t) = A\,e^{\alpha t}\cos(\beta t - \varphi).
  5. α\alpha is the decay rate of the envelope; β\beta is the angular frequency of the oscillation. Left half-plane = stable; imaginary axis = perpetual motion; right half-plane = unstable growth.
  6. Engineers reparameterise as (ωn,ζ)(\omega_n, \zeta). The damping ratio ζ\zeta classifies all four regimes: undamped (0), underdamped (0–1), critical (1), overdamped (>1).
  7. The same equation is the textbook model for springs, RLC circuits, molecules, momentum-SGD, and skyscrapers in the wind. Master this section and you have mastered every linear oscillator in physics and engineering.
Looking ahead: next section we close the loop with the third case — the repeated root — and explain why the second independent solution suddenly grows a factor of tt. After that we leave the homogeneous world entirely and add a forcing term to the right-hand side.
Loading comments...