Calibrate Bewley models

I have some issues with the function to calibrate Bewley models available in the estimation subfolder of the vfi-toolkit.

I was planning to use it to calibrate a model with entrepreneurs I am working on. Is there any example or code that shows how to use it @robertdkirkby? Has it ever been tested?

I will test it on my model but even now my AI found some clear bugs.

Bugs in CalibrateBIHAModel.m

I was looking through CalibrateBIHAModel.m and found two issues that look like definite bugs. I have tried to keep this report narrow and only include things that seem unambiguous.

1. calibomitparamsmatrix assignment fails for non-scalar omitted calibration parameters

In the setup of calibration parameters, the code initializes

calibomitparamsmatrix = zeros(1,1);

and later, inside the omitted-parameter branch, does

tempomitparam = caliboptions.omitcalibparam.(CalibParamNames{pp});
...
calibomitparamsmatrix(1,sum(calibomitparams_counter)) = tempomitparam;

The left-hand side is a single scalar location:

calibomitparamsmatrix(1,col)

but tempomitparam can be a vector or array. The surrounding code appears to allow this: omitted entries are identified using NaN, and calibrated entries are selected by

tempparam = tempparam(isnan(tempomitparam));

So this works only when tempomitparam is scalar. If tempomitparam is non-scalar, MATLAB will error because a vector or array is being assigned into one scalar element.

A minimal reproduction of the issue is:

A = zeros(1,1);
x = [1; NaN; 3];

A(1,1) = x;

This gives a size-mismatch assignment error.

Later in the function, the code reads the omitted parameter as a column:

currparamraw = calibomitparamsmatrix(:,sum(calibomitparams_counter(1:pp)));
currparamraw(isnan(currparamraw)) = calibparamsvec(calibparamsvecindex(pp)+1:calibparamsvecindex(pp+1));
CalibParams.(CalibParamNames{pp}) = currparamraw;

So it looks like the intention was to store each omitted-parameter vector as one column of calibomitparamsmatrix.

A possible fix would be something like:

col = sum(calibomitparams_counter);
calibomitparamsmatrix(1:numel(tempomitparam),col) = tempomitparam(:);

Then, when reconstructing the parameter, one may also want to restore the original shape of tempomitparam, depending on whether the toolkit wants to preserve row/column orientation.

For example:

omitmask = caliboptions.omitcalibparam.(CalibParamNames{pp});
currparamraw = calibomitparamsmatrix(1:numel(omitmask),sum(calibomitparams_counter(1:pp)));
currparamraw = reshape(currparamraw,size(omitmask));

currparamraw(isnan(currparamraw)) = calibparamsvec(calibparamsvecindex(pp)+1:calibparamsvecindex(pp+1));
CalibParams.(CalibParamNames{pp}) = currparamraw;

2. Non-struct vector caliboptions.logmoments branch refers to undefined variables

The function calls

[targetmomentvec,usingallstats,usingautocorr,usingcrosssec,usingcustomstats, ...
    allstatmomentnames,allstatcummomentsizes,AllStats_whichstats, ...
    FnsToEvaluate_AllStats, ...
    autocorrmomentnames,autocorrcummomentsizes,AutoCorrStats_whichstats, ...
    FnsToEvaluate_AutoCorrStats, ...
    crosssecmomentnames,crossseccummomentsizes,CrossSecStats_whichstats, ...
    FnsToEvaluate_CrossSecStats, ...
    cmsmomentnames,cmscummomentsizes] = SetupTargetMoments_InfHorz(...);

So the moment-name and cumulative-size variables available in this function include

allstatmomentnames
allstatcummomentsizes
autocorrmomentnames
autocorrcummomentsizes
crosssecmomentnames
crossseccummomentsizes
cmsmomentnames
cmscummomentsizes

However, in the non-struct caliboptions.logmoments branch, the code refers to variables that do not appear to be defined anywhere in CalibrateBIHAModel.m:

acsmomentnames
allstatmomentsizes
acsmomentsizes

Specifically, the branch contains:

