1. Extending RISE through paradigms

This chapter describes how RISE separates what a model is from how it is solved, through the notion of a paradigm. A paradigm is a self-contained computational strategy – perturbation, recursive local linearization (RLL), \(k\)-th order Taylor projection (TP), occasionally-binding-constraint (occbin) piecewise linearization, and so on – that plugs into mainland RISE through a small, fixed interface. The aim is to add advanced solution methods without modifying RISE’s core and without paradigms interfering with one another.

This chapter is primarily for developers extending RISE. The three paradigms currently targeted by this framework are:

  1. occbin – piecewise-linear, OccBin-style occasionally-binding constraints (being migrated out of mainland RISE into this framework);

  2. recursive local linearization (RLL);

  3. \(k\)-th order Taylor projection (TP).

1.1. Goals

The paradigm architecture is designed to deliver:

  • a clean separation between model structure and computational method;

  • support for multiple parameterizations of the same model;

  • extensibility to advanced methods without touching RISE core;

  • prevention of cross-talk between parameterizations;

  • strictly read-only access to RISE internals by paradigm code.

A model contains exactly one paradigm at a time, but may carry multiple parameter vectors, each producing its own solution object.

1.2. What a paradigm is

A paradigm \(\mathcal{P}\) is a computational universe that

  1. solves a parameterized model \(m(\theta)\),

  2. defines the internal representation of the solution \(S(\theta)\), and

  3. provides operators acting on that solution (e.g. a one-step forecast).

It is not the model, not a parameter vector, and not a container for multiple solutions. Formally,

\[S(\theta) = \mathcal{P}.\texttt{solve\_engine}\bigl(m(\theta),\, \texttt{regular\_solver}\bigr),\]

where \(S(\theta)\) is partly transparent to RISE (through the standard envelope below) and partly opaque (through the S.solution payload).

1.3. Responsibilities of a paradigm

A paradigm must:

  • operate on a single parameter vector at a time;

  • return a single solution object S conforming to the standard envelope;

  • provide operators derived from that solution;

  • remain unaware of parameter multiplicity;

  • treat the model object M as read-only – it may call regular_solver(M, ...) to obtain derivatives, but must never modify M nor return a modified M to RISE;

  • avoid modifying RISE core structures or global options.

A paradigm may use RISE’s baseline solver internally, use RISE’s constraint enforcement, and keep mutable internal state inside S.solution (see Mutable solution objects).

A paradigm must not store global state across parameterizations, return a modified M, or manage arrays of solutions.

1.4. The standard solution envelope

Every paradigm’s solve_engine returns a struct S with the fields below. This standard envelope lets RISE orchestrate the workflow (convergence checking, re-solve decisions, filter routing) without knowing anything about the paradigm’s internals.

Field

Type

Meaning

is_solved

[1x2 logical]

[ss_ok, dynamics_ok]: first entry, the steady state / baseline solve succeeded; second entry, the dynamics / projection solve succeeded.

warrant_resolving

logical

true if the solve failed or degraded and RISE should attempt a re-solve.

retcode

integer

0 = success (RISE’s usual retcode convention).

H

matrix or []

measurement-error covariance for the Kalman filter; [] if the paradigm provides none.

solution

handle object

opaque, paradigm-owned payload. RISE stores it but never inspects it. All paradigm-specific state lives here.

The envelope is deliberately minimal. Fields specific to perturbation theory (eigenvalues, number of solutions, Tz, …) are not required, because they are meaningless or misleading for paradigms such as RLL and TP; those paradigms keep such quantities inside S.solution.

A paradigm should start from a failed default and populate fields as each stage succeeds, so partial failures are reported correctly:

function S = make_envelope()
S.is_solved         = [false, false];
S.warrant_resolving = true;
S.retcode           = 1;
S.H                 = [];
S.solution          = [];
end

1.5. The minimum interface contract

A paradigm exposes an engines struct with these fields.

name (required)

A short string identifier, e.g. 'perturbation', 'rll', 'rtp'.

solve_engine (required)
S = solve_engine(M, regular_solver)

M is read-only: the paradigm may call regular_solver(M, ...) internally, but the solved model is used internally only and never returned. S must conform to the standard envelope, and no global or shared state may be modified.

one_step_engine (required)
one_step = one_step_engine(S)

The only input is S – the model M is not passed here, so everything the closure needs must have been captured in S.solution during solve_engine. It returns a closure

[x_t, retcode, extra...] = one_step(x_tm1, eps_t, regime)

with x_tm1 the lagged endogenous vector \(x_{t-1}\), eps_t the shock vector \(\varepsilon_t\), and regime the integer index of the current Markov regime. It returns a nonzero retcode if the step fails; extra... are optional outputs such as the local linear coefficients (k, H, G) consumed by the smoother.

