Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

The Function Representation

In dynamic programming, the value function VT(x)V_T(x) is computed on a discrete grid but must be evaluated at arbitrary points when solving earlier periods. The function representation turns a pre-computed array VTarrV^\text{arr}_T into a callable function that:

  • Accepts named arguments (e.g., wealth=150.0)

  • Returns exact values at grid points

  • Linearly interpolates between grid points

This notebook explains how it works, using a minimal terminal-regime model.

The two steps

Converting an array into a callable function requires two things:

  1. Coordinate finding — For continuous variables, convert physical values (e.g., wealth = 150) to generalized coordinates (fractional indices into the grid). See the interpolation notebook for details. Discrete variables use integer codes that directly serve as array indices.

  2. Interpolation — Use the generalized coordinates with map_coordinates to linearly interpolate between grid points.

Worked example

We set up a minimal model with a single terminal regime: a retiree choosing consumption given wealth, with CRRA utility. The wealth grid is intentionally coarse (10 points) to clearly show the interpolation behavior.

Computing the last-period value function array

In the terminal period, the value function is the maximum of utility over feasible actions. We use the internal regime representation to access the compiled functions and grids.

<Signature (consumption: 'ContinuousAction', utility__risk_aversion: 'float', wealth: 'ContinuousState') -> ('FloatND', 'FloatND')>

The function returns (utility, feasibility) for scalar inputs:

Utility: -0.2, feasible: False

To evaluate on the full state-action grid, we use productmap:

V_arr shape: (10,) (10 wealth grid points)
Loading...

Creating the function representation

The function representation turns V_arr into a callable that can be evaluated at any wealth value. The V_arr_name argument sets the name of the array parameter in the resulting function.

<Signature (V_arr: 'FloatND | IntND', next_wealth: 'FloatND') -> 'FloatND'>

This scalar function is then wrapped with productmap so it can evaluate on arrays:

Visualizing interpolation

We evaluate the function representation on the original grid points (which should match exactly) and on additional points between grid points (which are interpolated).

Loading...

The orange points from the function representation lie exactly on the blue line connecting the grid points. The function representation behaves like an analytical function corresponding to this piecewise linear interpolation.

Technical details

The function representation is assembled from three building blocks, each implemented as a small function with a carefully chosen signature. These functions are composed using dags.concatenate_functions.

Lookup function

Indexes into the value function array using named axes. This is important because dags.concatenate_functions matches functions by argument names.

Signature: (wealth_index: 'FloatND | IntND', V_arr: 'FloatND | IntND') -> 'FloatND'
Array([-2. , -0.22028813, -0.13457806], dtype=float32)

Coordinate finder

Converts physical values to generalized coordinates — fractional indices into the grid. For a linearly spaced grid [1, 45.3, 89.7, ...], the value 23.2 might correspond to coordinate 0.5 (halfway between indices 0 and 1).

Signature: (wealth: 'FloatND') -> 'FloatND'
  wealth =     1.00  →  coordinate = 0.0000
  wealth =    23.17  →  coordinate = 0.5000
  wealth =   390.00  →  coordinate = 8.7744

Interpolator

Uses the generalized coordinates to linearly interpolate on the value function array via map_coordinates.

Signature: (V_arr: 'FloatND', wealth_index: 'FloatND') -> 'FloatND'
Loading...

Re-implementation from scratch

To understand how the pieces fit together, let’s re-implement the function representation manually using dags.concatenate_functions.

The general idea: create functions for array lookup, coordinate finding, and interpolation, each with signatures that declare their dependencies. Then let dags wire them together.

Steps

  1. Discrete lookup — index into the array using discrete positions. With no discrete states in our model, this is the identity (returns the array unchanged)

  2. Coordinate finder for each continuous state — maps values to fractional indices

  3. Interpolator — uses coordinates to interpolate on the array

Implementation

Discrete states: {}
Composed signature: (V_arr, wealth)
Loading...

The orange points coincide perfectly with the blue grid points — our manual re-implementation matches pylcm’s built-in function representation.