# 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 6: Constraints and Real-World Models
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:
- Incorporate borrowing constraints into a DEQN using complementarity conditions
- Implement the Fischer–Burmeister function for smooth constraint handling
- Solve a consumption–savings model with an occasionally binding borrowing limit
- Understand basic architecture search for neural networks
1. Why constraints are hard
Many economic models feature occasionally binding constraints: borrowing limits, zero lower bounds on interest rates, irreversibility of investment. These create kinks in policy functions that traditional methods struggle with.
The problem with grids + interpolation: - Policy functions have kinks at the constraint boundary - Linear interpolation smooths these kinks away - Higher-order interpolation can oscillate (Runge phenomenon — you saw this in PNM!)
The DEQN approach: - Use the Fischer–Burmeister complementarity function to encode constraints smoothly - The neural network can represent kinks (with ReLU) or smooth approximations (with tanh) - No grid, no interpolation
2. Fischer–Burmeister complementarity
A complementarity condition \(a \geq 0, \; b \geq 0, \; ab = 0\) (at least one must be zero) can be encoded as:
\[\Phi(a, b) = a + b - \sqrt{a^2 + b^2} = 0\]
This is the Fischer–Burmeister function. It’s smooth, differentiable (almost everywhere), and satisfies: - \(\Phi(a, b) = 0 \iff a \geq 0, \; b \geq 0, \; ab = 0\)
# Fischer-Burmeister function
def fischer_burmeister(a, b):
return a + b - torch.sqrt(a**2 + b**2 + 1e-8)
# Visualise
a_grid = torch.linspace(0, 3, 100)
b_vals = [0.0, 0.5, 1.0, 2.0]
fig, ax = plt.subplots(figsize=(8, 5))
for b_val in b_vals:
fb = fischer_burmeister(a_grid, torch.tensor(b_val))
ax.plot(a_grid.numpy(), fb.numpy(), linewidth=2, label=f'b = {b_val}')
ax.axhline(y=0, color='gray', linewidth=0.5)
ax.set_xlabel('a', fontsize=11); ax.set_ylabel('Φ(a, b)', fontsize=11)
ax.set_title('Fischer–Burmeister Complementarity Function', fontsize=12)
ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout(); plt.show()3. Consumption–savings with a borrowing limit
Consider a household that solves:
\[\max \mathbb{E}_0 \sum_{t=0}^{\infty} \beta^t u(c_t)\] \[\text{s.t.} \quad a_{t+1} = (1+r) a_t + y_t - c_t, \quad a_{t+1} \geq \underline{a}\]
The borrowing constraint \(a_{t+1} \geq \underline{a}\) binds when the household would like to borrow more but cannot.
Kuhn–Tucker conditions
The optimality conditions become:
\[u'(c_t) = \beta (1+r) u'(c_{t+1}) + \mu_t\] \[\mu_t \geq 0, \quad a_{t+1} - \underline{a} \geq 0, \quad \mu_t (a_{t+1} - \underline{a}) = 0\]
Using Fischer–Burmeister, we replace the complementarity with:
\[\Phi\big(\mu_t, \; a_{t+1} - \underline{a}\big) = 0\]
4. Architecture search basics
How do you choose the right network size? Architecture search automates this:
- Grid search: try all combinations of depth × width
- Random search: sample architectures randomly (often better than grid search!)
- Hyperband: allocate more training time to promising architectures, kill poor ones early
For economic models, a sensible starting point is 2 hidden layers of 64 neurons, trained with Adam at lr=1e-3.
Exercises
Exercise 1: Solve the consumption–savings model with \(\underline{a} = 0\) (no borrowing). Plot the policy function \(c(a)\) and identify the kink where the constraint binds.
Exercise 2: Compare the DEQN solution with and without the borrowing constraint. How does the constraint affect consumption at low wealth levels?
Next week: Physics-Informed Neural Networks — solving continuous-time economic models.