Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/mathematics/differential_eq.tex
51 views
unlisted
1
\documentclass[a4paper, 11pt]{report}
2
\usepackage[utf8]{inputenc}
3
\usepackage[T1]{fontenc}
4
\usepackage{amsmath, amssymb}
5
\usepackage{graphicx}
6
\usepackage{siunitx}
7
\usepackage{booktabs}
8
\usepackage{xcolor}
9
\usepackage[makestderr]{pythontex}
10
11
\definecolor{stable}{RGB}{46, 204, 113}
12
\definecolor{unstable}{RGB}{231, 76, 60}
13
\definecolor{saddle}{RGB}{241, 196, 15}
14
\definecolor{limit}{RGB}{155, 89, 182}
15
16
\title{Ordinary Differential Equations:\\
17
Phase Portraits, Stability Analysis, and Limit Cycles}
18
\author{Department of Applied Mathematics\\Technical Report AM-2024-002}
19
\date{\today}
20
21
\begin{document}
22
\maketitle
23
24
\begin{abstract}
25
This report provides a comprehensive computational analysis of ordinary differential equations (ODEs). We examine first and second-order ODEs, construct phase portraits for autonomous systems, perform stability analysis of equilibrium points, and investigate limit cycles in nonlinear oscillators. The van der Pol oscillator is analyzed in detail to demonstrate relaxation oscillations and Hopf bifurcations.
26
\end{abstract}
27
28
\tableofcontents
29
30
\chapter{Introduction}
31
32
Ordinary differential equations describe the evolution of systems in terms of derivatives with respect to a single variable. This report focuses on qualitative analysis through phase portraits and stability theory.
33
34
\section{Classification of ODEs}
35
\begin{itemize}
36
\item \textbf{First-order}: $\frac{dy}{dt} = f(t, y)$
37
\item \textbf{Second-order}: $\frac{d^2y}{dt^2} = f(t, y, \frac{dy}{dt})$
38
\item \textbf{Linear}: Coefficients are functions of $t$ only
39
\item \textbf{Autonomous}: No explicit time dependence
40
\end{itemize}
41
42
\chapter{First-Order ODEs}
43
44
\section{Analytical Solutions}
45
Consider the first-order linear ODE:
46
\begin{equation}
47
\frac{dy}{dt} + p(t)y = q(t)
48
\end{equation}
49
50
Solution via integrating factor $\mu(t) = e^{\int p(t)dt}$:
51
\begin{equation}
52
y(t) = \frac{1}{\mu(t)}\left[\int \mu(t)q(t)dt + C\right]
53
\end{equation}
54
55
\begin{pycode}
56
import numpy as np
57
import matplotlib.pyplot as plt
58
from scipy.integrate import solve_ivp, odeint
59
from scipy.optimize import fsolve
60
plt.rc('text', usetex=True)
61
plt.rc('font', family='serif')
62
63
np.random.seed(42)
64
65
# First-order examples
66
def exponential_decay(t, y, k=1.0):
67
return -k * y
68
69
def logistic_growth(t, y, r=1.0, K=10.0):
70
return r * y * (1 - y/K)
71
72
# Solve multiple first-order ODEs
73
t_span = (0, 10)
74
t_eval = np.linspace(0, 10, 200)
75
76
# Exponential decay with different initial conditions
77
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
78
79
ax = axes[0, 0]
80
for y0 in [1, 2, 3, 4, 5]:
81
sol = solve_ivp(exponential_decay, t_span, [y0], t_eval=t_eval, args=(0.5,))
82
ax.plot(sol.t, sol.y[0], label=f'$y_0 = {y0}$')
83
ax.set_xlabel('Time $t$')
84
ax.set_ylabel('$y(t)$')
85
ax.set_title('Exponential Decay: $dy/dt = -ky$')
86
ax.legend()
87
ax.grid(True, alpha=0.3)
88
89
# Logistic growth
90
ax = axes[0, 1]
91
for y0 in [0.5, 2, 5, 8, 12, 15]:
92
sol = solve_ivp(logistic_growth, t_span, [y0], t_eval=t_eval)
93
ax.plot(sol.t, sol.y[0], label=f'$y_0 = {y0}$')
94
ax.axhline(10, color='red', linestyle='--', alpha=0.5, label='$K = 10$')
95
ax.set_xlabel('Time $t$')
96
ax.set_ylabel('$y(t)$')
97
ax.set_title('Logistic Growth: $dy/dt = ry(1-y/K)$')
98
ax.legend(loc='right')
99
ax.grid(True, alpha=0.3)
100
101
# Direction field for dy/dt = y - y^2
102
ax = axes[1, 0]
103
y_range = np.linspace(-0.5, 1.5, 20)
104
t_range = np.linspace(0, 5, 20)
105
T, Y = np.meshgrid(t_range, y_range)
106
dY = Y - Y**2
107
dT = np.ones_like(dY)
108
norm = np.sqrt(dT**2 + dY**2)
109
ax.quiver(T, Y, dT/norm, dY/norm, alpha=0.5)
110
111
# Plot solutions
112
for y0 in [0.1, 0.5, 0.9, 1.1, 1.5]:
113
sol = solve_ivp(lambda t, y: y - y**2, (0, 5), [y0], t_eval=np.linspace(0, 5, 100))
114
ax.plot(sol.t, sol.y[0], linewidth=2)
115
ax.set_xlabel('Time $t$')
116
ax.set_ylabel('$y(t)$')
117
ax.set_title('Direction Field: $dy/dt = y - y^2$')
118
ax.set_xlim(0, 5)
119
ax.set_ylim(-0.5, 1.5)
120
ax.grid(True, alpha=0.3)
121
122
# Bernoulli equation
123
ax = axes[1, 1]
124
# dy/dt = y - y^3 (bistable)
125
for y0 in np.linspace(-1.5, 1.5, 15):
126
sol = solve_ivp(lambda t, y: y - y**3, (0, 5), [y0], t_eval=np.linspace(0, 5, 100))
127
ax.plot(sol.t, sol.y[0], 'b-', linewidth=0.8, alpha=0.6)
128
ax.axhline(1, color='green', linestyle='--', label='Stable: $y=1$')
129
ax.axhline(-1, color='green', linestyle='--', label='Stable: $y=-1$')
130
ax.axhline(0, color='red', linestyle='--', label='Unstable: $y=0$')
131
ax.set_xlabel('Time $t$')
132
ax.set_ylabel('$y(t)$')
133
ax.set_title('Bistable System: $dy/dt = y - y^3$')
134
ax.legend()
135
ax.grid(True, alpha=0.3)
136
137
plt.tight_layout()
138
plt.savefig('first_order_odes.pdf', dpi=150, bbox_inches='tight')
139
plt.close()
140
\end{pycode}
141
142
\begin{figure}[htbp]
143
\centering
144
\includegraphics[width=0.95\textwidth]{first_order_odes.pdf}
145
\caption{First-order ODE solutions: (a) exponential decay, (b) logistic growth, (c) direction field, (d) bistable system.}
146
\end{figure}
147
148
\chapter{Second-Order Linear ODEs}
149
150
\section{Harmonic Oscillator}
151
The damped harmonic oscillator:
152
\begin{equation}
153
m\frac{d^2x}{dt^2} + c\frac{dx}{dt} + kx = 0
154
\end{equation}
155
156
Characteristic equation: $mr^2 + cr + k = 0$
157
158
\begin{pycode}
159
# Second-order ODE as system of first-order
160
def harmonic_oscillator(t, state, m=1.0, c=0.0, k=1.0):
161
x, v = state
162
return [v, -(c*v + k*x)/m]
163
164
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
165
166
# Undamped
167
t_span = (0, 20)
168
t_eval = np.linspace(0, 20, 500)
169
170
ax = axes[0, 0]
171
sol = solve_ivp(harmonic_oscillator, t_span, [1, 0], t_eval=t_eval, args=(1, 0, 1))
172
ax.plot(sol.t, sol.y[0], 'b-')
173
ax.set_xlabel('Time $t$')
174
ax.set_ylabel('$x(t)$')
175
ax.set_title('Undamped ($\\zeta = 0$)')
176
ax.grid(True, alpha=0.3)
177
178
# Underdamped
179
ax = axes[0, 1]
180
sol = solve_ivp(harmonic_oscillator, t_span, [1, 0], t_eval=t_eval, args=(1, 0.3, 1))
181
ax.plot(sol.t, sol.y[0], 'b-')
182
ax.set_xlabel('Time $t$')
183
ax.set_ylabel('$x(t)$')
184
ax.set_title('Underdamped ($\\zeta = 0.15$)')
185
ax.grid(True, alpha=0.3)
186
187
# Critically damped
188
ax = axes[0, 2]
189
sol = solve_ivp(harmonic_oscillator, t_span, [1, 0], t_eval=t_eval, args=(1, 2, 1))
190
ax.plot(sol.t, sol.y[0], 'b-')
191
ax.set_xlabel('Time $t$')
192
ax.set_ylabel('$x(t)$')
193
ax.set_title('Critically Damped ($\\zeta = 1$)')
194
ax.grid(True, alpha=0.3)
195
196
# Overdamped
197
ax = axes[1, 0]
198
sol = solve_ivp(harmonic_oscillator, t_span, [1, 0], t_eval=t_eval, args=(1, 4, 1))
199
ax.plot(sol.t, sol.y[0], 'b-')
200
ax.set_xlabel('Time $t$')
201
ax.set_ylabel('$x(t)$')
202
ax.set_title('Overdamped ($\\zeta = 2$)')
203
ax.grid(True, alpha=0.3)
204
205
# Phase portraits
206
ax = axes[1, 1]
207
for zeta in [0, 0.15, 0.5, 1.0]:
208
c = 2 * zeta
209
sol = solve_ivp(harmonic_oscillator, (0, 30), [1, 0], t_eval=np.linspace(0, 30, 1000), args=(1, c, 1))
210
ax.plot(sol.y[0], sol.y[1], label=f'$\\zeta = {zeta}$')
211
ax.set_xlabel('$x$')
212
ax.set_ylabel('$\\dot{x}$')
213
ax.set_title('Phase Portraits')
214
ax.legend()
215
ax.grid(True, alpha=0.3)
216
217
# Energy decay
218
ax = axes[1, 2]
219
for zeta in [0.1, 0.3, 0.5]:
220
c = 2 * zeta
221
sol = solve_ivp(harmonic_oscillator, t_span, [1, 0], t_eval=t_eval, args=(1, c, 1))
222
energy = 0.5 * sol.y[0]**2 + 0.5 * sol.y[1]**2
223
ax.plot(sol.t, energy, label=f'$\\zeta = {zeta}$')
224
ax.set_xlabel('Time $t$')
225
ax.set_ylabel('Energy $E$')
226
ax.set_title('Energy Decay')
227
ax.legend()
228
ax.grid(True, alpha=0.3)
229
ax.set_yscale('log')
230
231
plt.tight_layout()
232
plt.savefig('second_order_odes.pdf', dpi=150, bbox_inches='tight')
233
plt.close()
234
\end{pycode}
235
236
\begin{figure}[htbp]
237
\centering
238
\includegraphics[width=0.95\textwidth]{second_order_odes.pdf}
239
\caption{Second-order ODE: damped harmonic oscillator with various damping ratios.}
240
\end{figure}
241
242
\chapter{Phase Portrait Analysis}
243
244
\section{2D Autonomous Systems}
245
Consider the system:
246
\begin{align}
247
\frac{dx}{dt} &= f(x, y) \\
248
\frac{dy}{dt} &= g(x, y)
249
\end{align}
250
251
Equilibrium points satisfy $f(x^*, y^*) = g(x^*, y^*) = 0$.
252
253
\section{Linear Stability Analysis}
254
Near an equilibrium, linearize using the Jacobian:
255
\begin{equation}
256
J = \begin{pmatrix}
257
\frac{\partial f}{\partial x} & \frac{\partial f}{\partial y} \\
258
\frac{\partial g}{\partial x} & \frac{\partial g}{\partial y}
259
\end{pmatrix}
260
\end{equation}
261
262
Eigenvalues $\lambda$ determine stability.
263
264
\begin{pycode}
265
# 2D systems with different equilibrium types
266
def saddle_system(t, state):
267
x, y = state
268
return [x, -y]
269
270
def stable_node(t, state):
271
x, y = state
272
return [-x, -2*y]
273
274
def unstable_spiral(t, state):
275
x, y = state
276
return [0.1*x - y, x + 0.1*y]
277
278
def center(t, state):
279
x, y = state
280
return [-y, x]
281
282
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
283
systems = [
284
(saddle_system, 'Saddle Point'),
285
(stable_node, 'Stable Node'),
286
(unstable_spiral, 'Unstable Spiral'),
287
(center, 'Center')
288
]
289
290
for ax, (system, title) in zip(axes.flatten(), systems):
291
# Vector field
292
x_range = np.linspace(-3, 3, 20)
293
y_range = np.linspace(-3, 3, 20)
294
X, Y = np.meshgrid(x_range, y_range)
295
296
state = np.array([X, Y])
297
derivatives = system(0, state)
298
U, V = derivatives[0], derivatives[1]
299
300
norm = np.sqrt(U**2 + V**2)
301
norm[norm == 0] = 1
302
ax.quiver(X, Y, U/norm, V/norm, alpha=0.4)
303
304
# Trajectories
305
angles = np.linspace(0, 2*np.pi, 8, endpoint=False)
306
for angle in angles:
307
x0 = 2.5 * np.cos(angle)
308
y0 = 2.5 * np.sin(angle)
309
try:
310
sol = solve_ivp(system, (0, 10), [x0, y0], t_eval=np.linspace(0, 10, 500), max_step=0.1)
311
ax.plot(sol.y[0], sol.y[1], 'b-', linewidth=1)
312
except:
313
pass
314
315
ax.plot(0, 0, 'ro', markersize=8)
316
ax.set_xlabel('$x$')
317
ax.set_ylabel('$y$')
318
ax.set_title(title)
319
ax.set_xlim(-3, 3)
320
ax.set_ylim(-3, 3)
321
ax.grid(True, alpha=0.3)
322
323
plt.tight_layout()
324
plt.savefig('phase_portraits.pdf', dpi=150, bbox_inches='tight')
325
plt.close()
326
\end{pycode}
327
328
\begin{figure}[htbp]
329
\centering
330
\includegraphics[width=0.95\textwidth]{phase_portraits.pdf}
331
\caption{Phase portraits showing different equilibrium types based on eigenvalue classification.}
332
\end{figure}
333
334
\chapter{Limit Cycles}
335
336
\section{Van der Pol Oscillator}
337
The van der Pol oscillator exhibits self-sustained oscillations:
338
\begin{equation}
339
\frac{d^2x}{dt^2} - \mu(1 - x^2)\frac{dx}{dt} + x = 0
340
\end{equation}
341
342
\begin{pycode}
343
def van_der_pol(t, state, mu=1.0):
344
x, v = state
345
return [v, mu*(1 - x**2)*v - x]
346
347
fig, axes = plt.subplots(2, 3, figsize=(14, 8))
348
349
# Time series for different mu
350
mu_values = [0.1, 1.0, 5.0]
351
t_span = (0, 50)
352
t_eval = np.linspace(0, 50, 2000)
353
354
for i, mu in enumerate(mu_values):
355
ax = axes[0, i]
356
sol = solve_ivp(van_der_pol, t_span, [0.1, 0], t_eval=t_eval, args=(mu,))
357
ax.plot(sol.t, sol.y[0], 'b-', linewidth=0.8)
358
ax.set_xlabel('Time $t$')
359
ax.set_ylabel('$x(t)$')
360
ax.set_title(f'Van der Pol: $\\mu = {mu}$')
361
ax.grid(True, alpha=0.3)
362
363
# Phase portraits with limit cycle
364
for i, mu in enumerate(mu_values):
365
ax = axes[1, i]
366
367
# Multiple initial conditions
368
for x0, v0 in [(0.1, 0), (4, 0), (0, 4), (-3, -3)]:
369
sol = solve_ivp(van_der_pol, (0, 100), [x0, v0], t_eval=np.linspace(0, 100, 3000), args=(mu,))
370
ax.plot(sol.y[0], sol.y[1], linewidth=0.5, alpha=0.7)
371
372
ax.set_xlabel('$x$')
373
ax.set_ylabel('$\\dot{x}$')
374
ax.set_title(f'Phase Portrait: $\\mu = {mu}$')
375
ax.grid(True, alpha=0.3)
376
377
plt.tight_layout()
378
plt.savefig('van_der_pol.pdf', dpi=150, bbox_inches='tight')
379
plt.close()
380
381
# Calculate limit cycle amplitude
382
mu_test = 1.0
383
sol_lc = solve_ivp(van_der_pol, (0, 100), [0.1, 0], t_eval=np.linspace(0, 100, 5000), args=(mu_test,))
384
limit_cycle_amp = np.max(np.abs(sol_lc.y[0][-1000:]))
385
\end{pycode}
386
387
\begin{figure}[htbp]
388
\centering
389
\includegraphics[width=0.95\textwidth]{van_der_pol.pdf}
390
\caption{Van der Pol oscillator: time series (top) and phase portraits (bottom) showing limit cycles.}
391
\end{figure}
392
393
The limit cycle amplitude for $\mu = 1$ is approximately \py{f'{limit_cycle_amp:.3f}'}.
394
395
\chapter{Predator-Prey Dynamics}
396
397
\section{Lotka-Volterra Equations}
398
\begin{align}
399
\frac{dx}{dt} &= \alpha x - \beta xy \\
400
\frac{dy}{dt} &= \delta xy - \gamma y
401
\end{align}
402
403
\begin{pycode}
404
def lotka_volterra(t, state, alpha=1.0, beta=0.1, delta=0.075, gamma=1.5):
405
x, y = state
406
return [alpha*x - beta*x*y, delta*x*y - gamma*y]
407
408
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
409
410
t_span = (0, 50)
411
t_eval = np.linspace(0, 50, 1000)
412
sol = solve_ivp(lotka_volterra, t_span, [10, 5], t_eval=t_eval)
413
414
# Time series
415
ax = axes[0]
416
ax.plot(sol.t, sol.y[0], 'b-', label='Prey $x$')
417
ax.plot(sol.t, sol.y[1], 'r-', label='Predator $y$')
418
ax.set_xlabel('Time $t$')
419
ax.set_ylabel('Population')
420
ax.set_title('Population Dynamics')
421
ax.legend()
422
ax.grid(True, alpha=0.3)
423
424
# Phase portrait
425
ax = axes[1]
426
for x0, y0 in [(10, 5), (20, 10), (30, 5), (5, 2)]:
427
sol_pp = solve_ivp(lotka_volterra, t_span, [x0, y0], t_eval=t_eval)
428
ax.plot(sol_pp.y[0], sol_pp.y[1], linewidth=1)
429
ax.set_xlabel('Prey $x$')
430
ax.set_ylabel('Predator $y$')
431
ax.set_title('Phase Portrait')
432
ax.grid(True, alpha=0.3)
433
434
# Equilibrium
435
x_eq = 1.5 / 0.075
436
y_eq = 1.0 / 0.1
437
ax.plot(x_eq, y_eq, 'ko', markersize=8)
438
439
# Conservation quantity
440
ax = axes[2]
441
H = 0.075*sol.y[0] + 0.1*sol.y[1] - 1.5*np.log(sol.y[0]) - 1.0*np.log(sol.y[1])
442
ax.plot(sol.t, H, 'g-')
443
ax.set_xlabel('Time $t$')
444
ax.set_ylabel('$H(x, y)$')
445
ax.set_title('Conserved Quantity')
446
ax.grid(True, alpha=0.3)
447
448
plt.tight_layout()
449
plt.savefig('lotka_volterra.pdf', dpi=150, bbox_inches='tight')
450
plt.close()
451
452
period_peaks = []
453
for i in range(1, len(sol.y[0])-1):
454
if sol.y[0][i] > sol.y[0][i-1] and sol.y[0][i] > sol.y[0][i+1]:
455
period_peaks.append(sol.t[i])
456
if len(period_peaks) > 1:
457
lv_period = np.mean(np.diff(period_peaks))
458
else:
459
lv_period = 0
460
\end{pycode}
461
462
\begin{figure}[htbp]
463
\centering
464
\includegraphics[width=0.95\textwidth]{lotka_volterra.pdf}
465
\caption{Lotka-Volterra predator-prey model: populations oscillate with period $T \approx \py{f'{lv_period:.2f}'}$.}
466
\end{figure}
467
468
\chapter{Numerical Results}
469
470
\begin{pycode}
471
# Summary of eigenvalue classifications
472
classifications = [
473
('Real, opposite signs', 'Saddle', 'Unstable'),
474
('Real, both negative', 'Stable node', 'Stable'),
475
('Real, both positive', 'Unstable node', 'Unstable'),
476
('Complex, $\\text{Re} < 0$', 'Stable spiral', 'Stable'),
477
('Complex, $\\text{Re} > 0$', 'Unstable spiral', 'Unstable'),
478
('Pure imaginary', 'Center', 'Neutral'),
479
]
480
\end{pycode}
481
482
\begin{table}[htbp]
483
\centering
484
\caption{Equilibrium classification by eigenvalues}
485
\begin{tabular}{@{}lll@{}}
486
\toprule
487
Eigenvalues & Type & Stability \\
488
\midrule
489
\py{classifications[0][0]} & \py{classifications[0][1]} & \py{classifications[0][2]} \\
490
\py{classifications[1][0]} & \py{classifications[1][1]} & \py{classifications[1][2]} \\
491
\py{classifications[2][0]} & \py{classifications[2][1]} & \py{classifications[2][2]} \\
492
\py{classifications[3][0]} & \py{classifications[3][1]} & \py{classifications[3][2]} \\
493
\py{classifications[4][0]} & \py{classifications[4][1]} & \py{classifications[4][2]} \\
494
\py{classifications[5][0]} & \py{classifications[5][1]} & \py{classifications[5][2]} \\
495
\bottomrule
496
\end{tabular}
497
\end{table}
498
499
\chapter{Conclusions}
500
501
\begin{enumerate}
502
\item First-order ODEs: direction fields reveal solution behavior
503
\item Second-order ODEs: damping ratio determines oscillation character
504
\item Phase portraits: eigenvalues classify equilibrium types
505
\item Limit cycles: nonlinear systems can have isolated periodic orbits
506
\item Predator-prey: conservative systems show closed orbits
507
\end{enumerate}
508
509
\end{document}
510
511