I don’t want to clutter Aiyagari example, Transition - #24 by MichaelTiemann with topics too far off-piste from the core transition path topic, so here’s a new thread. I spent yesterday with “Geography vs Income: The Heterogenous effects of carbon taxation” by Labrousse and Perereau (2026, DOI: Redirecting).
The first puzzler, easily solved, was how to present a spatial dimension to the toolkit (we know how to build grids, we know how to present age-dependent vectors, and we can even work with an array of agent types). The answer is to register location-specific data using CreateParamVectorIndexes as follows:
k_grid = (1:K)'; % The different locations/regions we want to model
n_k = K;
Params.w_k = [w_1, w_2, ..., w_K]; % 1-by-K vector of location-dependent wages
% Maps Params.w_k to the index/values of endogenous state variable 'k'
ParamVectorIndexes = CreateParamVectorIndexes(Params, 'w_k', 'k');
Awesome. But what happens when we take it to the next (dimensional) level? In the paper, moving from one location to another is an AR process with a migration probability matrix (easy) and a migration cost matrix (a percentage of income, w_k*z_w). A suggestion for how to get both the probability and the cost in a consistent way is to create a z_grid of indexes and then pull the correct value from each of the matrices with that grid value:
% Suppose Rouwenhorst or Tauchen gives you these 5 states:
actual_z_values = [0.8, 0.9, 1.0, 1.1, 1.2];
% 1. Set the VFI Toolkit grid to be indices, not the raw values
n_z = length(actual_z_values);
z_grid = 1:n_z;
pi_z = ... % Your transition matrix from the AR process remains unchanged
% 2. Store the actual values and your kappa_m data as parameters
Param.z_val = actual_z_values;
Param.kappa_m = [0.01, 0.02, 0.03, 0.04, 0.05]; % Corresponding costs
But we cannot walk around the Kronecker product within the return function. We must make everything ultimately map to scalars, so:
% After building your 75x75 pi_joint matrix...
n_Regions = 5;
n_z = 5; % Number of income shocks
n_Joint = n_Regions^2 * n_z;
% Initialize the 1 x 75 vectors to hold the scalar values for each state
Param.z_grid_wage = zeros(1, n_Joint);
Param.z_grid_kappa = zeros(1, n_Joint);
Param.z_grid_shock = zeros(1, n_Joint);
Param.z_grid_T = zeros(1, n_Joint);
Param.z_grid_P_E = zeros(1, n_Joint); % Regional energy price index
Param.z_grid_e_bar = zeros(1, n_Joint); % Regional subsistence energy
for i = 1:n_Joint
% Unpack the state exactly once on the CPU
Pair_Idx = ceil(i / n_z);
k = mod(Pair_Idx - 1, n_Regions) + 1;
k_prime = ceil(Pair_Idx / n_Regions);
z_idx = mod(i - 1, n_z) + 1;
% Populate the scalar value for this specific point on the Markov chain
Param.z_grid_wage(i) = w_baseline(k);
Param.z_grid_kappa(i) = kappa_matrix(k, k_prime);
Param.z_grid_shock(i) = z_val_matrix(k, z_idx);
Param.z_grid_T(i) = T_matrix(k, z_idx);
Param.z_grid_P_E(i) = P_E_regional(k);
Param.z_grid_e_bar(i) = e_bar_regional(k);
end
To ensure the arrayfun engine automatically broadcasts these 1x75 vectors across the state space, we must ensure they are dimensionally aligned with the exogenous state z.
In standard VFI Toolkit models, if you define a parameter in ReturnFnParamNames and its dimensions match the corresponding grid (e.g., shaping it as a [1, 1, 75] array to match the z dimension), the toolkit expands it perfectly:
% Reshape to ensure VFIToolkit broadcasts them along the exogenous 'z' dimension
% (Assuming 'z' is the 3rd dimension in your specific FHorz setup: a, aprime, z)
Param.z_grid_wage = reshape(Param.z_grid_wage, 1, 1, n_Joint);
Param.z_grid_kappa = reshape(Param.z_grid_kappa, 1, 1, n_Joint);
% ... reshape the rest similarly
% Add them to the return function signature list
ReturnFnParamNames = {'a', 'aprime', 'z_grid_shock', 'z_grid_wage', ...
'z_grid_kappa', 'z_grid_T', 'z_grid_P_E', 'z_grid_e_bar', ...
'r', 'j', 'Param'};
Now here’s where the AI came up with something I’ve never seen: the integration of a root-finding algorithm into a return function:
function F = HouseholdReturnFn(a, aprime, shock_val, wage_val, kappa_val, T_val, P_E_val, e_bar_val, r, e_j, Param)
% All inputs are now SCALARS provided perfectly by the arrayfun expansion!
% 1. Discretionary Budget
% (Assuming e_j is handled natively by your age loop)
Income = wage_val * shock_val * e_j;
Total_Resources = Income + (1+r)*a + T_val;
% 2. Deduct Savings, Mobility Frictions, and Subsistence Energy
Subsistence_Cost = P_E_val * e_bar_val;
Mobility_Cost = kappa_val * Income;
X_tilde = Total_Resources - aprime - Mobility_Cost - Subsistence_Cost;
% 3. Mask Infeasibility
if X_tilde <= 0
F = -Inf;
return; % Fast exit for this scalar thread
end
% 4. Vectorized Newton-Raphson (Now strictly scalar math)
U = max(X_tilde, 1e-6);
tol = 1e-6;
diff = inf;
% Pre-compute scalar constants for this thread
Term_C = Param.Lambda_C * (1.0)^(1 - Param.sigma);
Term_H = Param.Lambda_H * (Param.P_H_val)^(1 - Param.sigma); % Assuming P_H is passed similarly
Term_E = Param.Lambda_E * (P_E_val)^(1 - Param.sigma);
X_term = X_tilde^(1 - Param.sigma);
pow_C = Param.epsilon_C * (1 - Param.sigma);
pow_H = Param.epsilon_H * (1 - Param.sigma);
pow_E = Param.epsilon_E * (1 - Param.sigma);
iter = 0;
while diff > tol && iter < 50
F_U = Term_C * U^(pow_C) + Term_H * U^(pow_H) + Term_E * U^(pow_E) - X_term;
dF_dU = Term_C * pow_C * U^(pow_C - 1) + Term_H * pow_H * U^(pow_H - 1) + Term_E * pow_E * U^(pow_E - 1);
U_new = U - (F_U / dF_dU);
diff = abs(U_new - U);
U = U_new;
iter = iter + 1;
end
F = U;
end
What I think it is doing is making the non-homothetic choice of consumption vs. housing vs. energy following Comin, Lashkari and Mestieri (2021) as cited when elaborating Equation 1 of the Households.
I’ve got a lot of code to stich together, but wanted to share this particular bit for comment, criticism, etc.