4. Simulation plans

A simulation plan is the object that collects every choice you might want to impose on a forward path – a conditioning value on an endogenous variable at a given date, a pinned or activated shock, whether a constraint is anticipated or hits as a surprise, an algebraic identity that must hold over a window – into a single object the toolbox’s simulators consume.

The plan is the universal interface for controlled simulations in RISE:

  • simulate drives the perturbation solution forward through the plan.

  • forecast produces conditional forecasts (with or without shock uncertainty) from the plan.

  • perfect_foresight solves the full nonlinear model under the plan as a deterministic boundary-value problem.

The same plan flows into all three; what changes between them is the solver, not the plan. This chapter is the reference for building the plan. The next chapter, Forecasting and simulation, is the reference for the solvers that consume it.

4.1. Why a simulation plan

A plan separates what you want enforced from how the solver enforces it. Two payoffs:

  1. One vocabulary for stochastic and deterministic contexts. A DSGE solved by perturbation accepts anticipated shocks natively, so a plan that announces a shock \(k\) periods ahead – or springs it as a surprise at \(t\) – works identically through simulate and through perfect_foresight. You do not switch APIs to switch contexts.

  2. Hard endogenous constraints with an explicit identification audit. A plan that pins \(y_t\) at a chosen value requires a free shock at \(t\) to enforce it. Because the plan stores the shock matrix explicitly – pinned entries are numeric, free entries are NaN – the identification condition is checkable at construction time, before the solver is ever called.

Plans also accept algebraic constraints (a linear combination of variables fixed over a window), bounds (soft inequality constraints, where the corresponding feature is supported), and multi-page anticipation structures (the same shock at the same date entered with several lead times).

4.2. Construction

The constructor takes the model, a horizon, an anticipation horizon, and (optionally) a shock-initialization rule:

sp = simplan(m, [start_date, end_date], anticipationHorizon, ...
             'shockInit', initRule);

The four arguments:

  • m – a solved (or at least parsed) rise_model object, used to read the endogenous, exogenous and parameter lists and to anchor dates.

  • [start_date, end_date] – the horizon. start_date is the last date of history (the point from which the simulation continues forward); end_date is the last simulation date. Dates may be integers ([1, 40]) or RISE date objects ([rq(2020,1), rq(2030,4)]).

  • anticipationHorizon – the maximum number of periods ahead a shock can be announced. 1 means contemporaneous only; k allows announcements up to \(k-1\) periods ahead.

  • 'shockInit', initRule – the default value for every shock, at every date, before any append calls. See below.

4.2.1. The shockInit rule

shockInit chooses what every shock entry starts as:

Value

Effect

'zero' (default)

Every shock pinned at 0. append lifts entries individually – either to a numeric value (still pinned) or to NaN (handed to the solver).

'randn'

Every shock drawn from a standard normal. Reproduce by seeding the RNG before construction.

numeric scalar

Every shock pinned at the scalar.

struct

Per-shock rule, where each field name is a shock and the value is any of the rules above.

Example:

spec         = struct();
spec.EZ      = 'randn';
spec.ER      = 'zero';
spec.EETA    = 'zero';
rng(42)
sp = simplan(m, [rq(2020,1), rq(2030,4)], 1, 'shockInit', spec);

shockInit only sets defaults; later append calls overwrite the entries they touch.

4.3. Appending conditions

append is the workhorse. Three forms cover almost everything.

4.3.1. Single variable, vector of values

Pin one variable over a window:

target_dates = rq(2022,1):rq(2022,4);
sp = simplan(m, [rq(2020,1), rq(2030,4)], 1, 'shockInit', 'zero');
sp = append(sp, 'PAI', target_dates, 1.005 * ones(4,1));

Numeric values pin the variable. A single scalar is replicated across the date range:

sp = append(sp, 'R', rq(2023,1):rq(2023,4), 1.01);

4.3.2. Multiple variables, struct form

When several variables share the same dates:

S       = struct();
S.PAI   = 1.005 * ones(4,1);
S.R     = 1.010 * ones(4,1);

sp = append(sp, S, rq(2022,1):rq(2022,4));

Scalar fields in the struct are replicated across the dates.

4.3.3. Algebraic constraint

A linear combination of variables can be fixed over a window through a constraint-string and a fixing shock:

sp = append(sp, ...
    {'R{t} - 1.005 * PAI{t} = 0', 'ER'}, ...
    rq(2022,1):rq(2022,4), ...
    NaN);

The constraint string uses the same syntax as model equations (var{t} for the contemporaneous variable). The fixing shock (ER here) is the shock the solver is allowed to use to enforce the constraint – it must be activated (NaN) at the same dates.

