4. Modern architecture
The modern branch of RISE is a ground-up redesign whose distinctives are architectural, not cosmetic. The chapter is in two parts:
Part A – what is the same for every model shape (one class, one state space, one set of engines, one validation style).
Part B – what each shape contributes on top: the inputs the factory needs and the behaviours that are intrinsic to that shape.
If you only have time for one part, read Part A – it is what makes the toolbox a toolbox rather than a collection of look-alike libraries. Part B is the per-shape contract you sign at construction time and is the only place model-type-specific code lives.
4.1. Part A. Common across all shapes
4.1.1. One model class
The legacy toolbox shipped multiple user-facing classes for the same
underlying mathematics – @dsge, @rise, @rfvar,
@svar, @prfvar, @proxy_svar, @bvar_dsge, plus shared
backbones in @generic and @estimable. Each class carried its
own methods, sometimes with subtle behavior drift between siblings.
The modern toolbox has one model class. A DSGE, a reduced-form
VAR, a structural VAR with sign restrictions, a proxy SVAR, a
DSGE-VAR, and a panel VAR are all instances of the same class with
different shape data attached. The engines that solve, estimate,
filter, and forecast them have a single body each – there is no
switch on class name in the internals.
What this buys you:
A single, predictable API surface.
Cross-shape composition without translator code (a DSGE prior on a VAR is two field assignments, not a sibling class).
The same maintenance fix lands once and benefits every shape.
What this costs:
The model object is more polymorphic, so understanding its fields takes one more page of reading on day one.
4.1.2. Unified state-space
Every engine sees the model through a single state-space representation. There is no separate code path for constant-parameter linear DSGE, regime-switching DSGE, reduced-form VAR, and so on – the state space is the canonical description, and the engines have one body each.
4.1.3. Always regime-switching
A consequence of the unified state-space: every model is treated as
Markov-switching from the start. h = 1 is the trivial
non-switching case; h > 1 is a switching model. There is no
if non_switching branch in the solver, the filter, or the
estimator. This eliminates a whole class of bugs where the
constant-parameter and switching paths drifted apart.
4.1.4. No construction metadata after build
The legacy toolbox stashed user-supplied, class-specific metadata inside the model object after construction – flags, hints, and overrides that the engines later branched on.
The modern toolbox does not. Once a model is parsed and built, the canonical fields are all the engines look at; there is no “construction-time hint” carried through to runtime. Parsers populate canonical names; engines read canonical names. A model object is a value describing a state-space, not a record of the path the user took to reach it.
4.1.5. Estimate dispatches on properties, not class
In the legacy toolbox, estimate knew which class of model it was
working with and branched accordingly.
In the modern toolbox, the estimator dispatches on properties of the data and the model, not on the class:
If the data has missing observations or unobserved states, the estimator runs a Kalman filter (or the regime-conditional Kalman filter, if
h > 1).If the data are complete and the model admits a closed-form posterior moment (notably a DSGE-VAR with a conjugate prior), the estimator short-circuits to the closed form.
For nonlinear models the estimator runs the appropriate nonlinear filter.
The class is never inspected. User code that constructs a model from a new shape gets correct estimation behaviour for free, without touching estimator internals.
4.1.6. OOP only at the user boundary
Object-oriented MATLAB is used where it pays for itself: the user constructs a model with the appropriate factory, calls methods on it, and gets a typed return. Inside the engines, the code is plain MATLAB – structs, arrays, function handles. The boundary between OO and procedural is the user-facing method.
This means the engines are easy to read, easy to step through with the MATLAB debugger, and easy to vectorise. It also means the performance-sensitive code paths are not paying the OOP dispatch tax on every inner-loop call.
4.1.7. Arguments blocks systematically
Every entry point – user-facing methods, internal helpers, package
functions – declares an arguments ... end block. The blocks
specify default values, sizes, classes, and validators in one
place. Hand-rolled nargin checks, inputParser boilerplate,
and ad-hoc default assignment are absent.
Concretely:
function db = irf(m, opts)
arguments
m (1,1) rise_model
opts.irf_periods (1,1) double {mustBePositive} = 40
opts.irf_shock_list (1,:) string = []
opts.irf_regime_specific (1,1) logical = true
end
...
The block is the contract. The function body assumes its inputs are valid. Errors are raised at the boundary, in MATLAB’s standard validator messages, not deep inside the implementation.
4.1.8. Stay close to plain MATLAB
Wherever a modern MATLAB built-in does the job, the modern branch
uses the built-in. dictionary and configureDictionary
replace containers.Map. datetime and calmonths replace
custom date arithmetic. isMATLABReleaseOlderThan replaces
verLessThan. Old custom utilities – fix_point_* option
names, custom date types, hand-rolled validators – are being
retired.
4.1.9. No internal switching syntax
In the legacy toolbox, certain solve-time choices were exposed via
@-style markers in the model file – e.g. an @commitment
parameter declared alongside the other parameters but meaning
something special to the solver. In the modern toolbox these
solve-time choices are options on the solve call, not
parameters in the model file. The model file describes the model;
the solve call describes how to solve it.
Concretely, commitment vs discretion is selected via:
m = solve(m, solve_policy_type = "discretion");
There is no @commitment parameter to declare, calibrate, or
forget to set.
4.1.10. The common engine surface
Once the model is built, every shape sees the same surface of methods. The list below is exhaustive for the common engines – anything not on it is shape-specific (Part B).
Engine |
Modern method(s) |
|---|---|
Build |
|
Steady state / BGP |
|
Filtering |
|
Estimation |
|
Posterior simulation |
|
Diagnostics |
|
Forecast / IRF / decomposition |
|
Reporting / plotting |
|
4.2. Part B. What each shape contributes
Each shape is constructed by its own factory. The factory is the only place where shape-specific knowledge enters the system. Everything after construction – the entries in the table above – is the same across shapes.
4.2.1. DSGE – dsge_model
Constructed from a model file (*.rs / *.rsa / *.dsge):
m = dsge_model('nk.rs');
Specific to this shape:
Model file language (Model file language) –
@endogenous,@exogenous,@parameters,@model; Markov-switching parameters via@parameters(chain, N) nameand the implicitchain_tp_i_jtransition probabilities;@endogenous(log)for log-level variables.Steady state / BGP (Zero-th order approximation: steady state and balanced-growth path) – the recommended pattern is a separate
sstate_file(regime-agnostic, returnsnewp.<bare_name>for parameters implied by SS conditions). The@steady_state_modelblock inside the model file is also supported.Optimal policy (Optimal policy) – declared in the model file via
@optimization_problem(single or multi-player); commitment vs. discretion vs. loose commitment vs. stochastic replanning is selected at solve time, not in the model file.Optimized simple rules (Optimal (optimized) simple rules) – the OSR objective is always a runtime argument to
optimal_simple_rule; it is not declared in the model file.Perturbation order –
solve_order = 1..5; the engines share one code path across orders.Occasionally-binding constraints, time-varying transition probabilities and deterministic / quasi-deterministic solutions all live here as well; see the matching chapters under the DSGE shape.
4.2.2. Reduced-form VAR – rfvar_model
mdl = rfvar_model(endog, ...
lag_length = 4, ...
constant_term = true, ...
deterministic_vars = {});
Specific to this shape:
Lag length and deterministics –
lag_length,constant_term,deterministic_vars.Priors – VAR-coefficient priors are built by the
var_priors.*factories:var_priors.minnesota(...),var_priors.normal_wishart(...),var_priors.sims_zha(...),var_priors.independent_normal_wishart(...),var_priors.diffuse(...). They are passed throughestim_var_priors.Linear restrictions on the coefficients –
estim_linear_restrictionsaccepts strings on the expanded namesb<lag>(<row>, <col>)andc(<row>, <col>).Estimation short-circuit – with no holes in the data and an uninformative prior, the closed-form OLS / Bayesian posterior is used. With a Minnesota prior on complete data, the Sims-Zha dummy-observation closed form is used.
4.2.3. Structural VAR – svar_model
mdl = svar_model(endog, ...
lag_length = 4, ...
constant_term = true);
Specific to this shape:
Identification at the model level – a structural VAR is identified at construction by zero / linear / sign / nonlinear restrictions on the structural-form coefficients (
a0/a1/ …), wired through theestim_*options.Structural shocks –
structural_shocks,print_structural_form.The implication: there is no separate “identify a reduced-form VAR after the fact” step. Reduced-form VARs that need identification at IRF time still use the
identificationcall from Reduced-form VAR Modeling; an SVAR has already paid that cost at the model level.
4.2.4. Proxy SVAR – proxy_svar_model
prox = struct();
prox.var = 'R';
prox.eqtn = 1;
prox.coef = 'beta_mp';
prox.shock_eqtn = 'R';
mdl = proxy_svar_model(prox, endog, ...
lag_length = 4, ...
constant_term = true);
Specific to this shape:
Proxies struct – one entry per instrument, with fields
var(variable whose shock is instrumented),eqtn(the proxy equation number),coef(relevance parameter \(\beta_m\)),shock_eqtn(the structural-shock equation).Relevance + noise parameters (\(\beta_m\), \(\sigma_m\)) are estimated alongside the VAR coefficients and can be made regime-dependent.
4.2.5. Panel VAR – prfvar_model
panel = struct();
panel.members = {'US','CA','MX','BR'};
panel.homogeneity = 'dynamic';
mdl = prfvar_model(panel, endog, ...
lag_length = 4, ...
constant_term = true);
Specific to this shape:
Panel struct –
panel.members(cross-sectional units),panel.homogeneity(‘pooled’, ‘meanGroup’, ‘static’, ‘dynamic’, ‘independent’).Name expansion – internally RISE expands the endogenous list with per-unit suffixes (
GROWTH_US,GROWTH_CA, …); restrictions are written on the expanded names.Translation back to units –
translate_panel_outputun-stacks the engine output by unit.
See Panel VAR Modeling.
4.2.6. DSGE-VAR – dsge_var_model
mdl = dsge_var_model(dsge_model_obj, ...
lag_length = 4, ...
lambda = 1.0);
Specific to this shape:
DSGE backbone – the prior on the reduced-form VAR is the DSGE-implied prior; the DSGE-shaped
rise_modelis the first argument to the factory.Lambda hyperparameter – weighting between DSGE prior and data;
lambda = infrecovers the pure DSGE,lambda = 0the pure unrestricted VAR.No-holes assumption – the closed-form posterior moments require complete sample moments;
dsge_varis the one shape for which the estimator requires no missing observations.
See DSGE-VAR Modeling.
4.3. Summary
Part A is the bargain: one class, one state space, one set of engines, one validation style, one set of solve-time options. Part B is the only place where the shape of your model matters – the factory you call and the options it accepts.
Everything that used to require knowing which sibling class you were working with now requires knowing only which shape of model you are working with – and the engines handle the rest.