if length(caliboptions.logmoments)==(length(acsmomentnames)+length(allstatmomentnames))
    temp = caliboptions.logmoments;
    caliboptions.logmoments = zeros(length(targetmomentvec),1);
    cumsofar = 1;

    for mm = 1:length(temp)
        if mm <= allstatmomentsizes
            caliboptions.logmoments(cumsofar:cumsofar+allstatmomentsizes(mm)) = temp(mm);
            cumsofar = cumsofar + allstatmomentsizes(mm);
        else
            caliboptions.logmoments(cumsofar:cumsofar+acsmomentsizes(mm)) = temp(mm);
            cumsofar = cumsofar + acsmomentsizes(mm);
        end
    end

This means that if caliboptions.logmoments is non-scalar and has at least one positive entry, MATLAB will try to evaluate

length(acsmomentnames)

before it ever reaches the later case

elseif length(caliboptions.logmoments)==length(targetmomentvec)

Since acsmomentnames is not defined in the function, this branch errors immediately.

A minimal reproduction of the issue is:

caliboptions.logmoments = [1; 0; 0];

if any(caliboptions.logmoments > 0)
    if isscalar(caliboptions.logmoments)
        % not reached
    else
        if length(caliboptions.logmoments) == length(acsmomentnames)
            % acsmomentnames is undefined
        end
    end
end

A simple conservative fix would be to allow only the two unambiguous non-struct cases:

  1. scalar 0 or 1;
  2. a vector already of length length(targetmomentvec).

For example:

elseif any(caliboptions.logmoments > 0)

    if isscalar(caliboptions.logmoments)

        caliboptions.logmoments = ones(length(targetmomentvec),1);

    else

        if length(caliboptions.logmoments) == length(targetmomentvec)
            caliboptions.logmoments = caliboptions.logmoments(:);
        else
            fprintf('Relevant to following error: length(caliboptions.logmoments)=%i \n', ...
                length(caliboptions.logmoments))
            fprintf('Relevant to following error: length(targetmomentvec)=%i \n', ...
                length(targetmomentvec))
            error('You are using caliboptions.logmoments, but it must be scalar or have length equal to the number of target moments.')
        end

    end

end

This avoids the undefined variables and keeps the already-supported vector-of-target-moments case.

I think there is a more urgent issue: in my Pijoan-Mas test, gridinterplayer now does not work i.e. gives nonsensical results :frowning:

Codex fixed it, sent PR: Fix grid interpolation policy unpacking with discrete choices by aledinola · Pull Request #104 · vfitoolkit/VFIToolkit-matlab · GitHub

@robertdkirkby Let me know if you agree. This change make my Pijoan-Mas replication generate good results.

I have a suggestion to improve the outputs of CalibrateBIHAModel.m.
Now it displays results on the screen, but for long calibrations (which are the norm in macro!) you would like to save some intermediate results.
Would it be possible to modify CalibrateBIHAModel.m so that it saved a log file in txt format? I am using this workflow in my research: every time the objective function improves (i.e. goes down), record an entry in the log file with best parameter values so far and best obj value so far.

Why this helps? Because calibrating a model takes usually a lot of time and you rarely let the estimation reoutine complete naturally. As an idea, for a paper of mine where the model takes about 15 seconds to solve, one calibration takes at least 2-3 hours to converge. However, a typical minimization routine will improve significantly over maybe the first hour and then it will “stall”, that is, make only tiny improvements. So in practice you want to cut the code after, say, one hour.

Note: I have experienced this with local minimization routines, which are the ones typically used by the toolkit. For simulated annealing is different, since I have seen improving even after several hours.

Glad to share the matlab function I use for this task:

function report_estim_progress(obj_smm,Params,calibNames,model_mom,data_mom,moment_contrib,targetNames,write_to_file)
% Print the current estimation state and optionally append the best-so-far one.
%
% INPUTS
% obj_smm     : scalar objective value for the current parameter vector
% Params      : parameter struct after inserting the current guess
% calibNames  : names of calibrated parameters to display
% model_mom   : struct of model moments
% data_mom    : struct of empirical target moments
% moment_contrib : struct of per-moment objective contributions
% targetNames : names of targeted moments to display
% write_to_file : if true, append this report to estim_best_so_far.txt
%
% OUTPUTS
% none; report is printed to the command window or appended to the txt log

