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:

  1. Incorporate borrowing constraints into a DEQN using complementarity conditions
  2. Implement the Fischer–Burmeister function for smooth constraint handling
  3. Solve a consumption–savings model with an occasionally binding borrowing limit
  4. Understand basic architecture search for neural networks

# 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}")

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.