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

set, get, solve

Steady state / BGP

solve (with sstate_file), print_steady_state

Filtering

filter (Kalman / regime-conditional / nonlinear, picked by data + model properties)

Estimation

estimate, pull_objective, log_prior_density, log_posterior_kernel

Posterior simulation

rsamplers.rwmh, rsamplers.imh, rsamplers.apt, rsamplers.slice, rsamplers.usrsmplr + sample

Diagnostics

mcmc (constructed from draws), mdd

Forecast / IRF / decomposition

forecast, irf, variance_decomposition, historical_decomposition, simulate

Reporting / plotting

rnotes (and the rprt thin subclass), fanchart / plot_fanchart, quick_irfs, quick_plots, plot_probabilities, plot_decomp

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) name and the implicit chain_tp_i_j transition 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, returns newp.<bare_name> for parameters implied by SS conditions). The @steady_state_model block 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 ordersolve_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 deterministicslag_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 through estim_var_priors.

  • Linear restrictions on the coefficientsestim_linear_restrictions accepts strings on the expanded names b<lag>(<row>, <col>) and c(<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.

See Reduced-form VAR Modeling.

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 the estim_* options.

  • Structural shocksstructural_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 identification call from Reduced-form VAR Modeling; an SVAR has already paid that cost at the model level.

See Structural VAR Modeling.

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.

See Proxy (instrumental) SVAR Modeling.

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 structpanel.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 unitstranslate_panel_output un-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_model is the first argument to the factory.

  • Lambda hyperparameter – weighting between DSGE prior and data; lambda = inf recovers the pure DSGE, lambda = 0 the pure unrestricted VAR.

  • No-holes assumption – the closed-form posterior moments require complete sample moments; dsge_var is 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.