header_params = sprintf('%-20s %12s\n', 'Variable', 'Value');
sep_params = sprintf('%s\n', repmat('-', 1, 35));
header_targets = sprintf('%-20s %-16s %-16s %-16s\n','Moment','Model','Data','Contribution');
sep_targets = sprintf('%s\n', repmat('-', 1, 69));

if ~write_to_file
    fprintf('Objective value: %.12f\n\n',obj_smm);
    fprintf(header_params);
    fprintf(sep_params);
    for ii=1:numel(calibNames)
        value = Params.(calibNames{ii});
        for jj=1:numel(value)
            fprintf('%-20s % .16g\n',calibNames{ii},value(jj));
        end
    end

    fprintf('\n');
    fprintf(header_targets);
    fprintf(sep_targets);
    for ii=1:numel(targetNames)
        model_vals = model_mom.(targetNames{ii})(:);
        data_vals = data_mom.(targetNames{ii})(:);
        contrib_vals = moment_contrib.(targetNames{ii})(:);
        for jj=1:numel(model_vals)
            fprintf('%-20s %-16.8g %-16.8g %-16.8g\n',targetNames{ii},model_vals(jj),data_vals(jj),contrib_vals(jj));
        end
    end
    fprintf('\n');

    return
end

res_dir = Params.res_dir;
outfile = fullfile(res_dir,'estim_best_so_far.txt');
fid = fopen(outfile,'a');
if fid==-1
    warning('report_estim_progress:OpenFailed', ...
        'Could not open best-so-far log file: %s', outfile)
    return
end

fprintf(fid,'%s\n',repmat('=',1,68));
fprintf(fid,'Best-so-far estimation result\n');
timestamp_str = char(datetime('now','Format','yyyy-MM-dd HH:mm:ss'));
fprintf(fid,'Timestamp: %s\n',timestamp_str);
fprintf(fid,'Objective value: %.12f\n\n',obj_smm);
fprintf(fid,'%s',header_params);
fprintf(fid,'%s',sep_params);
for ii=1:numel(calibNames)
    value = Params.(calibNames{ii});
    for jj=1:numel(value)
        fprintf(fid,'%-20s % .16g\n',calibNames{ii},value(jj));
    end
end
fprintf(fid,'\n');
fprintf(fid,'%s',header_targets);
fprintf(fid,'%s',sep_targets);
for ii=1:numel(targetNames)
    model_vals = model_mom.(targetNames{ii})(:);
    data_vals = data_mom.(targetNames{ii})(:);
    contrib_vals = moment_contrib.(targetNames{ii})(:);
    for jj=1:numel(model_vals)
        fprintf(fid,'%-20s %-16.8g %-16.8g %-16.8g\n',targetNames{ii},model_vals(jj),data_vals(jj),contrib_vals(jj));
    end
end
fprintf(fid,'\n');

fclose(fid);

end %end function

Another suggestion/request. Now the output calibsummary of CalibrateBIHAModel is a structure but it contains only one field, objvalue. It would be helpful if it contained also the model moments/targets. They can be easily recomputed afterwards but why doing this extra step? CalibrateBIHAModel should have them in memory, right?

Would it be possible to modify CalibrateBIHAModel.m so that it saved a log file in txt format? I am using this workflow in my research: every time the objective function improves (i.e. goes down), record an entry in the log file with best parameter values so far and best obj value so far.

To be sure I understand, you do not want a list of all attempted parameter vectors with their associated objective fn value. You only want the best one so far?

1 Like

Yes. Whenever the objective function improves, you write current parameter values and objective function value to the txt. That’s the idea. I can share with you the files I am using for a project if you think they might help with the implementation.

This raises the question about where to save the log file. This could be another option hidden in caliboptions, maybe?

The PR was good but turned out the bit above it needs removing too so I have instead just pushed a file that fixes both:

and

1 Like

Great, thanks! I’ve tested on my code and it works well.

