# Setup
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
torch.manual_seed(42)
np.random.seed(42)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"PyTorch {torch.__version__} on {device}")Week 7: Physics-Informed Neural Networks
Deep Learning for Macroeconomics — Honours, The University of Edinburgh
Instructor: Juan Zurita · juan.zurita@ed.ac.uk
Learning objectives
By the end of this notebook you will be able to:
- Explain the PINN principle: minimise the PDE/ODE residual over collocation points
- Solve a simple ODE boundary-value problem with a PINN
- Apply PINNs to the cake-eating HJB equation — a continuous-time consumption problem
- Discuss soft vs hard boundary conditions and their impact on training
1. From discrete to continuous time
So far we’ve worked in discrete time: \(k_{t+1} = f(k_t) - c_t\). Many models in macro and finance are naturally formulated in continuous time:
\[\frac{dk}{dt} = f(k) - c, \quad \rho V(k) = \max_c \Big\{ u(c) + V'(k) [f(k) - c] \Big\}\]
The second equation is the Hamilton–Jacobi–Bellman (HJB) equation — a PDE that the value function must satisfy.
The PINN idea
A Physics-Informed Neural Network parameterises the solution \(V(k; \theta)\) as a neural network and minimises:
\[\mathcal{L}(\theta) = \frac{1}{N} \sum_{i=1}^{N} \Big| \rho V(k_i; \theta) - \max_c \big\{ u(c) + V'(k_i; \theta) [f(k_i) - c] \big\} \Big|^2\]
The key: \(V'(k; \theta)\) is computed via automatic differentiation — differentiating the network output with respect to its input \(k\).
2. Warm-up: solving an ODE with a PINN
Before tackling the HJB equation, let’s solve a simple ODE:
\[y''(x) + y(x) = 0, \quad y(0) = 0, \quad y(\pi/2) = 1\]
The analytical solution is \(y(x) = \sin(x)\).
# PINN for a simple ODE: y'' + y = 0
class PINN_ODE(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(1, 32), nn.Tanh(),
nn.Linear(32, 32), nn.Tanh(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.net(x)
model = PINN_ODE()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
# Collocation points (where we enforce the ODE)
x_col = torch.linspace(0, np.pi/2, 50).reshape(-1, 1).requires_grad_(True)
# Boundary points
x_bc0 = torch.tensor([[0.0]])
x_bc1 = torch.tensor([[np.pi/2]])
for epoch in range(5000):
# ODE residual: y'' + y = 0
y = model(x_col)
dy = torch.autograd.grad(y, x_col, torch.ones_like(y), create_graph=True)[0]
d2y = torch.autograd.grad(dy, x_col, torch.ones_like(dy), create_graph=True)[0]
ode_residual = d2y + y
# Boundary conditions
bc0_loss = (model(x_bc0) - 0.0)**2
bc1_loss = (model(x_bc1) - 1.0)**2
# Total loss
loss = torch.mean(ode_residual**2) + 100 * (bc0_loss + bc1_loss)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Plot
model.eval()
x_test = torch.linspace(0, np.pi/2, 200).reshape(-1, 1)
with torch.no_grad():
y_pinn = model(x_test).numpy().flatten()
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x_test.numpy(), np.sin(x_test.numpy()), 'k-', linewidth=2, label='Analytical: sin(x)')
ax.plot(x_test.numpy(), y_pinn, '--', color='#7a2318', linewidth=2, label='PINN solution')
ax.set_xlabel('x'); ax.set_ylabel('y(x)')
ax.set_title('PINN Solution to y\'\' + y = 0'); ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()
print(f"Max error: {np.max(np.abs(y_pinn - np.sin(x_test.numpy().flatten()))):.6f}")3. The cake-eating HJB equation
A consumer with a cake of size \(k\) solves:
\[\rho V(k) = \max_c \Big\{ \ln(c) + V'(k) (-c) \Big\}\]
The FOC gives \(c^*(k) = 1/V'(k)\), substituting back:
\[\rho V(k) = \ln\left(\frac{1}{V'(k)}\right) - 1\]
This is a nonlinear ODE for \(V(k)\) — perfect for a PINN.
4. Soft vs hard boundary conditions
Two approaches:
Soft BCs (penalty method): add \(\lambda \cdot (\text{BC violation})^2\) to the loss. Simple but requires tuning \(\lambda\).
Hard BCs (construction): write the network output so BCs are satisfied by construction: \[\hat{y}(x) = A(x) + B(x) \cdot \text{NN}(x)\] where \(A(x)\) satisfies the BCs and \(B(x) = 0\) at the boundaries.
Hard BCs are preferred when possible — they guarantee exact boundary satisfaction and make training more stable.
Exercises
Exercise 1: Solve the cake-eating HJB with a PINN. Verify against the analytical solution \(V(k) = \frac{1}{\rho}[\ln(\rho k) - 1]\).
Exercise 2: Solve the Black–Scholes PDE: \(\frac{\partial V}{\partial t} + \frac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + rS\frac{\partial V}{\partial S} - rV = 0\)
Next week: Heterogeneous Agents — the frontier where deep learning really shines.