# Assign a value to a variable and print it
x = 3.14
print("The value of x is", x)The value of x is 3.14
Useful Python Commands for Differential Equations
This page is a quick-reference guide to Python tools used throughout this course. It is not a complete Python tutorial. Students who want a deeper introduction may find the following resources helpful:
Python (Python Software Foundation 2024) can be installed and managed in several ways. Below we describe two popular approaches: uv (a modern, fast tool) and conda (a traditional choice popular in scientific computing). Either approach works fine for this course.
uv (Recommended for New Users)uv is a fast, modern Python package and project manager written in Rust. It can install Python itself, create isolated project environments, and manage packages — all in one tool.
Installing uv:
On macOS and Linux (run in your terminal):
On Windows (run in PowerShell):
Installing Python with uv:
Creating a virtual environment and installing packages:
A virtual environment is an isolated folder that holds a specific Python version and a set of packages for a project. This keeps your projects from interfering with each other.
# Create a new project folder and enter it
mkdir my_diffeq_project
cd my_diffeq_project
# Create a virtual environment using a specific Python version
uv venv --python 3.14
# Activate the environment
# On macOS/Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate
# Install packages needed for this course
uv pip install numpy scipy sympy matplotlib
# Deactivate when you're done working
deactivateRegistering the environment as a Jupyter kernel (so it appears in VS Code or JupyterLab):
For full documentation see https://docs.astral.sh/uv/.
conda / AnacondaConda is a package and environment manager that is widely used in scientific computing. Anaconda is a full distribution that bundles Python, conda, and hundreds of scientific packages. Miniconda is a lighter-weight installer that includes only Python and conda itself.
Installing conda:
Download and run the installer for your operating system from one of the following:
Creating a conda environment:
# Create a new environment named "diffeq" with Python 3.14
conda create -n diffeq python=3.14
# Activate the environment
conda activate diffeq
# Install the packages needed for this course
conda install numpy scipy sympy matplotlib
# Install Jupyter support
conda install ipykernel jupyter
# Register the environment as a Jupyter kernel
python -m ipykernel install --user --name diffeq_py --display-name "DiffEq Python"
# Deactivate when done
conda deactivateUseful conda commands:
For full documentation see https://docs.conda.io/.
You can write and run Python code in several ways:
| Interface | Description |
|---|---|
| VS Code | Full-featured editor; excellent Jupyter notebook support via extension |
| JupyterLab | Browser-based environment for notebooks |
| Spyder | MATLAB-like IDE popular with scientists and engineers |
Terminal / python REPL |
Lightweight; good for quick tests |
For this course, VS Code or JupyterLab are recommended. Both work seamlessly with Quarto and Jupyter kernels.
Python is a general-purpose, high-level programming language known for its clean, readable syntax. It is one of the most widely used languages in scientific computing, data analysis, machine learning, and engineering.
Key characteristics:
A minimal Python example:
The value of x is 3.14
Python supports the standard arithmetic operators +, -, *, /, ** (exponentiation), and // (integer division):
Python’s power comes largely from its ecosystem of modules (also called libraries or packages). A module is a file (or collection of files) containing Python code (e.g., functions, classes, and constants) that extends what plain Python can do.
For example, plain Python does not have a function to compute \(\sin(x)\). But the math module (part of Python’s standard library) does:
For this course we rely on four major third-party packages:
| Package | Purpose | Documentation |
|---|---|---|
| NumPy | Fast arrays and numerical math | numpy.org |
| SciPy | Scientific algorithms (ODE solvers, linear algebra, …) | scipy.org |
| SymPy | Symbolic (exact) mathematics | sympy.org |
| Matplotlib | 2-D and 3-D plotting | matplotlib.org |
To use a module you must first import it. Importing makes the module’s contents available in your current session.
Method 1 — import the whole module:
The prefix math. is the module’s namespace — it tells Python where to find sqrt. Using namespaces prevents name conflicts: if two modules both define a function called array, you can still use both as numpy.array and someother.array.
Method 2 — import with an alias (most common in scientific Python):
[1 2 3]
y
Using standard aliases (np, sym, sp, plt) makes code more concise and matches what you will see in textbooks and online documentation.
Method 3 — import specific names:
1.4142135623730951
3.141592653589793
Avoid from numpy import *. This imports everything into your namespace, which can silently overwrite built-in Python names and makes code harder to read and debug.
Functions let you package a block of code under a name so you can reuse it without copying and pasting. In Python, functions are defined with the def keyword.
25
6.25
The string immediately inside the function (in triple quotes) is called a docstring. It documents what the function does and is a good habit to include. Note: Indentation matters in Python, that is how scope is determined, i.e., indentation determines which lines belong to the function body.
def exponential_decay(t, y0=1.0, k=1.0):
"""
Evaluate the exponential decay function y0 * exp(-k*t).
Parameters
----------
t : float or array-like — time value(s)
y0 : float — initial value (default 1.0)
k : float — decay rate (default 1.0)
"""
return y0 * np.exp(-k * t)
t_vals = np.linspace(0, 5, 6) # create input grid, i.e., [0, 1, 2, 3, 4, 5]
print(exponential_decay(t_vals)) # uses defaults y0=1, k=1
print(exponential_decay(t_vals, y0=2.0, k=0.5)) # override defaults[1. 0.36787944 0.13533528 0.04978707 0.01831564 0.00673795]
[2. 1.21306132 0.73575888 0.44626032 0.27067057 0.16417 ]
For short, one-line functions, Python offers lambda expressions:
Lambda functions are often used when passing a function as an argument to another function, as we will see with ODE solvers.
Python functions can return multiple values as a tuple:
sin(π/4) = 0.707107
cos(π/4) = 0.707107
We use Matplotlib for all plotting (Hunter 2007). The standard import is:
The Matplotlib gallery is an excellent resource for example plots with source code.
import matplotlib.pyplot as plt
import numpy as np
# --- Data ---
t = np.linspace(0, 4 * np.pi, 400) # 400 points from 0 to 4π
# --- Create figure and axes ---
fig, ax = plt.subplots(figsize=(8, 4))
# --- Plot curves ---
ax.plot(t, np.sin(t), label=r'$\sin(t)$', color='steelblue', linewidth=2)
ax.plot(t, np.cos(t), label=r'$\cos(t)$', color='tomato', linewidth=2, linestyle='--')
# --- Labels and formatting ---
ax.set_xlabel('t', fontsize=13)
ax.set_ylabel('y', fontsize=13)
ax.set_title('Sine and Cosine', fontsize=14)
ax.legend(fontsize=12)
ax.axhline(0, color='black', linewidth=0.8) # draw y = 0 line
ax.grid(True, linestyle=':', alpha=0.6)
plt.tight_layout()
plt.show()
Key ideas:
fig, ax = plt.subplots() creates a figure (the whole image) and an axes (the plotting region). This is the recommended, object-oriented style.ax.plot(x, y) draws a line.r'$ ... $'.plt.tight_layout() adjusts spacing to prevent clipped labels.fig, axes = plt.subplots(1, 2, figsize=(10, 4)) # 1 row, 2 columns
t = np.linspace(0, 4*np.pi, 400)
axes[0].plot(t, np.sin(t), color='steelblue', linewidth=2)
axes[0].set_title(r'$y = \sin(t)$')
axes[0].set_xlabel('t')
axes[0].grid(True, linestyle=':', alpha=0.6)
axes[1].plot(t, np.exp(-0.3 * t) * np.sin(t), color='tomato', linewidth=2)
axes[1].set_title(r'$y = e^{-0.3t}\sin(t)$')
axes[1].set_xlabel('t')
axes[1].grid(True, linestyle=':', alpha=0.6)
plt.tight_layout()
plt.show()
In the study of differential equations, direction fields (also called slope fields for first-order equations or vector fields for systems) are invaluable for understanding qualitative behavior.
For \(\dfrac{dy}{dt} = f(t, y)\), we draw a short line segment at each grid point \((t, y)\) with slope \(f(t, y)\).
import matplotlib.pyplot as plt
import numpy as np
# ODE: dy/dt = t - y
def f(t, y):
return t - y
# Create a grid of (t, y) values
t_vals = np.linspace(-1, 4, 20)
y_vals = np.linspace(-2, 5, 20)
T, Y = np.meshgrid(t_vals, y_vals)
# Compute slope at each grid point
dY = f(T, Y)
dT = np.ones_like(dY) # dt component is always 1
# Normalize for uniform arrow length
magnitude = np.sqrt(dT**2 + dY**2)
dT_norm = dT / magnitude
dY_norm = dY / magnitude
fig, ax = plt.subplots(figsize=(7, 6))
ax.quiver(T, Y, dT_norm, dY_norm,
angles='xy', color='steelblue', alpha=0.8, scale=25)
# Overlay a particular solution for reference
from scipy.integrate import solve_ivp
sol = solve_ivp(f, [-1, 4], [3.0], dense_output=True)
t_plot = np.linspace(-1, 4, 200)
ax.plot(t_plot, sol.sol(t_plot)[0], 'tomato', linewidth=2.5,
label=r'Solution with $y(-1)=3$')
ax.set_xlabel('t', fontsize=13)
ax.set_ylabel('y', fontsize=13)
ax.set_title(r"Slope field: $\dfrac{dy}{dt} = t - y$", fontsize=14)
ax.legend(fontsize=12)
ax.set_xlim(-1, 4)
ax.set_ylim(-2, 5)
ax.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.show()
For the system \(\dfrac{d\mathbf{x}}{dt} = A\mathbf{x}\), we draw arrows at each grid point \((x_1, x_2)\) pointing in the direction \((f_1, f_2)\).
# System: dx/dt = -x + y, dy/dt = -x - y
def system(X, Y):
dX = -X + Y
dY = -X - Y
return dX, dY
x_vals = np.linspace(-3, 3, 18)
y_vals = np.linspace(-3, 3, 18)
X, Y = np.meshgrid(x_vals, y_vals)
dX, dY = system(X, Y)
mag = np.sqrt(dX**2 + dY**2)
dX_n = dX / mag
dY_n = dY / mag
fig, ax = plt.subplots(figsize=(6, 6))
ax.quiver(X, Y, dX_n, dY_n, mag, cmap='coolwarm', alpha=0.9)
ax.set_xlabel(r'$x_1$', fontsize=13)
ax.set_ylabel(r'$x_2$', fontsize=13)
ax.set_title(r"Vector field: $\dot{x}_1 = -x_1+x_2,\;\dot{x}_2 = -x_1-x_2$",
fontsize=12)
ax.set_aspect('equal')
ax.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.show()
The cmap='coolwarm' coloring encodes the speed (magnitude) of the vector field — blue regions are slow, red regions are fast.
NumPy (Numerical Python) is the foundation of scientific computing in Python (Harris et al. 2020). Its central object is the ndarray — an efficient, multidimensional array.
Full documentation: https://numpy.org/doc/stable/
import numpy as np
# From a Python list
a = np.array([1, 2, 3, 4, 5])
print(a) # [1 2 3 4 5]
print(a.dtype) # int64 (data type)
print(a.shape) # (5,) (dimensions)
# Floating-point array
b = np.array([1.0, 2.0, 3.0])
print(b.dtype) # float64
# 2-D array (matrix)
A = np.array([[1, 2, 3],
[4, 5, 6]])
print(A.shape) # (2, 3) → 2 rows, 3 columns[1 2 3 4 5]
int64
(5,)
float64
(2, 3)
[0. 0. 0. 0.]
[[1. 1. 1.]
[1. 1. 1.]]
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
[0. 0.25 0.5 0.75 1. ]
[0 2 4 6 8]
NumPy operations act element-wise by default:
[5. 7. 9.]
[ 4. 10. 18.]
[1. 4. 9.]
[1. 1.41421356 1.73205081]
a * b is not matrix multiplication. For matrix multiplication use A @ B or np.dot(A, B).
NumPy provides vectorized versions of all standard math functions:
[0.00000000e+00 7.07106781e-01 1.00000000e+00 7.07106781e-01
1.22464680e-16]
[1. 0.45593813 0.20787958 0.09478022 0.04321392]
[0. 0.57964145 0.94421571 1.21080774 1.42108041]
These are far faster than looping over elements manually.
a = np.array([10, 20, 30, 40, 50])
print(a[0]) # 10 (first element)
print(a[-1]) # 50 (last element)
print(a[1:4]) # [20 30 40] (slice: indices 1,2,3)
print(a[::2]) # [10 30 50] (every other element)
# 2-D indexing
A = np.arange(12).reshape(3, 4)
print(A)
print(A[1, 2]) # element in row 1, column 2
print(A[:, 1]) # all rows, column 1 (column vector)
print(A[0, :]) # row 010
50
[20 30 40]
[10 30 50]
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
6
[1 5 9]
[0 1 2 3]
SciPy builds on NumPy and provides algorithms for optimization, integration, interpolation, signal processing, statistics, and — most importantly for us — differential equations (Virtanen et al. 2020).
Full documentation: https://docs.scipy.org/doc/scipy/
SciPy is organized into submodules:
| Submodule | Content |
|---|---|
scipy.integrate |
Numerical integration, ODE solvers |
scipy.linalg |
Linear algebra |
scipy.optimize |
Root-finding, minimization |
scipy.signal |
Signal processing |
scipy.sparse |
Sparse matrices |
Import submodules explicitly:
SciPy’s ODE solvers and linear algebra routines are covered in the dedicated sections below.
SymPy is a Python library for symbolic mathematics — it performs exact algebraic manipulations rather than numerical approximations (Meurer et al. 2017). Think of it as a free, Python-native computer algebra system (CAS).
Full documentation: https://docs.sympy.org/latest/
f'(t) = -exp(-t)*sin(2*t) + 2*exp(-t)*cos(2*t)
∫f dt = -exp(-t)*sin(2*t)/5 - 2*exp(-t)*cos(2*t)/5
∫₀^π sin(t) dt = 2
| Feature | SymPy (symbolic) | NumPy/SciPy (numerical) |
|---|---|---|
| Results | Exact | Approximate (float) |
| Speed | Slower | Faster |
| Good for | Deriving formulas, exact solutions | Large-scale computation |
You can convert a SymPy expression to a fast numerical function with sym.lambdify:
[0. 0.02449962 0.04776835 0.06983789 0.09073981]
scipy.integrate.solve_ivp is the primary tool for solving initial value problems (IVPs) numerically. It implements several Runge–Kutta and multi-step methods.
General call signature:
| Argument | Meaning |
|---|---|
fun |
Function f(t, y) defining \(y' = f(t,y)\) |
t_span |
(t0, tf) — start and end times |
y0 |
Initial condition (list or array) |
method |
ODE algorithm (default 'RK45') |
t_eval |
Times at which to store the solution |
Solve \(y' = -2y\), \(y(0) = 3\), and compare with the exact solution \(y(t) = 3e^{-2t}\).
from scipy.integrate import solve_ivp
def ode(t, y):
"""dy/dt = -2*y"""
return [-2 * y[0]] # must return a list/array
t_span = (0, 4)
y0 = [3.0] # initial condition as a list
t_eval = np.linspace(0, 4, 200) # times at which to record solution
sol = solve_ivp(ode, t_span, y0, method='RK45', t_eval=t_eval)
# sol.t → time values
# sol.y → array of shape (n_variables, n_times)
# Exact solution for comparison
y_exact = 3 * np.exp(-2 * sol.t)
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(sol.t, sol.y[0], 'steelblue', linewidth=2.5, label='RK45 (numerical)')
ax.plot(sol.t, y_exact, 'tomato', linewidth=1.5, linestyle='--',
label=r'Exact: $3e^{-2t}$')
ax.set_xlabel('t', fontsize=13)
ax.set_ylabel('y(t)', fontsize=13)
ax.set_title(r"$y' = -2y,\quad y(0)=3$", fontsize=14)
ax.legend(fontsize=12)
ax.grid(True, linestyle=':', alpha=0.6)
plt.tight_layout()
plt.show()
Solve the damped harmonic oscillator: \(m x'' + c x' + k x = 0\).
We reduce to a first-order system: let \(x_1 = x\), \(x_2 = x'\). Then
\[x_1' = x_2, \qquad x_2' = -\frac{k}{m} x_1 - \frac{c}{m} x_2.\]
from scipy.integrate import solve_ivp
m = 1.0 # mass
c = 0.4 # damping coefficient
k = 5.0 # spring constant
def oscillator(t, state):
"""
Damped harmonic oscillator as a first-order system.
state = [x1, x2] = [position, velocity]
"""
x1, x2 = state
dx1 = x2
dx2 = -(k/m) * x1 - (c/m) * x2
return [dx1, dx2]
t_span = (0, 20)
y0 = [2.0, 0.0] # initial position = 2, initial velocity = 0
t_eval = np.linspace(0, 20, 500)
sol = solve_ivp(oscillator, t_span, y0, method='RK45', t_eval=t_eval)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(sol.t, sol.y[0], 'steelblue', linewidth=2, label='Position $x(t)$')
ax.plot(sol.t, sol.y[1], 'tomato', linewidth=2, label='Velocity $x\'(t)$')
ax.set_xlabel('t', fontsize=13)
ax.set_ylabel('State', fontsize=13)
ax.set_title('Damped Harmonic Oscillator', fontsize=14)
ax.legend(fontsize=12)
ax.grid(True, linestyle=':', alpha=0.6)
plt.tight_layout()
plt.show()
Solve \(y'' + y = \cos(t)\) (resonance) with \(y(0) = 0\), \(y'(0) = 0\).
from scipy.integrate import solve_ivp
def forced_oscillator(t, state):
x1, x2 = state
dx1 = x2
dx2 = -x1 + np.cos(t) # forcing term cos(t) at natural frequency
return [dx1, dx2]
t_span = (0, 40)
y0 = [0.0, 0.0]
t_eval = np.linspace(0, 40, 800)
sol = solve_ivp(forced_oscillator, t_span, y0, method='RK45', t_eval=t_eval,
rtol=1e-8, atol=1e-10) # tighter tolerances for accuracy
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(sol.t, sol.y[0], 'steelblue', linewidth=1.5)
ax.set_xlabel('t', fontsize=13)
ax.set_ylabel('y(t)', fontsize=13)
ax.set_title(r"Resonance: $y'' + y = \cos(t)$", fontsize=14)
ax.grid(True, linestyle=':', alpha=0.6)
plt.tight_layout()
plt.show()
solve_ivpmethod |
Description |
|---|---|
'RK45' |
Runge–Kutta 4(5); default; good for most problems |
'RK23' |
Runge–Kutta 2(3); lower accuracy, faster |
'DOP853' |
Dormand–Prince 8(5,3); high accuracy |
'Radau' |
Implicit Runge–Kutta; best for stiff problems |
'BDF' |
Backward differentiation formula; stiff problems |
'LSODA' |
Automatically detects stiffness |
Success? True
Message: The solver successfully reached the end of the integration interval.
Solution shape (n_vars × n_times): (2, 800)
SymPy can find exact, closed-form solutions to ODEs when they exist.
Solve \(y' + 2y = 4\), \(y(0) = 1\).
General solution: Eq(y(t), C1*exp(-2*t) + 2)
Particular solution: Eq(y(t), 2 - exp(-2*t))
Solve \(y'' - 3y' + 2y = 0\), \(y(0) = 0\), \(y'(0) = 1\).
ode2 = sym.Eq(y(t).diff(t,2) - 3*y(t).diff(t) + 2*y(t), 0)
general2 = sym.dsolve(ode2, y(t))
print("General solution:", general2)
C1, C2 = sym.symbols('C1 C2')
ics = sym.solve([
general2.rhs.subs(t, 0) - 0, # y(0) = 0
general2.rhs.diff(t).subs(t, 0) - 1 # y'(0) = 1
], [C1, C2])
particular2 = general2.subs(ics)
print("Particular solution:", particular2)General solution: Eq(y(t), (C1 + C2*exp(t))*exp(t))
Particular solution: Eq(y(t), (exp(t) - 1)*exp(t))
Solve \(y'' + y = \sin(t)\) (the resonance equation) symbolically.
Eq(y(t), C2*sin(t) + (C1 - t/2)*cos(t))
A^T:
[[2. 5.]
[1. 3.]]
det(A): 1.0
A^{-1}:
[[ 3. -1.]
[-5. 2.]]
A @ x: [4. 7.]
A @ B:
[[2. 2.]
[5. 6.]]
For a square matrix \(A\), the eigenvalue problem is \(A\mathbf{v} = \lambda \mathbf{v}\).
Eigenvalues (NumPy): [5. 2.]
Eigenvectors (columns):
[[ 0.70710678 -0.4472136 ]
[ 0.70710678 0.89442719]]
# Using SciPy (more options; can return left eigenvectors, etc.)
vals, vecs = linalg.eig(A)
print("Eigenvalues (SciPy):", vals.real)
# The j-th eigenvector is vecs[:, j]
for j in range(len(vals)):
lam = vals[j].real
v = vecs[:, j].real
print(f" λ = {lam:.4f}, v = {v}")
print(f" Verify A @ v = λ * v: {np.allclose(A @ v, lam * v)}")Eigenvalues (SciPy): [5. 2.]
λ = 5.0000, v = [0.70710678 0.70710678]
Verify A @ v = λ * v: True
λ = 2.0000, v = [-0.4472136 0.89442719]
Verify A @ v = λ * v: True
np.linalg.eig and scipy.linalg.eig may return complex eigenvalues even for real matrices (when complex eigenvalues occur in conjugate pairs). Use .real and .imag to extract real and imaginary parts.
Eigenvalues of symmetric matrix: [1.38196601 3.61803399]
Matrix A:
⎡4 1⎤
⎢ ⎥
⎣2 3⎦
det(A) = 10
A^{-1}:
⎡3/10 -1/10⎤
⎢ ⎥
⎣-1/5 2/5 ⎦
# eigenvals() returns {eigenvalue: algebraic_multiplicity, ...}
evals = A.eigenvals()
print("Eigenvalues:", evals)
# eigenvects() returns [(eigenvalue, multiplicity, [eigenvectors]), ...]
evects = A.eigenvects()
for lam, mult, vecs in evects:
print(f"\nλ = {lam} (multiplicity {mult})")
for v in vecs:
print(f" eigenvector: {v.T}")Eigenvalues: {5: 1, 2: 1}
λ = 2 (multiplicity 1)
eigenvector: Matrix([[-1/2, 1]])
λ = 5 (multiplicity 1)
eigenvector: Matrix([[1, 1]])
P =
⎡-1 1⎤
⎢ ⎥
⎣2 1⎦
D =
⎡2 0⎤
⎢ ⎥
⎣0 5⎦
P * D * P^{-1} =
⎡4 1⎤
⎢ ⎥
⎣2 3⎦
Eigenvalues of [[a,1],[1,a]]: {a - 1: 1, a + 1: 1}
A phase portrait shows the trajectories of a two-dimensional autonomous system \[\frac{d\mathbf{x}}{dt} = \mathbf{f}(\mathbf{x}), \qquad \mathbf{x} = (x_1, x_2)^T,\] in the \((x_1, x_2)\) plane (the phase plane) for many different initial conditions.
from scipy.integrate import solve_ivp
def linear_system(t, state, A):
"""Linear system dx/dt = A*x."""
return A @ np.array(state)
# System matrix
A_mat = np.array([[-1, 2],
[-2, -1]], dtype=float)
# Grid for the vector field
x1 = np.linspace(-3, 3, 18)
x2 = np.linspace(-3, 3, 18)
X1, X2 = np.meshgrid(x1, x2)
dX1 = A_mat[0, 0]*X1 + A_mat[0, 1]*X2
dX2 = A_mat[1, 0]*X1 + A_mat[1, 1]*X2
mag = np.sqrt(dX1**2 + dX2**2)
fig, ax = plt.subplots(figsize=(7, 7))
# Plot the vector field
ax.quiver(X1, X2, dX1/mag, dX2/mag, mag,
cmap='Blues', alpha=0.7, scale=28)
# Plot several trajectories from different initial conditions
initial_conditions = [
[2.5, 0], [-2.5, 0], [0, 2.5], [0, -2.5],
[2, 2], [-2, -2], [2, -2], [-2, 2]
]
t_span = (0, 6)
t_eval = np.linspace(0, 6, 300)
for ic in initial_conditions:
sol = solve_ivp(linear_system, t_span, ic, args=(A_mat,),
t_eval=t_eval, method='RK45')
ax.plot(sol.y[0], sol.y[1], 'tomato', linewidth=1.4, alpha=0.85)
# Mark starting point
ax.plot(ic[0], ic[1], 'ko', markersize=5)
ax.set_xlabel(r'$x_1$', fontsize=13)
ax.set_ylabel(r'$x_2$', fontsize=13)
ax.set_title(
r'Phase portrait: $\dot{\mathbf{x}} = A\mathbf{x}$' +
f'\n$A = [{A_mat[0,0]:.0f},{A_mat[0,1]:.0f};{A_mat[1,0]:.0f},{A_mat[1,1]:.0f}]$',
fontsize=12
)
ax.axhline(0, color='k', linewidth=0.7)
ax.axvline(0, color='k', linewidth=0.7)
ax.set_aspect('equal')
ax.set_xlim(-3.2, 3.2)
ax.set_ylim(-3.2, 3.2)
ax.grid(True, linestyle=':', alpha=0.4)
plt.tight_layout()
plt.show()
from scipy.integrate import solve_ivp
g = 9.8 # gravitational acceleration (m/s²)
L = 1.0 # pendulum length (m)
def pendulum(t, state):
"""
Nonlinear pendulum (no damping).
state = [theta, omega] (angle, angular velocity)
"""
theta, omega = state
return [omega, -(g/L) * np.sin(theta)]
x1 = np.linspace(-np.pi, np.pi, 22)
x2 = np.linspace(-5, 5, 18)
X1, X2 = np.meshgrid(x1, x2)
dX1 = X2
dX2 = -(g/L) * np.sin(X1)
mag = np.sqrt(dX1**2 + dX2**2)
fig, ax = plt.subplots(figsize=(9, 6))
ax.quiver(X1, X2, dX1/mag, dX2/mag, mag, cmap='Blues', alpha=0.6, scale=28)
for theta0 in np.linspace(-2.8, 2.8, 9):
for omega0 in [-3, -1, 0, 1, 3]:
sol = solve_ivp(pendulum, (0, 8), [theta0, omega0],
t_eval=np.linspace(0, 8, 500), method='RK45',
rtol=1e-8, atol=1e-10)
ax.plot(sol.y[0], sol.y[1], 'tomato', linewidth=0.9, alpha=0.7)
ax.set_xlabel(r'$\theta$ (rad)', fontsize=13)
ax.set_ylabel(r'$\omega$ (rad/s)', fontsize=13)
ax.set_title('Phase portrait: Nonlinear Pendulum', fontsize=14)
ax.set_xlim(-np.pi, np.pi)
ax.set_ylim(-5, 5)
ax.axhline(0, color='k', linewidth=0.7)
ax.grid(True, linestyle=':', alpha=0.4)
plt.tight_layout()
plt.show()
The Laplace transform \(\mathcal{L}\{f(t)\}\) is an important tool for solving linear ODEs with constant coefficients. SymPy can compute Laplace transforms and inverse Laplace transforms symbolically.
L{e^{-2t} sin(3t)} = 3/((s + 2)**2 + 9)
L^{-1}{s/(s²+4)} = cos(2*t)
# Solving an IVP via Laplace transform
# Solve y'' + 4y = 0, y(0) = 1, y'(0) = 0
Y = sym.Function('Y') # Y(s) = L{y(t)}
# Using known Laplace transform rules:
# L{y''} = s^2*Y - s*y(0) - y'(0)
# = s^2*Y - s - 0
# So: (s^2 + 4)*Y = s => Y = s/(s^2 + 4)
Y_s = s / (s**2 + 4)
y_t = sym.inverse_laplace_transform(Y_s, s, t)
print("Solution y(t) =", y_t) # cos(2t)Solution y(t) = cos(2*t)
Even experienced programmers spend much of their time debugging. Here are some practical strategies.
Python’s error messages are informative. The last line tells you the error type and a short description; the lines above show the traceback — where in the code the error occurred.
# Common error types
# NameError — you used a variable or function that isn't defined
# TypeError — wrong type passed to a function
# ValueError — right type, but inappropriate value
# IndexError — index out of bounds
# ZeroDivisionError — division by zero
# AttributeError — object doesn't have the attribute/method you calledprint StatementsInsert print() calls to inspect intermediate values:
Shape mismatches are a common source of errors in NumPy code:
Python version: 3.14.4 | packaged by conda-forge | (main, Apr 8 2026, 02:33:53) [Clang 20.1.8 ]
numpy==2.4.3
scipy==1.17.1
sympy==1.14.0
matplotlib==3.10.8