Another implementation detail: to record only the “best so far” estimates, we need to define a persistent variable. Persistent variables in matlab are more robust than globals (they stay local to the function where they are defined), and indeed they are not discouraged. This is the approach I currently use in my project.

I sent a PR: Add BIHA calibration progress report by aledinola · Pull Request #105 · vfitoolkit/VFIToolkit-matlab · GitHub

Just to explain briefly:

  • Added two more fields to caliboptions: progressreport and progressreportfilename; they are set to default values in CalibrateBIHAModel.
  • In CalibrateBIHAModel_Joint_objectivefn.m (line 241) we compute the current scalar objective value, build readable moment names, and call CalibrateBIHAModel_ProgressReport(...). Similar for the nested objective function.
  • The helper CalibrateBIHAModel_ProgressReport does the job: if currentObj<bestObj, we append the best results so far to the log file. We set bestObj as persistent and initialize it to +Inf of course.

What do you think?

I attach the calibration report file that was generated by CalibrateBIHAModel.

CalibrateBIHAModel progress report
Started: 2026-06-08 13:49:12
Only new best objective values are reported.

New best objective
Evaluation: 1
Timestamp: 2026-06-08 13:49:13
Objective: 0.005024649223500906
Parameter values:
    beta: 0.945
    sigma: 1.458
    nu: 2.833
    lambda: 0.8560000000000001
    theta: 0.64
    delta: 0.083
    r: 0.037
Model and target moments:
    CustomModelStats.corr_h_z: model 0.01217689211305187, target 0.02
    CustomModelStats.cv_h: model 0.1998336727608556, target 0.22
    CustomModelStats.H: model 0.3564179627812684, target 0.33
    CustomModelStats.K_to_Y: model 2.943083637115059, target 3
    CustomModelStats.wL_to_Y: model 0.6469328657908955, target 0.64
    CustomModelStats.I_to_Y: model 0.2442759418805499, target 0.25
GE price values:
    r: 0.037
General equilibrium residuals:
    CapitalMarket: -0.002320682790003172

New best objective
Evaluation: 2
Timestamp: 2026-06-08 13:49:15
Objective: 0.0009421654498280088
Parameter values:
    beta: 0.9453948262698868
    sigma: 1.458
    nu: 2.833
    lambda: 0.8560000000000001
    theta: 0.64
    delta: 0.083
    r: 0.037
Model and target moments:
    CustomModelStats.corr_h_z: model 0.02153280529283889, target 0.02
    CustomModelStats.cv_h: model 0.203101726702583, target 0.22
    CustomModelStats.H: model 0.3553696228726225, target 0.33
    CustomModelStats.K_to_Y: model 3.002929193222047, target 3
    CustomModelStats.wL_to_Y: model 0.6396487647203305, target 0.64
    CustomModelStats.I_to_Y: model 0.2492431230374299, target 0.25
GE price values:
    r: 0.037
General equilibrium residuals:
    CapitalMarket: 0.0001170534381692914

New best objective
Evaluation: 17
Timestamp: 2026-06-08 13:49:30
Objective: 7.479035844441271e-06
Parameter values:
    beta: 0.9454471654505545
    sigma: 1.459316037329529
    nu: 2.835059301574856
    lambda: 1.053906217134599
    theta: 0.6399484477407869
    delta: 0.08333337959770608
    r: 0.03660534921593452
Model and target moments:
    CustomModelStats.corr_h_z: model 0.0181385085798845, target 0.02
    CustomModelStats.cv_h: model 0.2195477138132528, target 0.22
    CustomModelStats.H: model 0.3316902005670289, target 0.33
    CustomModelStats.K_to_Y: model 3.000865229196578, target 3
    CustomModelStats.wL_to_Y: model 0.6400800766572171, target 0.64
    CustomModelStats.I_to_Y: model 0.2500722412661957, target 0.25
GE price values:
    r: 0.03660534921593452
General equilibrium residuals:
    CapitalMarket: -4.385112901472005e-05