4.4. Boundary conditions: initval, endval, histval

Besides append, a plan exposes Dynare-style setters for the boundary conditions of a perfect-foresight problem. They are convenient when porting a Dynare model, or when you think in terms of initial / terminal / historical blocks rather than per-date pins.

initval(plan, steady, C)

Set the initial conditions (and, in the absence of other blocks, the start values and the terminal condition). steady is a logical: true treats C as a guess and solves the steady state from it; false uses the values as given. C is the conditions – {name, value} pairs, a cell of 'name=value;' strings, or a single string:

plan = initval(plan, false, {'k', 12; 'c', 1.1});
plan = initval(plan, false, 'k=12; c=1.1;');
endval(plan, steady, C)

Set the terminal conditions, with the same steady flag and the same C formats. Use it for a permanent-change experiment whose terminal steady state differs from the initial one:

plan = endval(plan, true, {'tau', 0.25});   % solve the terminal SS at tau = 0.25
histval(plan, C)

Set the historical (lagged) initial conditions, with the lag written into the name:

plan = histval(plan, {'k(-1)', 12; 'y(0)', 1});
from_history(m, db, history_end_date, nsteps)

Build a plan whose initial conditions are read from a historical database at the jump-off date. For every declared endogenous variable present in db, the jump-off value and its lags are written into the plan; the simulation then projects nsteps periods forward:

plan = simplan.from_history(m, db, rq(2021,4), 40);

These setters and append combine freely: a common pattern is histval (or from_history) for the lagged initial state plus append for known future shocks. For commitment / optimal-policy models, whether the initial policy multipliers start at the steady state or at zero is a separate choice – see simul_optimal_policy_start in Deterministic and quasi-deterministic solutions.

4.5. Anticipation: pages

Each shock entry sits on a page indexed by lead time:

  • Page 1 – the shock is realised at the date but unanticipated before then.

  • Page k (for k <= anticipationHorizon) – the shock is announced at the date but realised \(k-1\) periods later; agents see it that many periods in advance.

Pages let surprises and announcements coexist in the same plan. An anticipated monetary shock at rq(2023,1) announced two quarters ahead:

sp = append(sp, 'ER', rq(2023,1), 0.01, 3);

(page 3 because the announcement is \(k-1 = 2\) periods ahead). The same shock and date entered on page 1 would be an unanticipated realization. The two coexist if you write both.

Sequences of announcements with different lead times are common:

shock_dates  = rq(2024,1):rq(2024,4);
shock_values = [0.02; 0.015; 0.01; 0.005];

for lead = 0:3
    sp = append(sp, 'EZ', shock_dates(lead+1), ...
                shock_values(lead+1), lead+1);
end

Each entry is the same shock at a different date, announced a different number of periods ahead.

4.6. Hard endogenous conditions and the identification rule

Pinning a variable at a chosen value is a hard endogenous condition. The solver enforces it by choosing values for free shocks at the same dates. Two ingredients are required:

  1. The variable is appended with a numeric value at the condition dates.

  2. Enough shocks are activated at the condition dates by being set to NaN.

At every date \(t\) in the conditioning window:

\[\#\{\text{NaN shock entries at } t\} \;\geq\; \#\{\text{endogenous constraints at } t\}.\]

If the inequality fails, the solver raises an identification error (active constraints exceed free shocks).

4.6.1. The recipe

A non-trivial conditional forecast follows four steps:

  1. Construct the plan with shockInit = 'zero'.

  2. Append background shocks at fixed values (these stay pinned and shape the baseline scenario).

  3. Activate the shocks the solver may use, by appending them at NaN at the conditioning dates.

  4. Append the endogenous conditions (numeric values at those same dates).

A worked single-variable example – a faster disinflation than the Taylor rule alone would deliver, after a cost-push shock:

horizon    = [rq(2020,1), rq(2028,1)];
cond_dates = rq(2021,1):rq(2022,4);   % 8-quarter window

sp = simplan(m, horizon, 1, 'shockInit', 'zero');

% Step 2: background -- cost-push shock at the start of history
sp = append(sp, 'EETA', rq(2020,2), 2.0);

% Step 3: activate the monetary shock at the conditioning dates
sp = append(sp, 'ER',   cond_dates, NaN);

% Step 4: pin inflation to the target path
PAI_target = linspace(PAI_uncon_peak, PAI_ss, numel(cond_dates)).';
sp = append(sp, 'PAI',  cond_dates, PAI_target);

At each date in cond_dates there is exactly one constraint (PAI) and exactly one NaN shock (ER): the system is exactly identified and the solver backs out the ER path that makes inflation follow PAI_target.

Two-variable conditioning is the same pattern with two activated shocks:

sp = simplan(m, horizon, 1, 'shockInit', 'zero');
sp = append(sp, 'EETA', rq(2020,2), 2.0);    % background
sp = append(sp, 'ER',   cond_dates, NaN);    % activate
sp = append(sp, 'EETA', cond_dates, NaN);    % activate
sp = append(sp, 'PAI',  cond_dates, PAI_target);
sp = append(sp, 'R',    cond_dates, R_peg);

Two constraints, two free shocks – exactly identified.

Note

Appending EETA = NaN at cond_dates overrides the zero that shockInit placed there. The earlier background entry EETA = 2.0 at rq(2020,2) is untouched – only the cond_dates entries are overwritten.

4.6.2. Soft conditions

A soft condition restricts a variable to a band instead of pinning it to a single value. You append a two-element [lo hi] value where a hard condition would take a scalar:

sp = append(sp, {'Y', cond_dates, [-0.5  0.5]});   % a band
sp = append(sp, {'R', cond_dates, [ 0.0  NaN]});   % one-sided (a floor)

A scalar is a hard point; a [lo hi] pair is a band. An open side is written NaN[lo NaN] is a floor and [NaN hi] a ceiling. The plan recognizes a band automatically:

is_soft(sp)      % true when the plan carries at least one (non-degenerate) band

A degenerate band [v v] is just the hard point v – the hard condition is the zero-width limit of the soft one.

A band is still an endogenous condition, so the identification rule above applies unchanged: at least as many shocks must be freed (set to NaN) at each conditioning date as there are conditioned variables. The convenience method free_shocks does the freeing in bulk:

sp = free_shocks(sp, 'EPS_D', cond_dates);   % free one shock
sp = free_shocks(sp, 'all',   cond_dates);   % free every shock --
                                             % recovers the classic
                                             % minimum-norm conditioning

How a band is consumed – a simulated distribution (a fan chart) or a single conditional mean – is chosen on the solver call through the simul_soft_conditioning option, documented in Forecasting and simulation. A plan that carries no band is an ordinary hard-conditioning plan and is solved exactly as described above.

4.7. Inspecting a plan

Three methods help you check what is in a plan:

  • disp(sp) – a formatted summary: the horizon, the number of constrained endogenous and exogenous entries, the pages in use.

  • details(sp) – the raw property dump, useful when something in disp does not match what you expected.

  • query(sp, name, dates, pages) – inspect specific entries.

query examples:

query(sp, 'PAI')                       % all conditioned PAI dates
query(sp, 'PAI', rq(2022,2))           % a specific date
query(sp, 'ER',  rq(2023,1))           % a shock across all pages
query(sp, 'ER',  rq(2023,1), 3)        % a specific page

query is the right tool to verify the identification rule before calling the solver – count the NaN entries at each condition date and compare to the number of pinned endogenous constraints.

4.8. Consumption: the universal interface

Once a plan is built, the same plan drives every consumer. The plan is passed through the simul_historical_data option:

res_sim  = simulate         (m, simul_historical_data = sp);
res_fkst = forecast         (m, simul_historical_data = sp, ...
                                forecast_nsteps = H);
res_pf   = perfect_foresight(m, simul_historical_data = sp);

The three solvers differ in what they assume about the model solution and the shock structure, not in how they read the plan:

Solver

Solution it uses

When to prefer it

simulate

The perturbation solution (linear or higher-order) returned by solve.

Fast. Accurate near steady state. The default for stochastic conditional simulations.

forecast

Same perturbation solution, with options for shock uncertainty (forecast_shock_uncertainty) and the hard/soft-condition routing.

When you want a true forecast – shocks drawn past the conditioning window, fan charts, the conditional-forecast output structure.

perfect_foresight

The full nonlinear model solved as a boundary-value problem over the entire horizon.

More accurate for large or persistent deviations; honours occasionally-binding constraints exactly; for deterministic scenarios.

The identification rule is solver-independent: a plan that is under-identified for one solver is under-identified for all three. This is by design – the plan separates the problem statement from the solution method.

4.9. Scope: shapes other than DSGE

The simulation-plan object is built around the canonical fields of the rise_model class and therefore works for every factory that returns one. The construction interface (horizons, pages, append patterns, the identification rule) is identical across shapes.

Each shape’s chapter under Model shapes documents the consumer-side options that are specific to that shape (e.g. structural-shock identification under SVAR conditioning).

4.10. See also