filter_engine (optional)

Future extension for filtering / smoothing.

use_rise_constraint_enforcer (optional)

Logical, default true.

1.6. Mutable solution objects

For RLL and TP the effective policy function is state-dependent: it is updated at each simulation step as the economy evolves. This conflicts with RISE’s assumption that the stored solution S is static. The recommended resolution is to implement S.solution as a MATLAB handle class. Because handle objects are passed by reference, the one_step closure and RISE’s stored copy of S.solution point to the same object: when one_step updates it in place, RISE’s copy reflects the change automatically, without RISE needing to know an update occurred.

classdef SolutionHandle < handle
    properties
        % paradigm-specific fields populated by solve_engine, e.g.
        T        % TP: current polynomial coefficients
        k, H, G  % RLL: current local linear policy
        % ...
    end
end

The workflow is:

  1. solve_engine constructs a fresh SolutionHandle, populates it, and stores it as S.solution;

  2. one_step_engine(S) extracts payload = S.solution and builds a closure capturing payload by reference;

  3. at each step the closure updates payload in place; RISE’s stored S.solution reflects the updates automatically;

  4. RISE reads only the envelope fields and never the payload.

Note

Because RISE never reads S.solution directly, this pattern is safe. The only contract is that one_step(x_tm1, eps_t, regime) always returns a valid x_t and retcode, whatever the payload’s internal state.

1.7. Responsibilities of RISE

RISE manages model structure and parameter multiplicity; calls solve_engine(M, regular_solver) for each \(\theta_i\) (passing M read-only); stores each \(S_i\) in model.solution(i); and routes simulation, forecasting and filtering through the closures returned by one_step_engine (and filter_engine). The workflow is

\[\theta_i \;\rightarrow\; m(\theta_i) \;\rightarrow\; S_i = \texttt{solve\_engine}(m(\theta_i),\,\texttt{regular\_solver}) \;\rightarrow\; \text{store } S_i \;\rightarrow\; \text{simulation / filtering}.\]

RISE must not inspect or modify S.solution, assume any format inside it, or reuse a one_step closure across different parameter vectors.

1.8. Isolation, safety and persistence

To prevent cross-talk between parameterizations, each call to solve_engine must construct a fresh SolutionHandle (copying a handle copies only the reference, not the data), no paradigm may keep persistent state outside S.solution, and RISE must not reuse engines across parameter vectors unless the paradigm is stateless.

For saving models to disk, paradigms should provide serialization hooks

blob = serialize_solution(S)
S    = deserialize_solution(blob)

which RISE stores without inspecting.

1.9. Examples

1.9.1. Recursive local linearization (RLL)

  • solve_engine calls regular_solver read-only, extracts the baseline local policy \((k_0, H_0, G_0)\), stores everything in an rll.SolutionHandle, and returns the standard envelope with S.solution = payload.

  • one_step_engine(S) returns a closure that builds a zero-shock stance, re-evaluates the structural derivatives at that stance, solves the local linear system for updated \((k_t, H_t, G_t)\), updates the payload in place, and returns \(x_t\) together with \((k_t, H_t, G_t)\) for the smoother.

1.9.2. \(k\)-th order Taylor projection (TP)

  • solve_engine calls regular_solver read-only, uses the perturbation solution as a warm start, runs rtp.solve to obtain \(T^* = \{T_1^*,\dots,T_h^*\}\), and stores \(T^*\), the topology and get_F in an rtp.SolutionHandle.

  • one_step_engine(S) returns a closure that builds the expansion point \(z_{\text{anchor}} = (p_{t-1}, b_{t-1}, \sigma{=}0, \varepsilon{=}0)\), re-solves rtp.solve around it (warm-started from the payload), updates the payload in place, and evaluates

    \[x_t = \sum_{q=0}^{K} T^*_{\text{regime}}\{q{+}1\}\, \bigl(z_t - z_{\text{anchor}}\bigr)^{\otimes q},\]

    with \(z_t = (p_{t-1}, b_{t-1}, \sigma{=}1, \varepsilon_t)\).

1.10. Design principles

  • A model has exactly one paradigm.

  • A model may have multiple parameterizations; each has one solution S.

  • M is always read-only inside a paradigm.

  • S.solution is always opaque to RISE.

  • The envelope gives RISE just enough to orchestrate workflows.

  • Mutable simulation-time state lives inside S.solution via handle semantics.

  • Paradigm-specific solution concepts (eigenvalues, number of solutions, Taylor coefficients) live inside S.solution, not in the envelope.