New best objective
Evaluation: 25
Timestamp: 2026-06-08 13:49:38
Objective: 4.391642810729072e-06
Parameter values:
    beta: 0.9455295440806921
    sigma: 1.445001310197087
    nu: 2.861310434773296
    lambda: 1.051660199052065
    theta: 0.6400063742581237
    delta: 0.08333333338677372
    r: 0.03667182219935359
Model and target moments:
    CustomModelStats.corr_h_z: model 0.02099065591325617, target 0.02
    CustomModelStats.cv_h: model 0.2201251745549516, target 0.22
    CustomModelStats.H: model 0.3298618755181313, target 0.33
    CustomModelStats.K_to_Y: model 3.001663110261218, target 3
    CustomModelStats.wL_to_Y: model 0.6397850577786328, target 0.64
    CustomModelStats.I_to_Y: model 0.2501385926821782, target 0.25
GE price values:
    r: 0.03667182219935359
General equilibrium residuals:
    CapitalMarket: 7.376671332736534e-05

New best objective
Evaluation: 33
Timestamp: 2026-06-08 13:49:46
Objective: 4.021874038689279e-06
Parameter values:
    beta: 0.9454937012882687
    sigma: 1.448190287031664
    nu: 2.861903080378347
    lambda: 1.052378374328707
    theta: 0.6399969796728858
    delta: 0.08333333333338647
    r: 0.0366633117278634
Model and target moments:
    CustomModelStats.corr_h_z: model 0.01910213166673853, target 0.02
    CustomModelStats.cv_h: model 0.2198102954430491, target 0.22
    CustomModelStats.H: model 0.3300220327235265, target 0.33
    CustomModelStats.K_to_Y: model 2.998375881823546, target 3
    CustomModelStats.wL_to_Y: model 0.6402050474599109, target 0.64
    CustomModelStats.I_to_Y: model 0.2498646568187881, target 0.25
GE price values:
    r: 0.0366633117278634
General equilibrium residuals:
    CapitalMarket: -6.936217603098482e-05

New best objective
Evaluation: 41
Timestamp: 2026-06-08 13:49:54
Objective: 3.358294246148939e-07
Parameter values:
    beta: 0.9455059712156906
    sigma: 1.447473035226541
    nu: 2.86059077689366
    lambda: 1.052457360977862
    theta: 0.6400000717291408
    delta: 0.08333295852492338
    r: 0.03666753091632199
Model and target moments:
    CustomModelStats.corr_h_z: model 0.02000671659048116, target 0.02
    CustomModelStats.cv_h: model 0.2200055248474263, target 0.22
    CustomModelStats.H: model 0.3299763599838959, target 0.33
    CustomModelStats.K_to_Y: model 3.000530858044369, target 3
    CustomModelStats.wL_to_Y: model 0.639934837687285, target 0.64
    CustomModelStats.I_to_Y: model 0.2500431135461642, target 0.25
GE price values:
    r: 0.03666753091632199
General equilibrium residuals:
    CapitalMarket: 2.174391169820766e-05

New best objective
Evaluation: 49
Timestamp: 2026-06-08 13:50:02
Objective: 5.590842512119035e-08
Parameter values:
    beta: 0.9455030919387456
    sigma: 1.447402839215763
    nu: 2.859677233102068
    lambda: 1.052655449924112
    theta: 0.6399998124757905
    delta: 0.0833333329442448
    r: 0.03666647808097931
Model and target moments:
    CustomModelStats.corr_h_z: model 0.01999221555249907, target 0.02
    CustomModelStats.cv_h: model 0.2200223687946467, target 0.22
    CustomModelStats.H: model 0.3299941533407194, target 0.33
    CustomModelStats.K_to_Y: model 2.999784537947013, target 3
    CustomModelStats.wL_to_Y: model 0.6400264238667074, target 0.64
    CustomModelStats.I_to_Y: model 0.249982043661736, target 0.25
GE price values:
    r: 0.03666647808097931
General equilibrium residuals:
    CapitalMarket: -8.870588484699571e-06

Hi @robertdkirkby, what do you think about the log file that records estimation/calibration progress? I find it very useful for long calibration runs.