3. What’s new
This page records user-visible changes to the modern RISE Toolbox – new features, behaviour changes, and things you may need to know when upgrading. Newest entries go on top.
Note
This page covers the modern toolbox only. The legacy toolbox
(RISE_Tbx_Beta) has its own release notes in rise-stable-docs;
the two are never mirrored into each other.
3.1. Unreleased
Fixed: an observed driver of the transition probabilities in a model with occasionally-binding constraints. Filtering such a model stopped with an index error: the constraint engine evaluated the transition matrix without the driver’s value. It now receives it, so time-varying transition probabilities driven by data and constraints such as the zero lower bound can be filtered and estimated together.
Fixed: the first period of the constrained filter.
immupdated through the constrained step from inputs set at the end of each period for the next, and nothing set them for the first observed period: its likelihood ignored the constraints. They are now set from the initial distribution whenever it is the stationary one, so the first period is treated like any other. Models whose first period is far from their constraints are unchanged bit for bit; an exact diffuse start, or an initial state given as the first prediction, keep today’s update.Changed: the steady state of the lead of a growing variable moves with the trend. In a model with a balanced growth path,
stst(V{+1})written in a dynamic equation was the value of the path at the expansion date, a constant: a hand-written anchor such asstst(C{+1})+mcd*(C{+1}-stst(C{+1}))then had no balanced growth path formcd < 1and the solution drifted without shocks. It is now the moving reference of@cognitive_discount: exogenous unit-root trends at their realized value, the other trends at their previous value grown at trend. For the log price level withp = p{-1} + pi,stst(p{+1})+m*(p{+1}-stst(p{+1}))isp + pis + m*(pi{+1} - pis). Models without trends, stationary variables andstst()of current values or lags are unchanged. See Steady states of growing variables in the dynamic equations.Fixed: ``’bm’`` on the planner-discount knife-edge. The solve failed (retcode 30) because the anchor condition has a continuum there; the solution is now the limit of those at nearby planner discounts. See Optimal policy.
Fixed: the smoothed shocks of the first period. They were exactly zero: the filter gave the shocks of the first period an initial covariance independent of the first prediction of the state, which they move. They are now the exact ones. On fs2000, RISE’s smoothed shocks match Dynare’s in every period; they differed by 6e-4 in the first. The likelihood and the filtered and smoothed variables do not change.
Changed: the known-regimes filter (``myKnownRegimesFilter``) takes the regime of each period, and refuses what it cannot filter. In
filter(m, kf_user_algo = {@myKnownRegimesFilter, regs}),regs(t)is the regime of period t: the regime of the solution that produces period t, whose standard deviations the shocks of period t have. The filter used to apply each regime one period late; a path shifted to compensate must now be passed as is. The constant of each period is that of its regime (models whose regimes have different steady states were filtered with the constants of the wrong regimes). The initial state and covariance are those of the regime of the first period instead of an ergodic average;kf_user_initoverrides them. Constraints (unlesssimul_disable_constraints), news shocks,solve_order > 1and deterministic exogenous variables raise errors with identifiersrise:knownRegimes:*. With shock standard deviations written as switching parameters the filter is the heteroskedastic filter: see Known regime paths and heteroskedastic shocks.Changed: the auxiliary equations of an ``@outside_optimization`` block are exempt with it. The equation RISE adds for a lead or lag beyond one inside a block (
LEAD_1_B{t} = B{t+1}for aB{t+2}) was a constraint of every player, with a multiplier zero along the solution, and could raise the feedback warning. It now belongs to the block, in both forms: the exempt players do not include it in their system and take its variable as given (no derivative with respect to it). The solutions are unchanged; the multipliers on those rows are gone. An auxiliary that an equation outside the block shares stays a constraint. See Optimal policy.Changed: one smoother for the regime-switching filters, in which identities written in the model hold.
imm,imm_o,gpbn(every history length),immnandcrskfcnow smooth pairs of linked regime histories against each pair’s own prediction, with Kim’s posterior weights. An identity the model states in every regime (a level and its growth rate) now holds in the smoothed path to rounding; before, it was off by up to about 1e-2. Likelihoods and filtered series are unchanged; all smoothed output of switching models changes.crskfc’s default smoother is now this one. See Filtering.Fixed: on the planner-discount knife-edge, the commitment solution of a regime-switching model is the limit too, rest points included. The continuation to the limit of the planner’s discount started only when the solver reported unit-circle eigenvalues, which the switching solvers never do. It now finds the unit roots in each regime’s solved transition of the state variables. Under
{'m','scl'}and'bm'the expansion points are extrapolated with the decision rules. On a switching tracker model the reporting variable’s multiplier is now zero, and the solution equals the model without it. Constant-parameter models, and switching models off the knife-edge, are unchanged. See Optimal policy.New: ``@outside_optimization(home){B_F}``, a block that exempts only the players it names. The parentheses, refused until now, name the players that are not constrained by the block’s equations and take the variables in the braces as given; the other players keep the equations as constraints. The exempt player has no multiplier on those rows and no first-order condition with respect to the variables it takes as given: the game is the one written with the equations in
@modeland the variables left out of the exempt player’s objective, under commitment, discretion and loose commitment, in Nash and leader-follower games and in@stategames. A block naming every player is the every-player form@outside_optimization{W}. The linear-quadratic solvers still refuse both forms. See Optimal policy.Changed: a constrained update that cycles between two branches takes the one consistent branch, and otherwise fails at once. Where a constraint binds,
immiterates its update through the constrained step. When the update kept alternating between two branches of the constraints, it failed after 20 iterations (retcode 314). It now checks each branch’s update against the constraints. When exactly one is consistent, the update takes it, and the period is listed in the filter’s output fieldresolved_cycle_updates. When neither or both are, the filter fails with retcode 314 at once. Wherever the update settled before, likelihoods and retcodes are unchanged bit for bit.Faster: filtering under constraints. Three overheads of the constraint engine are gone: the
max()/min()of a model’s constraints no longer go through name-value parsing, the projection onto the constraints no longer throws and catches an error to look up its tolerance, and a projection whose Gauss-Newton loop only returns to the same points stops at once instead of at its step cap. Likelihoods are unchanged bit for bit. On a switching NK-HANK with a ZLB and borrowing limits, an evaluation with the constraints enforced takes about 45% less time.Fixed: ``utils.math_comp.max(A)`` and ``utils.math_comp.min(A)`` with one argument errored. They now reduce as
max(A,[],'ComparisonMethod','real').Fixed: discretion under automatic differentiation. With
'solve_derivatives_type','automatic', next period’s discretionary policy entered the first-order conditions as a constant: its derivatives were lost, and order-2 solutions differed from the symbolic ones. Automatic and symbolic derivatives now give the same solution.Fixed: models built at ``max_deriv_order`` 0. Building a model without symbolic derivatives, to solve it with automatic derivatives, failed with an index out of bounds. It now builds and solves.
Changed: a plan and ``simul_regime`` that disagree about a regime are an error.
simul_regimeused to overwrite, silently, the regimes a plan fixes (append(plan, 'regime', t, r)). Where the two give different regimes for the same period,simulatenow stops withRISE:simulate:regimeConflict, naming the period; where only one gives a regime, or they agree, it is used. The jump-off date is not checked. See Forecasting and simulation.Changed: a constrained update that fails outright fails the filter (retcode 314). Where a constraint binds, the filter updates through the constrained step; when that update failed it fell back silently to the Kalman update, so the likelihood was not the constrained model’s. It now stops with retcode 314 (
CONSTRAINED_UPDATE_FAILED), which an estimation treats as an inadmissible draw. An observed variable held at its bound and an ill-conditioned branch keep the Kalman update, as before. On the test models no update fails outright, and likelihoods are unchanged. See Filtering.Fixed: in ``@transition_functions`` a log variable means its level, as in ``@model``. The transition matrix read a log variable as its log while the reported series (
zlb_tp_1_2) read its level, so the two disagreed, and a transition function with a negative power of a log variable divided by zero at the steady state. Every block now reads a log variable as its level. A model written for the old reading must be rewritten in levels (exp(theta*B)that stood forB^thetabecomesB^theta). See Time-varying transition probabilities.Fixed: under constraints, the regime-switching filters predict each regime with that regime’s solution. For each regime of the next period, the constrained prediction was taken in the most likely successor of that regime instead: the same regime when the transition probabilities are constant and persistent, often another one when they vary with the state or a regime stays with probability below 0.5. The prediction then disagreed with the covariance and the update of its branch, and the update through the constrained step fell back to the Kalman update (35 of 78 periods on a test HANK with time-varying probabilities; none now, and ten times faster). Constrained likelihoods of such models can change; models with constant, persistent probabilities are unchanged, bit for bit. Applies to every regime-switching filter, x-at-risk and the reduced models’ filter. See Filtering.
Changed: Taylor projection takes the expectation over next period’s shocks. The
rtpparadigm set next period’s shock to zero, so its solution had no risk correction at any order. The projection equations now hold in expectation (rtp.shock_nodes, a rule exact at the order): at the steady-state anchor order-2 Taylor projection is order-2 perturbation, and re-anchored it carries the same precautionary drift. Under regime switching the regimes’ policies now differ by their volatilities.'expectation','none'restores the old, certainty-equivalent projection; order 1 is unchanged. See Extending RISE through paradigms.Changed: under the OccBin paradigm a constrained variable lands on its bound when the binding branch is nonlinear in it. The constant of a binding regime is taken where its constrained variables sit on their bounds, so a rule
log(1+R) = gam*log(...)givesR = 0at the zero lower bound rather than the tangent value at the steady state. The new option'anchor_at_bound',falserestores the classical OccBin constant. See Occasionally-binding constraints and the tutorialModelShapes/occbin_bound.New: equations outside the optimization. An
@outside_optimization{W}block holds equations that are solved with the model but are not constraints of the optimal-policy players, such as a welfare tracker kept for reporting: they carry no multiplier, and the variables in the braces, one per equation, are taken as given by the players. The policy problem is then the one without those equations. A variable taken as given that enters what a player optimizes is reported (RISE:outside_optimization:feedback), or refused with'outside_optimization_feedback','error'. Exempting only some players and the linear-quadratic solvers are not available yet. See Optimal policy (Equations outside the optimization) and the tutorialOptimalPolicy/OutsideOptimization.Changed: the multiplier charts take the exact trend exponents of the Lagrange multipliers; one numeraire per independent trend. Under
geometric_multiplierswithmultiplier_charts, the exponents of the multipliers’ trends come from the symmetry of the static system rather than from the solved growth factors, so exponents such as -1 or 0 are exact and zero or negative multipliers get theirs from their first-order conditions (model_data.diagnostics.multiplier_trends).multiplier_numeraireaccepts a cellstr, one trending variable per independent trend, such as technology and the price level. The cases the numeraires do not span, multipliers that do not grow as a power law, and models where the exact route is declined are reported with warnings (rise:multiplier_charts:notSpanned,nonGeometric,exactDeclined). See Optimal policy and the tutorialOptimalPolicy/NonstationaryRamsey(howto_multiple_trends).Fixed: under discretion the mixed derivatives of next period’s policy functions counted several times. The first-order conditions of discretion and of stochastic replanning carry the derivatives of next period’s policy; a mixed second derivative entered them twice (a mixed third one three or six times). Solutions at order 2 and above change wherever the policy functions take two inputs or more; first-order solutions do not. On the quasi-hyperbolic consumer of Krusell and Smith (2003), whose Markov-perfect equilibrium has a closed form, an order-2 solution with a mixed derivative in the policy functions now matches the closed form to 5e-8, where its first-order coefficients were off by up to 0.5.
sstate_discr_policy_initnow sets the initial level and first derivatives of the policy functions (it had no effect). See Optimal policy.Changed: every first-order solve goes through the model’s configured solver; the linear-quadratic optimal-policy solvers state their own limits. The self-consistent linearization solved the first orders of its fixed point with the general solver and refused
loose_commitmentandstochastic_replanningby name. It now uses the solver the model is configured with, whichever it is, assolvedoes (rise.engine.dsge_tools.solve.configured_solver). Each solver applies its own requirements: the linear-quadratic solvers need the lead derivatives of a pair of regimes, divided by the transition probability, to depend on the next regime only, and stop withRISE:looseCommitment:leadBlockDependsOnPreviousRegime(orRISE:stochasticReplanning:...) when the matrices they are given do not have that structure; above order 1 they stop withRISE:looseCommitment:firstOrderOnly(orRISE:stochasticReplanning:firstOrderOnly). On a linear-quadratic planner where the structure holds, loose commitment under{'m','scl'}gives the general solver’s answer under{'m','scl'}. Nothing changes under'm'. See Optimal policy.Changed: the self-consistent linearization and the Barthélemy-Marx anchors solve optimal-policy models.
{'m','scl'}and'bm'were refused on any model with an@optimization_problem. A perturbation scheme does not depend on where the equations come from: the first-order conditions are now re-anchored like any other equations, under commitment, under discretion and wherever the commitment parameter switches, at every order, and with a common steady state both types return plain'm'. Under discretion the policy functions of next period’s regime, which the first-order conditions carry, are evaluated where the self-consistent linearization puts the leads, at the transition points; under'm'nothing changes. See Perturbation types for regime-switching models and Optimal policy.Fixed: Taylor projection’s analytical Jacobian follows the lead point; a re-anchored root far from the policy is rejected. The model’s derivatives are evaluated where the next period’s policy lands, which every coefficient moves;
rtp.jacobian_analyticalheld them fixed (2.3% off the numerical Jacobian, and order-3 Newton stalled where a root exists). With the exact Jacobian, Newton beyond a fold of the order-2 equations in levels converged to absurd roots, so a converged re-solve is now kept only if today’s policy at the new state stays withinreanchor_max_jump(new option ofrtp.paradigm, default 0.5) of the last converged policy there; otherwise the step warns and reuses the last converged policy. In levels, order 3 now converges at every step from 0.9 to 0.3 of the steady-state capital. Written in logs (@log_vars), the growth model keeps the root of its projection equations everywhere, and order-2 Taylor projection is 40 to 450 times more accurate than order-2 perturbation in logs. Write models in logs for Taylor projection far from the steady state. See Extending RISE through paradigms.Fixed: Taylor projection differentiates the next period’s policy where the state lands. The projection residual used the next period’s policy and its derivatives at that policy’s own anchor, not where the state lands one period later; the two coincide only at a steady-state anchor of a constant-parameter model. Re-anchored order-2 solutions were therefore less accurate than order-2 perturbation near the steady state (on a growth model started at 90% and 70% of its steady-state capital, largest consumption errors of 0.060% and 0.536% of the steady state against 0.006% and 0.188%); they are now more accurate (0.0033% and 0.138%). Order 3 is as accurate as order-3 perturbation or better from 90% down to 30%. From half the steady-state capital or less, the order-2 projection equations have no root near the policy: those re-solves warn and reuse the last converged policy, and order 3 is the order to use. See Extending RISE through paradigms.
Changed: the RLL paradigm solves each local system with QZ and keeps its own constant by default. The defaults of
rll.paradigmare now'solver', 'rise_qz'(was'fi') and'use_steady_state', false(wastrue); both old values remain options. Functional iteration, capped at 100 iterations, failed at every re-linearization of the growth model far from the steady state, so RLL silently equaled first order apart from one warning; withuse_steady_statetrue the step applied the local slopes around the global steady state, which was less accurate than first order. From capital at 0.9, 0.7, 0.5, 0.4 and 0.3 times its steady state, the largest error in consumption against perfect foresight (% of the steady state) is now 0.07, 0.73, 2.25, 3.28 and 4.27, against 0.11, 1.16, 3.76, 5.94 and 9.05 at first order. At an occasionally binding bound the local system has no unique stable solution, so RLL keeps the previous local solution there, as before. See Extending RISE through paradigms.Fixed: each simulation under the RLL paradigm starts from the solved local policy.
rll.paradigmupdated its stored solution in place, so a simulation started from the local policy the previous one ended with and the same simulation run twice could give two paths. Each simulation now works on its own copy, as underrtp.paradigm. See Extending RISE through paradigms.Changed: a lead that a lag multiplies in a constraint of an optimal policy problem gets its own expectation variable. In
b{t} = b{t-1}/betta*(1+0.1*pie{t+1}) + ...the first-order condition forbwould carrypie{t+2}. RISE now givespie{t+1}the variableLEAD_1_piebefore it derives the conditions, as ifEPIE{t} = pie{t+1}had been declared in the model. Solutions under commitment are unchanged up to terms of higher order; under discretion and stochastic replanning the solution is now that of the expectation form, which solves much faster. Terms in which a lag and a lead interact nonlinearly are left as written. See Optimal policy (“A lead that a lag multiplies”).Fixed: re-anchored Taylor projection from a state far from the steady state. Under
rtp.paradigm(re-anchoring is the default), a growth model started at half its steady-state capital took a path on which consumption jumped up and capital ran down: the first re-solve started from the steady-state policy and converged to another root of the projection equations. Each re-solve now starts from the current policy evaluated at the new state, and only the states of the date-0 vector matter. The solved model also kept the anchors of the last simulation, so running the same simulation again could give another path; every simulation now starts from the solved polynomial. See Extending RISE through paradigms.Fixed: the heterogeneous-agent steady-state engine no longer runs away. On a Huggett household with a borrowing limit of -2, the Anderson-accelerated time iteration of
rise.engine.dsge_tools.sstate.het.enginecould throw the policy to absurd values (steps of 4e35) and then sweep slowly without end. A mixed step much longer than the plain one is no longer taken, a sweep whose step blows up restarts from the best iterate, and repeated blow-ups stop the solve with the warninghet:engine:diverged. See Heterogeneous agents (HANK).Fixed: a plan with free shocks runs over its whole span on a regime-switching model. On a regime-switching model, a plan whose shocks are free (
NaN) holds nothing after the jump-off, the regimes being left to the chain, and its database lost those dates:simulatereturned the jump-off alone, and a conditioned plan stopped after its last condition. The plan’s database now keeps every date. Single-regime models were not affected. See Simulation plans.Fixed: simulate honors ``simul_history_end_date`` with a plain database, and forecast draws its shocks under ``forecast_shock_uncertainty``.
simulateignored the date and started at the database’s first date, pinned to the data after it. When the history runs past the database’s first date, the simulation now jumps off at the date from the plansimplan.from_historygenerates out of the history, asforecastdoes. A database that starts at the date is a conditioning database, as before. The shocks of such a plan are now drawn under shock uncertainty (the constraint-enforcing shocks excepted); before, they were pinned, so a forecast from a database gave the same path whateverforecast_shock_uncertaintysaid.simplan.from_historytakes the regime and the shocks as optional arguments and no longer fails when the deepest lag predates the data. See Forecasting and simulation (“Starting from history”).Fixed: simulate takes a recursive plan under a solve paradigm. A plan built with
'recursive', truecarries a second page, and under a solve paradigm (for example the piecewise-linear paradigm on a model with a lower bound)simulatefailed with a size mismatch in the sub-step forecast. A paradigm steps with the shocks of the model’s own anticipation horizon: later pages that announce nothing are now dropped, so the recursive plan runs as surprises, and a page that announces something beyond the horizon is refused withRISE:simulation:planExceedsParadigmHorizon. Undersimulate, page-1 entries are surprises whether the plan is recursive or not. See Surprises and announcements (“Under simulate”).Fixed: the historical decomposition of a VAR gives the constant its own column.
historical_decompositionread the constant of a VAR from a place where the solver leaves zero, so theconstcolumn was zero and the structural-shock columns absorbed the constant (the columns still added up to the data). The columns are nowinit(the initial lags propagated),const, one per deterministic variable, and one per structural shock, which contain the shocks’ contributions only; they add up to the data. With regime switching the regimes are weighted by the smoothed regime probabilities and the output has one page instead of one per regime.residualsandstructural_shocksno longer contain the constant either. Results change for every VAR with a constant and for every switching VAR. See Reduced-form VAR Modeling.Fixed: tuning the independence sampler maximizes its acceptance rate. With
do_tuning,imhused the random walk’s rule (shrink the scale until the acceptance rate reaches 0.234), which collapses an independence proposal: on a multimodal target the scale fell to about 1e-200. It now searches the scalec0*2^kthat maximizes the acceptance rate and fixes it; on Gaussian targets it lands within a fifth of the best scale. Warningsrsamplers:imhTuningFailedandrsamplers:imhTuningAtSearchEdgeflag a search that cannot succeed. The random walk is unchanged. See Posterior sampling (“Tuning the proposal scale”).Changed: the Choleski identification of a reduced-form VAR follows the order in which the variables were declared.
identify(m, 'choleski', shock_names)andidentificationused to run the recursion in RISE’s alphabetical order of the variables; they now follow the list given torfvar_model(units, then variables, forprfvar_model), as a recursivesvar_modeldoes. Shock k belongs to the k-th declared variable and takes the k-th name; the new option'ordering'sets another order. An unidentified reduced-form VAR uses the same recursive factor. Results change unless the variables were declared alphabetically;'ordering', sort(variables)reproduces the old recursion. Also,irfnow labels the responses by shock slot, so it always agrees with the variance and historical decompositions, including with fewer names than variables. See Reduced-form VAR Modeling.Changed: template differentiation is the default. The construction option
template_diffis nowtrue: every model is differentiated one distinct expression at a time, and an equation with no repeated term is differentiated as before. Same derivatives (to rounding) at every order, with and without regime switching.dsge_model(file, 'template_diff', false)keeps the standard path. See Template differentiation.
3.2. Release 20260831
New: template differentiation, construction option ``template_diff``. Models that repeat a few equations – the cells of a heterogeneous-agent model, the countries or sectors of a loop-generated model, or copies typed out by hand – are differentiated one distinct expression at a time: RISE finds the terms that are the same expression up to a renaming of variables and parameters, differentiates each once, and evaluates its derivatives for all copies in one vectorized call. Nothing to annotate:
dsge_model(file, 'template_diff', true). Same derivatives as before (to rounding) at every order, with regime switching (constant or time-varying probabilities) and in disk mode. On a two-asset HANK model with 150 cells, differentiation falls from 655 s to 40 s and peak memory from 29.8 GB to 5.2 GB; Jacobian evaluation is 30 to 230 times faster across the models measured. Off by default. See Template differentiation. Tests:models/dsge/template_diff; tutorial:ModelShapes/template_differentiation.Fixed: options given inside the model cell no longer discard the name-value options.
dsge_model({file, name, value}, name2, value2)rebuilt the construction options from the cell alone, silently droppingname2(e.g.'smooth_kinks', false). The cell’s names and values are now validated and merged with the name-value options; a runtime option in the cell, an unknown name, or the same option with two different values is an error. Options are meant to be passed as name-value arguments; cells holding the model line by line, several snippets or several files are unchanged.New: ``rise.microfound`` derives Kuhn-Tucker (inequality) constraints.
.subject_to(irrev="I >= 0")used to be accepted and then silently dropped from the derived model – you got a clean solution to the UNCONSTRAINED problem, with no error and no warning. Inequalities are now normalized to theirg <= 0side, given a multiplier<agent>_lambda_<label>that enters the FOC of every control the constraint touches, and emitted as the complementarity row. Since the Lagrangian isL = U + lambda*g, the derived multiplier is the negative of the textbook one, solambda <= 0,g <= 0,lambda*g = 0is exactlymax(lambda, g) = 0– written out as thatmax, which the parser already recognizes as a kink and routes through the occasionally-binding-constraint machinery. Both>=and<=are accepted (strict forms treated as non-strict). A slack constraint is inert:examples/microfound/rbc_irreversible.mreproduces the plain RBC steady state withI = delta*kand a zero multiplier, while a binding floor pins the variable and returns a negative multiplier. Regression testmodels/dsge/microfound/testMicrofoundKKT.``rise.whatsnew(n)`` no longer spends a slot on an empty section. Cutting a release leaves a freshly opened, empty
Unreleasedheading behind, andrise.whatsnew(1)used to print that – the preamble and nothing else. Sections with no entries are now skipped, soncounts sections that actually carry entries.Fixed/New: mixed OBC enforcement — a ``max``/``min`` kink and a regime-switching constraint can now coexist in one model, each enforced by its own mechanism. Previously a kink on a switching model silently disabled the regime machinery: the constrained simulation path took the deterministic modal transition every period (a persistent chain never left its initial regime), a user
simul_regimeswitch rule errored out with “Both simul_constraints and simul_regime were provided” (the kink had auto-setsimul_constraints), and user-declaredsimul_constraintsoverwrote the auto-generated kink rows, leaving the kink enforced by nothing. The enforcement contract is now: the regime comes first, the fudge shocks second. Each simulated period, the regime-switching machinery selects the regime (stochastic transition draw persimul_stochastic_regimes, or a switch rule reweighting the transition row), and the one-step forecast then resolves the fudge-owned restrictions within the selected regime — including inside the candidate-regime forecasts a switch rule compares, and under pre-specified regime paths. Ownership is per restriction row: kink-derived rows (with their autoFUDGE_FACTOR_*enforcers) are always fudge-owned; user rows are regime-owned when given in 2-column form on a switching model and fudge-owned otherwise; usersimul_constraintson a kinky model now merge with the kink rows; the auto-compiled switch rule covers only the regime-owned rows; a user switch rule is accepted alongside fudge-owned rows and conflicts only with regime-owned rows (clear error). Pure kink-only and pure regime-switching-OBC models are unchanged (all locked regression numbers reproduce). Estimation/filtering violation-check forecasts still force the deterministic modal regime path for reproducibility. Worked example and regression test:models/dsge/obc/worked_examples/mixed_enforcementin the test suite.New: ``threshold_chains``, a one-line declaration for threshold and smooth-transition VARs. Available on every VAR constructor (
rfvar_model,svar_model,prfvar_model,proxy_svar_model):m = rfvar_model(endo, 'lag_length', 1, 'threshold_chains', struct( ... 'name','sc', 'controlled_parameters',{{'b','c','covar'}}, ... 'threshold_variable','ACR'));
This was always expressible by hand — a two-state chain whose
endogenous_probabilitiesare a steep logistic — and the machinery is unchanged: the sugar desugars into exactly thatmarkov_chainsentry before anything downstream sees it, so a sugared model reports the chain it actually has. What the sugar buys is that four things which are invisible when wrong are now impossible to get wrong: that the third argument oflogisticis the threshold; that the two probabilities must be complements (a threshold rule is memoryless ins_{t-1}, so two independent logistics silently give a smooth-transition chain, which is a different model); thatdelay = 1is written with no lag operator because the transition matrix built attgoverns the regime att+1; and that a slope too flat for the scale of the variable leaves observations in the logistic’s interior, where they become genuine regime mixtures.threshold_valueand, for the smooth transitions,slopeeach take either'estimate'(the default — creates<chain>_bar/<chain>_gam, free to be given a prior) or a number, which is written into the transition expression as a literal and creates no parameter at all. The asymmetry is deliberate: a fixed value that arrived as a parameter would be one more name the user must remember toset, and forgetting is silent — the model builds, filters and returns a likelihood computed at whatever the uncalibrated value happened to be. That is the class of failure this sugar exists to remove, so a fixed threshold does not get a name.transitionselects'hard'(default),'logistic'or'exponential'; the hard case uses the newutils.smooth_transition.hard_threshold, theg -> inflimit of the logistic floored into[1e-10, 1-1e-10], so there is no slope to choose. It is namedhard_thresholdand notthresholdbecause MATLAB’s Econometrics Toolbox ships a@thresholdclass and a class folder takes precedence over a path function — the shorter name is silently shadowed.Two implementation notes for anyone touching it.
hard_thresholddeliberately does not wrap its comparison indouble(): RISE differentiates transition functions through@rsymbdiff, which overloadsgtbut has nodouble, so the cast would make the function unusable inside a@transition_functionsblock. Andthreshold_chainsis dropped from the options handed to the code generators (rise.engine.parser.generator_config_pairs), which declare exactly the configuration they accept.Covered by
models/var/reduced_form/testThresholdChains.m(11 tests, including that the sugared chain reproduces the lagged rule, that the contemporaneous rule demonstrably does not fit, and that a model built with a fixed threshold gives the same likelihood as the same chain written out by hand). Documented in the reduced-form VAR chapter.New: local projections, with state interaction and sign-restricted identified sets.
rise.engine.var.lptools.estimateruns the projections andrise.engine.var.lptools.identified_setsearches rotations over them.A local projection is neither a VAR nor a univariate model, and it does not get a model class. There is no dynamic system, no state space and no likelihood: it is a stack of OLS regressions of leads on current and lagged variables, and nothing about it is sampled. What makes it structurally usable is that, with
y_ton the right-hand side, the coefficient block ony_tat horizonkis the reduced-form impulse response matrix at that horizon (Plagborg-Møller & Wolf). So the estimator’s whole job is to hand back{B_k}andSigma— exactly the pair a rotation-based scheme consumes — and two functions are the honest shape for that.Restrictions are parsed by the same routine the VAR family uses (
vartools.read_identification_restrictions, extracted frombuild_identification_enginewhere it had been a local function), so'GDP{0:2}@MP'means the same thing on both sides.identified_setadds anelasticitybound on the ratio of two impact responses and anormalizeoption that rescales each accepted draw. Lag-structure restrictions are rejected: a projection has no lag polynomial.Two properties of the specification are enforced and documented rather than left to be discovered: horizon 0 is the identity by construction (its regression contains its own left-hand side), so
Sigmacomes from thek = 1residuals; and with a single pooledSigmaa state-dependent projection can only reveal state-dependence living in the propagation coefficients, never in the impact covariance. If a mechanism lives there, a switching VAR is the right tool.Both local projections, not one. Passing
shock— the name of a measured shock series — switchesestimatefrom the rotation-based form above to the Jordà (2005) / Ramey–Zubairy (2018) form, where the coefficient on that series is the structural response and no identification step exists.est.moderecords which you ran. The two answer different questions and are not interchangeable, so they are labelled rather than blended: shock mode returnsbetaandbeta_seand leavesBempty, andidentified_setrejects such an estimate outright rather than rotating nothing.Shock mode drops the contemporaneous block, and that is the whole design.
s_tcausesy_t, soy_tmediates the effect being measured; conditioning on it closes the channel and drivesbetatoward zero — a bad control, and a silent one. Only lags ofyare controls. A consequence worth having:betaatk = 0is a real estimate, the impact response, where lpvar mode’sB{r}{1}is the identity and carries no information.Standard errors are Newey–West, truncated at
k + 1by default (hac_lagsto override). Overlapping horizons leave the horizon-kresidual serially correlated of order roughlyk, so OLS standard errors are wrong and get worse with the horizon; shippingbetawithout a HAC correction would have shipped an unusable number. lpvar mode reports no standard errors — its uncertainty is an identified set.And the validation direction, with its precondition. Running a projection on data simulated from a model you control — and checking it returns that model’s own responses — is how you learn whether a specification can recover a mechanism before trusting it on real data. It works: on a known VAR(1) with a triangular impact matrix,
chol(Sigma,'lower')recoversAto0.002andB_k*Amatches the model at every horizon to0.004.It works only when the shock is recoverable from the observables handed over, and the failure is silent. For
y_t = e_t + 2 e_{t-1}— non-fundamental, with autocovariances identical to the invertibley_t = u_t + 0.5 u_{t-1}— the projection returns0.502where the truth is2, reporting decay where the effect grows, atT = 200,000. More data never helps. This is not a weakness of local projections: LP and VARs estimate the same object, so LP inherits invertibility problems in full and is not a robustness fix for non-fundamentalness. Supplying a shock series recovers[1, 2]on exactly that data — the fix is information, not a different estimator.Covered by
models/var/local_projections/testLocalProjections.m(19 tests, including that a linear projection on VAR(1) data reproducesB^k, that shock mode recovers(B^k A)(:,1)exactly — impact vector included — that the model→data→projection round trip closes, that the non-fundamental IRF is not recoverable from history while shock mode gets it, that a normalization is exact, and that an elasticity bound binds). The bad-control test uses a noisy proxy deliberately: with the exact structural shock,e_t = inv(A)*(y_t - B*y_{t-1})makes the bad-control design singular, so the comparison would measure a minimum-norm artefact rather than the bias it claims to. Documented in the new Local Projections chapter; tutorial atrise-modern-tutorials/ModelShapes/local_projections.New: a policymaker can declare that it distrusts its model. Add
@robustness = rhoto an@optimization_problemand the policy becomes robust in the sense of Hansen and Sargent: it is chosen to perform acceptably not only under the model as written, but under the least favourable model close to it.@objective = -0.5*(pi^2 + lambda_y*y^2 + lambda_i*i^2), @robustness = rho_m, @instrument = i
That is the whole of it. The objective stays the ordinary loss: you do not write the adversary, its instruments, or any entropy penalty.
rho = 1/thetais the robustness intensity and it belongs to the policymaker in the way risk aversion does – it prices how far the policymaker entertains its model being wrong. It must name a parameter, never a number, so it can be calibrated, estimated, and attached to a Markov chain, which lets a policymaker fear misspecification more in one regime than in another. ``rho = 0`` is expressed by omitting ``@robustness``: a model that does not ask for robustness does not pay for it, no distortion variables are created, and the answer is the ordinary one rather than an approximation to it.No new solver was needed. A robust policymaker behaves as if an adversary were choosing the worst misspecification it can afford, which is a two-player zero-sum game, which the non-cooperative-games engine already solves. What was missing was not an algorithm but the writing-out, and that is what the parser now does: one distortion instrument per shock, named
DISTORT_<SHOCK>, entering inside that shock’s own coefficient and at every occurrence of it; a one-period lag, so the adversary commits before seeing the innovation it distorts; the adversary’s objective as the policymaker’s, negated, with the entropy term carried by both; and the adversary one@orderbehind, sincemin_u max_wis the ordering the problem is written in.Each of those five changes the answer if it is wrong, and two do so silently – the entropy term placed in one objective instead of both, and the adversary allowed to see the shock it distorts, are each worth about 1e-2 in the policy rule while passing every check that does not involve robustness. The fourth cannot be got right by hand even in principle: a
#definition may not contain variables, so a hand-written game must duplicate its objective and the two copies drift.The distortions are ordinary endogenous variables and appear in the solution, in impulse responses and in simulations. That is deliberate: with more than one shock the worst case is a direction, and which misspecification a policy guards against is usually the point of the exercise. On a small two-shock New Keynesian model the policymaker puts 88% of the distortion on the cost-push shock and 12% on demand – not because demand shocks are smaller, but because the instrument can offset them.
Robustness composes with everything already there: both solution routes, every timing protocol, and regime switching, including a switching
rho. It is bounded, though: past a model-specific frontierrho < 1/lambda_max(C'*Pbar*C)the adversary can buy unbounded damage and the problem has no value. The frontier is an eigenvalue condition, so it moves with the model; under switching it couples regimes; and it moves as it is approached, since it depends on the value under the robust policy itself. It can be small – near 3e-4 on an estimated New Keynesian model – so bracket it rather than guess.Validated against a hand-written statement of the same game to 5e-14 over two multipliers and both timings, and on Chen-Kirsanova-Leith – three named shocks, two Markov chains, four regimes – to 4e-16. Independently, the underlying recursion agrees with a coupled robust Markov-switching Riccati, an algorithm that appears nowhere in RISE, to 1e-14.
Refused, with a reason:
@robustnesson more than one player of a game. Each would fear a different misspecification, so the problem stops being zero-sum and becomes a fixed point in rules – a different object, not a bigger version of this one.Fixed: a switching SVAR with endogenous transition probabilities could not be built at all.
generate_svar_dsgeappended the transition-probability names to the@endogenouslist. The modern parser rejects TVTPs declared as endogenous variables, so construction stopped with “The following transition-probability names appear in @endogenous … must be declared in @transition_functions”. There was no way round it from the user side: the names are generated, not written by hand.proxy_svar_modeldelegates to the same generator and was blocked identically.The same generator also emitted the equations as legacy
! <eqtn>;lines inside@model, which still parse but raiserise:tvp_bang_prefix_deprecatedon every build.Both are now the modern form — the names stay off
@endogenousand the equations go in a@transition_functionsblock — matchinggenerate_rfvar_dsge, which had been migrated while the SVAR generator was left behind. Covered bymodels/var/structural/testSwitchingSvarTransitionFunctions.m, which checks construction, that the names are not endogenous, that no deprecation warning fires, that the solve gives a regime-specificA0^{-1} diag(sigma), and that proxy-SVAR inherits the fix.Fixed: an informative VAR prior could not be used on a switching RFVAR.
estim_var_prioraccepts a+var_priorstemplate (Minnesota, NIW, IW, Sims-Zha, …). Every one of those templates already exposesevaluate_at_reduced_form(B, Sigma_u), andsvar_evaluatoralready evaluates it at each regime’s reduced form and sums — the standard reading of “the same prior applied in every regime”. But onlysvar_modelandproxy_svar_modelregistered aroutines.var_prior_evaluator, so an RFVAR never reached that path.Both ends then took the older single-regime branch.
merge_var_priorsassigned the template’s parameters under BARE names, where a switching parameter needs one entry per state (<pname>_<chain>_<state>), so a switching RFVAR died during prior setup with “parameter c_acr is not controlled by const”; and had it survived,log_prior_densitywould have calledtemplate.log_prior(param), which is dimensioned for a single regime’svec(B). Non-switching RFVARs were unaffected, which is why this stayed out of sight.rfvar_modelnow registers+rise/+engine/+var/+var_priors/rfvar_evaluator.m, the RFVAR counterpart ofsvar_evaluatorand the simpler of the two: a reduced-form VAR is parameterized by its reduced form, soBis read straight off thec_*/b<lag>_*parameters with noA0to invert, andSigma_uoffcovar_*(via the sharedvartools.rfvar_sigma). The evaluator branch ofmerge_var_priorsnow also emits the RFVAR-shaped names (b<lag>_*,covar_*) and keeps taking start values from the template’smeanwhere it carries the parameter, so start values do not regress. Covered bymodels/var/reduced_form/testReducedFormVarPrior.m, which checks the density against a hand-built two-regime sum.Fixed: a reduced-form VAR’s residual covariance never reached its state space, so every likelihood-based RFVAR route ran with ``Sigma = I``. An RFVAR carries one residual per equation, correlated across equations, parameterised by
covar_sigma_<i>(variances) andcovar_<i>_<j>(covariances). Butgenerate_rfvar_dsgewrites each residual into the generated model with a unit loading (... + e1{t}), so those parameters appear in no equation, and nothing read them back. The derivative solve therefore returned a shock block equal to the identity no matter what the parameters said.The consequences were silent and wide. The likelihood was exactly flat in
covar_*— moving a variance from 0.04 to 16 left the log-likelihood unchanged to the last digit — so any ML or Bayesian estimation of those parameters was estimating on a flat surface and returning its seed. Regime-dependent covariances (controlled_parameterscontaining's'or'covar') were a no-op, which made regime heteroskedasticity in an RFVAR a no-op. Identification readSigma_u = Iand rotated the wrong object. Only the OLS short-circuit escaped, becauseestimate_var_olsbypasses the parameter dictionary and writes Sigma into the state space by hand.Fixed by
+rise/+engine/+var/+vartools/apply_reduced_form_covariance.m, called fromdsge_tools.solve.solveafter a successful solve and before identification: it assembles each regime’s Sigma from thecovar_*parameters and writes a Cholesky factor into that regime’s structural shock columns, the same blockapply_identificationlater rotates. SVAR and proxy-SVAR are untouched — they already build their impact block structurally asA0^{-1} diag(sigma). Panel VARs are covered too:generate_prfvar_dsgeexpands the endogenous list to<unit>_<var>and delegates to the RFVAR generator, so theircovar_*names resolve againstvar_block_nameslike any other RFVAR’s.No behaviour change for an uncalibrated covariance. A
covar_*that was never given a value (NaN) keeps its identity entry — unit variance, zero covariance — which is precisely what the generated model hardwired before any of this was transmitted. Forcing people to calibrate a covariance matrix by hand to get a VAR to solve would be a tax on everyone to catch a mistake almost nobody makes;estimateseedscovar_*from an OLS pre-fit anyway, so the only affected population is hand-built models, and those keep working exactly as before. NaN means “unspecified”, not “invalid”: a value that was supplied and cannot be a covariance (negative variance, an inconsistent pair, Inf) still stops the solve, with the new retcodeRFVAR_COV_NOT_PD(264).Covered by
models/var/reduced_form/testReducedFormCovariance.m(per-regime Sigma, the panel case, identity defaulting for unset and partially-set covariances, rejection of a supplied-but-invalid one, likelihood responsiveness, and the shorthand forms).Fixed: ``controlled_parameters`` entries naming a VAR covariance were silently ignored. In
assign_markov_parameters, the shorthand expander testedstartsWith(param,'c')beforestartsWith(param,'covar'). Since'covar'also starts with'c', every covariance token fell into the constant/deterministic branch, matched neither'c'nor'c(', and came back as an empty expansion — which the caller could not distinguish from a shorthand that legitimately matched nothing, so the parameter was left uncontrolled without a word. This made the entire'covar'family ('covar','covar(diag)','covar(offdiag)','covar(y1,y2)') dead code, and it also swallowed a fully typed-out name such as'covar_y1_y2', so an off-diagonal could not be made to switch by any route.'s'was unaffected — it is matched later in the chain — which is why the diagonal appeared to work.The covariance branch now precedes the constant branch, a typed-out
covar_*name resolves to itself, and'covar(y1,y2)'strips its parentheses correctly (the old slice kept a leading(). Related: acontrolled_parametersentry that expands to nothing is now an error (assign_markov_parameters:emptyExpansion) instead of a silent drop — the check is on the expansion before the existence filter, so a shorthand may still generate candidates that do not exist, as the normaliseda0diagonal does. Covered bymodels/var/reduced_form/testReducedFormCovariance.m.Fixed: a switching parameter inside a player’s objective was frozen at its first-regime value by the linear-quadratic game solver. In a non-cooperative game solved with
solver = 'loose_commitment'orsolver = 'stochastic_replanning', each player’s objective curvature was read once, at regime 1, and reused in every regime. The constraint blocks were always read at the regime they belong to; only the objective rows were not. A model whose loss weights switch – a policymaker who penalises an instrument more in one regime than in another, say – was therefore solved against the wrong loss in every regime but the first, and the general route (solver = 'mfi') disagreed with it.The answer was wrong rather than merely imprecise: it was not an equilibrium of the game, and the wronged player’s own best response departed from it. The error grows with how much the objective switches and vanishes when it does not, which is what kept the condition out of sight. Models with regime-invariant objectives are unaffected and no existing result changes.
Fixed in
+rise/+engine/+dsge_tools/+optimal_policy/lq_game_engine.mby carrying the regime on the objective block, as the constraint blocks already did. Covered byrf3sw_obj.rs, a game whose loss weights switch and whose@modelblock does not.New: the linear-quadratic optimal-policy solvers now accept non-cooperative games.
solver = 'loose_commitment'andsolver = 'stochastic_replanning'previously errored with “cannot solve non-cooperative games”; both now handle any number of players at the same@order(same-level Nash), any number of Markov regimes, forward- or backward-looking constraints, and either equilibrium concept (solve_policy_equilibrium'MPE'or'OLE'). No new solver name was introduced: the re-optimisation protocol is the solver, the number of players is a property of the model, and the two are orthogonal.The point of the specialisation is that it never evaluates a policy-derivative (``f1942``) function. The general route approximates every player’s decision rule by a polynomial and iterates on that approximation, re-evaluating the model’s derivatives and re-solving the steady state at each pass. In a linear-quadratic problem those rules are linear and the coefficients being searched for are entries of the solution matrix, so the whole apparatus reduces to two blocks written in closed form: the rivals’ reactions entering an augmented lag block, and the total derivative of the constraint that a re-optimising player prices. Those two are held fixed while an ordinary constant-coefficient rational-expectations problem is solved, then updated – the same two-level structure the general route uses, and what selects the mean-square-stable root.
On a two-country central-bank game with 34 endogenous variables and two regimes, under discretion with Markov-perfect play, the specialised route is 8-9 times faster (4.4-4.9 s against 39-40 s) and agrees to 1.5e-06, which is the general route’s own accuracy. It also solves at default settings where the general route returns retcode 210 and needs
solve_discretion_maxiterlifted from 100 to 500. On small models the advantage disappears: warm, the two are within a factor of two of each other at 13-17 equations, because fixed costs dominate.New engine at
+rise/+engine/+dsge_tools/+optimal_policy/lq_game_engine.m, with the model-ordered blocks inlq_game_blocks.m. The single-planner path is untouched – a one-player model never enters the game branch, and the existing 26 optimal-policy regression tests pass unchanged. The parser now records the per-player bookkeeping the engine needs (player_wrt,player_instruments,player_order) instead of leaving it to be recovered from equation-tag strings, andset_lq_gamereplaces the blanket refusal inset_loose_commitment.No new options. The coefficient loop is the same loop, over the same objects, as the general route’s, so it reads the same two settings:
solve_discretion_maxiterfor its iteration cap (floored at 50, exactly as inrun_loop_local) andsolver{2}.TolFunfor its tolerance. Tighten either and both routes respond identically. The one genuinely engine-specific choice – which solver handles the inner constant-coefficient problem, since the name that selected this engine would not dispatch there – travels in the solver’s own option struct rather than becoming a property of the toolbox:solve(m,'solver',{'stochastic_replanning','InnerSolver','mnk'}), defaulting to'mfi'.One case is refused with a reason rather than answered. Loose commitment with a commitment probability strictly inside (0,1), for a game: the probability would have to scale each player’s forward multiplier block, which cannot be expressed in the form the shared first-order engine reads – use
stochastic_replanning.Settled: the promise coefficient is an EXPECTATION, not the realised value — and the last refusal is gone. The linear-quadratic game engine used the promise block at the realised regime. The parser uses its expectation over the successor regime, conditional on landing in one that honours the promise:
- Abar(r_t) = sum_j Q(r_t,j) 1{gate(j)>0} A(j)
/ sum_j Q(r_t,j) 1{gate(j)>0}
The two readings coincide unless the promise block VARIES across the regimes that honour it, which needs three things at once: a hierarchy (so a leader holds promises on its followers’ conditions), a commitment indicator that differs across regimes (so a promise can span a change of status), and a switching structural coefficient (so its value differs by regime). Any two agree; the triple did not, by 1.3e-03.
The relationship was verified in closed form before anything was changed: with the structural chain at (0.9/0.1, 0.2/0.8) inside the commitment chain, the parser’s coefficients are exactly that conditional mean of the engine’s, to six figures in both commitment regimes. The engine now reads it the same way and the two routes agree to 7.3e-09. Fixture
game_fllfsw.Because the transformation is the identity when the block does not vary across honouring regimes, nothing that already agreed moved – which is also why the case had to be constructed before the difference could be seen.
NOTE for whoever revisits this: it makes the games engine AVERAGE where the single-planner promise block was fixed to use the REALISED block (see the
sum_j Aplus{r0,j}defect). The objects differ – this average is over the successor conditional on honouring, that one was over where the economy might land when r_t was already known – but they rhyme, and the general route was taken as authoritative here rather than derived from first principles. One nested function (promise_expectation) to revert.New: three or more ``@order`` levels are covered. Every hierarchy fixture had exactly two. The construction is a forward pass over processing order and is generic in the count – at the third level the row map resolves rows belonging to two different earlier groups rather than one.
game_rf3x(orders 1/2/3, one player each) prices 3 / 7 / 15 rows and agrees with the general route at 2.1e-10 (MPE) and 4.3e-10 (OLE). Checked rather than assumed, because “generic by construction” was also true of the Markov-perfect cross-chain beforegame_fl3lfdisproved it.Settled: the re-optimiser faces the RESET rule, and the last refusal is lifted. Two channels in the linear-quadratic game engine stand in for what the general route evaluates with
f1942: the re-optimiser’s continuation, and the Markov-perfect reaction of a rival’s instrument. That machinery exists for DISCRETION and takes the non-multiplier variables as its inputs by construction – a planner that re-optimises has discarded the promise it inherited, so the continuation it faces is a function of the genuine states alone.That is the reset projector
R = diag(I,0)the engine already owns:loose_commitmentizeapplies exactly it, but only AFTER the fixed point. Inside the coefficient loop both channels were substituting the RAW rule. They now substituteH*R.It is a no-op almost everywhere, which is why it went unseen. Under pure discretion
Halready equalsH*R– the multiplier columns of the rule are zero at the fixed point. Under pure commitment the discretion gate is zero and the continuation term is never used. Only a MIXED indicator reaches the case that matters: a discretion regime substituting forward into a COMMITTED successor, whose rule does respond to the promises made today (those columns measure 2.57, not zero).It is also read only by a player whose differentiation targets include a multiplier. Own-level multipliers – a player’s own and its same-
@orderpeers’ – are never targets, so only a player above the bottom level reads them, and for a single-level Nash game the whole thing is a no-op.Costs, measured on a forward-looking two-level game with a regime-varying indicator: 0.37 of a leader’s contemporaneous block from the continuation channel, and 1.5e-01 of the solution from the cross-chain. With both reset, the routes agree to 5.3e-09 (two players) and 2.2e-08 (three), and the refusal of a hierarchy with a regime-varying commitment indicator is lifted.
New fixture
game_fl3lf: two leaders sharing a level above one follower, forward-looking. It is the only shape in which both channels are live on the same player – a two-player hierarchy leaves the leader’s rival set empty, andgame_rf3lfhas the two leaders but is backward-looking with zero multiplier columns. The cross-chain defect was invisible until it existed.Nothing in the general route changed, and nothing needed to.
Fixed (BOTH routes): the commitment indicator was mis-dated in a leader’s forward block. It is one symbol doing two jobs, and the right date depends on which row it sits in.
On a STRUCTURAL row it asks “does the planner honour an inherited promise NOW”, which is a property of
t, anddate_switching_parametershas always – correctly – left it there. On a row that is a previously eliminated player’s FIRST-ORDER CONDITION, which exists only under a hierarchy, it asks something else: that condition carries the FOLLOWER’s promise term, and the block it sits in is multiplied bylambda{t+1}, so what a leader prices isbeta*E_t[lambda^L_{t+1} * c_{t+1}]– “will my follower, NEXT period, honour the promise it is making now”. The indicator belongs to the regime realised att+1.Left undated it was read at
r_t, and being a bare parameter it carried no tag for the forward shift to move: the same mechanism as thedc/du^rivalmis-dating. Worth 0.5 of the leader’s lead block on a forward-looking two-level game, and it applied to the GENERAL route as much as to the linear-quadratic one – any leader-follower model with a switching commitment indicator was affected whichever solver was chosen.The dating pass now splits by row. That is what makes the fix safe: a single planner and a same-level Nash game have no rows above the structural block, so neither can be reached at all, by construction rather than by testing. Measured on a forward-looking hierarchy under commitment: the assembled system and the parser’s went from 9.1e-03 apart to 4.9e-15.
Hardened: the date shift refuses to run on an undated switching parameter. Every coefficient that has to travel from
ttot+1in the optimal-policy pipeline goes through oneregexprepin the parser,local_lead, and that expression moves time-tagged atoms only. A bare parameter carries no tag, so it does not move: it stays bound to the equation’s own regime while every coefficient beside it lands atr_{t+1}. For a constant parameter that is harmless. For a switching one it is a mis-dating, and an invisible one – nothing errors, nothing warns, and it cannot be seen at all with a single regime. Two independent defects have now come through that funnel.local_leadnow errors, naming the parameter, if it is handed a block still carrying an undated switching symbol. Exactly two are exempt, because their timing genuinely is not the constraint’s:commitment, which asks whether an inherited promise is honoured now, andole, which selects the equilibrium concept and is the same at every date. Models that were correct are unaffected – the guard is silent once the dating pass has run.New: leader-follower games on the linear-quadratic route. Players with distinct
@ordervalues – a Stackelberg hierarchy rather than Nash – were refused byset_lq_game, on the grounds that a leader differentiates its followers’ conditions and so brings in derivatives of every tracked decision rule, and second derivatives of them. Under LQ those rules are linear and their coefficients constant, so the second derivatives vanish; and the followers’ conditions are rows of the very system the engine writes, so a leader’s dated blocks are a row-restriction of the matrices being built. Assemble the players in processing order –@orderdescending, which is the order the parser eliminates them in, hence lower ``@order`` = leader – and the rows a leader needs are already there. No new algebra, and it composes with the timing exactly as Nash does.Verified block by block against the parser at its converged coefficients on a two-player hierarchy: for the follower
A0/A+/A-differ by0/6.3e-11/0, for the leader by9.1e-12/0/0. Against the general route the solutions agree to1.6e-09backward-looking,1.9e-10with a switching coefficient on a rival’s instrument,1.4e-07forward-looking (the reference’s own accuracy), and exactly under commitment.Beware the indexing:
player_order,player_constraint_rows,player_multipliersandmult_groupsare in processing order while the parser’s declaration list is not. With one player per level the two coincide;game_rf3lfin the regression suite is the fixture where they do not.Refused, and deliberately not papered over: a hierarchy whose commitment indicator differs across regimes. A leader prices its followers’ conditions, and a follower’s condition carries a promise term gated by that indicator. Read as a constraint at
t+1– which is what the leader’s forward block is – the gate belongs to the regime realised att+1; the parser reads it atr_t, because the indicator is a bare parameter with no time tag anddate_switching_parametersexcludes it by name, deliberately (in the single-planner promise block, “does the planner honour an inherited promise now” really is a question aboutt). The readings coincide while the indicator is constant across regimes – pure commitment and pure discretion solve normally – and part company by0.5of the leader’s lead block as soon as it is not. Making the engine agree by copying a dating it does not believe is precisely what hid the mis-dating below for as long as it hid it, so it refuses instead and says why.Fixed: ``MPE`` no longer differs from ``OLE`` under commitment on the linear-quadratic game route. Markov perfection is a discretionary concept and the general route says so by construction: it enters its coefficient loop on
any(commitment ~= 1)and never on the equilibrium concept, so with a commitment regime everywhere the tracked policy derivatives stay at zero and the cross-player chain term is absent. The linear-quadratic engine computes those derivatives itself, so nothing stopped it applying the term anyway – and it did, putting it1.6e-02of the decision rule away from the general route on a two-player backward-looking game undersolve_policy_type = 'ramsey'with'MPE'. Both routes now agree exactly there.Fixed: the linear-quadratic game engine wrote its first-order-condition rows negated relative to the parser. A condition and its negation are the same equation, so for a Nash game the choice was free and invisible. It is not free once a leader prices its followers’ conditions: the leader’s row then mixed structural-multiplier columns carrying one sign with follower-multiplier columns carrying the other. Every condition is now written the way the parser writes it, which also means the two routes report the same shadow prices rather than a sign flip on the rows a leader carries.
Fixed (tests): two refusal tests in the linear-quadratic game suite never reached the solver. They passed a handle of the form
@()evalc('solve(m,...)'); an anonymous function captures only the variables its body mentions, and a name inside a string literal is not mentioned, so the handle raised “Unrecognized variable m” wherever it was called and the “did it refuse?” assertion was satisfied by the wrong error. Both are now real.Fixed (BOTH routes): the Markov-perfect chain term was mis-dated when a rival’s instrument carries a switching coefficient. Player
p’s optimality condition carriesbeta*E_t[lambda^p_{t+1}' * dc(t+1)/dy_t], andc(t+1)is the constraint block at regimer_{t+1}. Its lag block and its derivative with respect to a rival’s instrument are coefficients of the same date-t+1equation, so both belong atr_{t+1}. Only the first was.In the parser the cause was mechanical:
mpe_envelope_inject(create_optimal_policy_first_order_conditions.m) builds the cross term attand shifts it forward withlocal_lead, which rewrites time-tagged atoms only – the multiplier and the variables. A bare parameter carries no tag, so the shift walked past it and it stayed bound to the equation’s own regime. The fix tags it attbefore the shift, so the shift carries it tot+1with everything else.Measured against the defining property of a Markov-perfect Nash equilibrium – each player’s rule must be its best response to the rivals, which with the rivals substituted into the law of motion and the loss is an ordinary single-player Markov-switching regulator – on a two-player reduced-form switching game (exact answer 6.3e-12):
route
before
after
linear-quadratic
4.3e-05
5.5e-12
general (
mfi)4.3e-05
1.9e-12
and the two agree to 2.1e-10. When the rival’s coefficient does not switch the two readings coincide and nothing changes.
Cost. Only SWITCHING parameters are tagged, and only those appearing in
dc/du^rival, so the fix mints at most one auxiliary variable per such parameter and nothing otherwise. Measured:rf3(13 vars) unchanged,game_rf3p(18, three players) unchanged,game_2country(34, two regimes, switching Phillips slopes) unchanged; only the two fixtures whose rival coefficient actually switches grow, by 1 and 2 variables. Parse and solve times unchanged within noise.This also corrects an earlier entry which described the
r_treading as “settled by measurement”. What had been measured was which dating reproduced the general route, on the assumption that the general route was the reference – the assumption that needed testing. The equilibrium residual test is in the regression suite asmpe_br_residual.m.Unrelated but worth recording, since it was conflated with the above during diagnosis:
rise_1is not designed for regime switching, especially with switching in the dynamics or non-extreme transition probabilities, and scores 2.6e-03 on this test for that reason alone.Fixed: the mean-square-stability check was dead on the grand replanning loop.
+rise/+engine/+dsge_tools/+solve/solve.mswitchessolve_check_stabilityoff for the duration of that loop and restores it afterwards, but the post-loop call tois_stable_systemsat between the loop and the restore – andis_stable_system_enginereturns “stable” immediately whenever that flag is false. The check was therefore inert on every discretion, loose-commitment and stochastic-replanning solve routed through the loop, which reported retcode 0 on solutions its own test rejects.Found by diffing the two routes on a two-player game with a commitment regime. Both returned the same solution – 6.0e-08 apart, which is the general route’s own accuracy, and each satisfies the system defined by the other’s coefficients to 8e-09, so there was no multiplicity to choose between – and
is_stable_systemrejected it with a mean-square-stability radius of 2.19. Only the linear-quadratic route, which is checked on the ordinary path, said so.The restore now happens before the call. This is a behaviour change: models previously reported as solved on that path may now return retcode
25. That is the correct answer – the model has no mean-square-stable equilibrium at that calibration – but it will look like a regression to anyone who has been relying on the silent pass.
New: ``solver = ‘stochastic_replanning’``, a linear-quadratic solver for the stochastic-replanning problem (
+rise/+engine/+dsge_tools/+optimal_policy/replanning_solver_h.m). It is the loose-commitment engine with the scalargammareplaced by a per-regime commitment indicator read off the samecommitmentparameter, which changes three things. The transition matrix is unrestricted:loose_commitmentrequiresPr(commit next)to be identical from every regime and errors otherwise, becausegammais one number, so persistent and asymmetric replanning processes were previously out of reach. The commitment chain also need not be the only chain: everything is indexed by the composite regime, so any number of other Markov processes may switch the structural matrices, the loss weights or the shock loadings alongside it, with any number of states each. The reset lives in the equations rather than in a post-solve projection —GAM1(y,lamb)is gated by the indicator, so a replanning regime’s zero multiplier response falls out of the fixed point and the reportedTzis the fixed point, unlike underloose_commitment. And it remains available underno_u_turn, since like the loose-commitment engine it computes the derivative of the future policy function inside its own fixed point and never needs the discretionary (f1942) machinery that@no_u_turn = truedrops at parse time; on a model parsed that way with a persistent chain it is the only route that solves at all,loose_commitmentrefusing the chain and the general route refusing the missing derivatives.Validated against the general (parser + MSRE) route over the full reported solution, state columns and shock impacts alike: agreement to 1e-8 or better at
gamma = 1, atgamma = 0, on i.i.d. chains with0 < gamma < 1— whereloose_commitmentdisagrees with the general route by O(1), because it is solving the other problem — and on persistent chainsloose_commitmentcannot express. Indicatively around 50x faster than the general route on the test fixture, though that model is far too small to say anything about scaling.loose_commitmentis untouched and reproduces its previous solutions exactly. Weighting the policy-derivative channel by the probability that the successor replans, rather than by the current regime indicator, turns the new solver back into loose commitment — exactly, on the i.i.d. edge. That variant is deliberately not exposed, because off that edge it has nothing to be validated against.Fixed: the loose-commitment promise term used an average of forward matrices where the realised one is required (
+rise/+engine/+dsge_tools/+optimal_policy/solver_h.m).lambda_{t-1}enters the date-toptimality condition becausey_tsat in the lead term of the date-(t-1)constraint, so the promise term carries the forward block that constraint attached toy_t, de-weighted:Aplus{iota,r_t}/q(iota,r_t)withiota = r_{t-1}. The engine instead formedsum_j Aplus{r_t,j}, marked in the source as an approximation. The two coincide wheneverA+does not switch, which is why constant-coefficient loose commitment was never affected; they do not coincide once it switches. On a model with a lead-dated switching forward coefficient (a{t+1}*x{t+1}, i.e.A+_{ij} = q_ij*A+_j) the decision rules were off by about six percent against the general solver, at bothgamma = 1andgamma < 1; they now agree to solver tolerance. The required block is now computed from the assembled derivatives and, where it cannot be represented, the engine says so: with a current-dated switching coefficient (a*x{t+1}, the default reading, givingA+_{ij} = q_ij*A+_i) the promise matrix is indexed by the regime in which the promise was made, which a solution indexed by the current regime alone cannot carry; the historical approximation is kept and a warning names the general solver as the alternative. Models whoseA+does not switch, including both existing loose-commitment fixtures, reproduce their previous solutions bit for bit.Documented: loose commitment and stochastic replanning are different problems. They are not two solvers for one problem, and at
0 < gamma < 1they do not agree. Under loose commitment the re-optimization probabilitygammais a scalar and re-optimization is not a regime but a state value (lambda_{t-1} = 0), so one decision rule per structural regime suffices; under stochastic replanningcommitmentis a regime indicator with its own rule, and the chain may be persistent, endogenous or time-varying. Both carry the same future-policy derivative — thef1942hyperparameters are filled from the solution’s ownTz— and differ only in the scalar in front of it,1-gammaagainst1-commitment_t. Those coincide only atgamma = 0andgamma = 1, where the two routes agree exactly and each solution satisfies the other’s equilibrium conditions. See Optimal policy in the documentation for the full comparison and the measurements.Fixed: an empty shock horizon no longer collapses ``siz.nz`` (
+rise/+engine/+dsge_tools/+solve/rehash_topology.m). A model with no exogenous shocks has an emptyexogenous.shock_horizon, somax(shock_horizon(:))returns[]rather than0. Since scalar plus empty is empty in MATLAB, the augmented-state sizenz = np+nb+1+ne*(1+shock_horizon)silently evaluated to a 0x1 empty instead of a size, and so did anything computed from it downstream. The horizon is now normalized to a scalar zero before it enters any arithmetic, matching whatload_stochastic_solutionandset_simulation_initial_conditionsalready did. Only shock-free models are affected: when shocks exist the value is non-empty and the normalization does not fire, so every other path is byte-identical.``solve_prune_hank_constants``: exact pruning of the variables that are constant in the linearized model (new option, default
false; sideline development pending merge). A binary, parameter-free switch: the solver detects variables whose equations pin their first-order deviations at exactly zero at the evaluated steady state – canonically the rail-stuck HANK interpolation weights (saturated clamps), but also slack borrowing multipliers, pinned policies, and fixed aggregates; the mechanism is generic – removes them together with their defining equations by constant propagation on the evaluated Jacobians, solves the smaller system through the unchanged first-order machinery, and re-inflates the solution with exact-zero rows and columns. The answer is identical with the option on or off (measured IRF gaps 1e-13..1e-10 across the HANK replication suite); only the cost of solving it changes. The reduction is on the SYSTEM and happens ahead of whichever solver runs – a direct decomposition for a constant-parameter model, the iterative MSRE solvers under regime switching – so it is tied to no particular solution method. Under regime switching only variables trivial in every regime pair, at equal steady-state levels, are removed. If the reduction would not be square it refuses loudly and solves the full system. The option is HANK-named because that is where it is validated: the detector itself is model-agnostic, and it has been exercised across the model suite including models with no heterogeneity. (+rise/+engine/+dsge_tools/+solve/{condense_ first_order,inflate_first_order}.m, hook infirst_order.m.) Applies at first order only; higher orders continue to run at full dimension on the inflatedTz.Stability check: inert states dropped before the Kronecker build (
+rise/+engine/+dsge_tools/+stability/is_stable_system.m), gated onsolve_prune_hank_constants. States whose transition rows are exactly zero in every regime and solution are identically-zero directions; mean-square stability is invariant to removing them, and the Kronecker build is quadratic in the state count. This makes the MSS check feasible for pruned heterogeneous-agent models under regime switching: the 2-regime Auclert HANK previously ran out of memory ingupta_murray_hassibi; withsolve_prune_hank_constantsit now checks clean in seconds. Gated rather than unconditional so that every path not asking for pruning stays byte-identical – without pruning an inert direction carries solver round-off (~1e-8) rather than an exact zero, so the scan would cost a pass and find nothing.HANK: ``span`` declarations are now guarded, twice. A
span = sbracket window is an assertion about where the savings policy lands, and its validity depends on the parameterization – so it is now checked machine-side instead of trusted. (1) The steady-state engine measures the landing distanced(i,j) = |b*(i,j) - i|at every solve on the full grid (out.landing:dmax,min_safe_span, per-celld), so the safesis handed to the user for free rather than guessed; if a declared span is violated at the current parameter values the engine stops with a hard error (het:engine:spanViolated) naming the violation and the minimal safe declaration – the span-pruned model would be well-formed and could solve cleanly to a wrong steady state, so nothing downstream would catch it. (2) The parser now rejectsspan = 0(and non-integer or negative spans) with a clear message (hank_expand:span); previously it crashed with a raw index error, since brackets run1..N-1and the top cell’s window would be empty. Validated inpeer-feature-mining/HANK in Dynare/_experiments/exp29_span_guard.m: span-free andspan=1Auclert steady states identical to 1e-12; a uniform grid plus deep impatience (beta=0.60) trips the guard with “lands up to 3 brackets away (10 of 20 cells); minimal safe span=4”.HANK: generic steady-state engine (
+rise/+engine/+dsge_tools/+sstate/+het/{engine,steady_state}.m, new). A nested solver for the heterogeneous block, driven entirely by the parsed heterogeneity context: household policy by time iteration (all grid cells solved simultaneously by a vectorized Newton, Fischer-Burmeister at the borrowing constraint), cross-section by Young (2010) forward iteration, aggregation through the model’s@Aggdefinitions, wrapped in a damped price/aggregate fixed point (spec.pricesclosure) and an outer Broyden calibration (free parameters against named or function-handle targets;free = {}for pure fixed-point models a la Krusell-Smith). Validated against three independent hand-rolled steady states: Auclert et al. one-asset (all ten digits, full and span-pruned variants), McKay- Nakamura-Steinsson (1e-12 on every calibrated value), Krusell-Smith (imposed residual 5.6e-12 over the full expanded system), and Bayer-Luetticke (5,625 equations, borrowing to -100, entrepreneur via a bin indicator; imposed residual 1e-12, ~30x faster than the reference solver). Hardened for that last class: a guarded power (het_pow; negative exponents floor the base at 1e-12 – the reference solvers’ marginal-utility convention, inert on interior solutions) so “dead” grid cells with infeasibly negative consumption stay real; NaN-loud convergence tests (MATLAB’s max ignores NaN, which had let a poisoned solve report success); per-cell monotone backtracking; and a Broyden that never commits a non-finite step near an asset-demand asymptote.HANK parser: correctness fixes and new constructs (
hank_expand.m):Individual leads on the left-hand side are now expanded correctly. Previously a lead written on the LHS (e.g.
beta*Va{t+1} = ...) was silently renamed to the own-cell next-period variable – no idiosyncratic expectation, no policy interpolation – and solved a different model. Both sides now go through one expansion path; lead-free sides reduce to the old rename byte-for-byte.@Aggaccepts nested parentheses (@Agg(c{t}^(-sigma))); the argument is scanned to its matching close paren. Leads/lags inside@Aggare rejected at parse time (previously mis-expanded silently).Bin indicators
<axis>_at_<j>resolve at expansion time to literal 0/1 by grid position – addressing a bin by position where the bare coordinate only carries its value (per-type income, e.g. the Bayer-Luetticke entrepreneur). Out-of-range indices are parse errors.Bracket window
@heterogeneity_axis(a, N, policy = ap, span = s)limits the live interpolation weights tosbrackets either side of each cell, cutting the expansion from O(N^2) to O(N) endogenous variables (measured: 7692 -> 1420 at N = 60). A window covering the grid reproduces the default expansion byte-identically.The parsed heterogeneity context (axes, cells, name maps, raw equation classification) is persisted to
model_data.heterogeneity; it is[]for models without heterogeneity constructs.
DSMH merged and corrected: ``rise_dsmh`` + ``rise_dsmh2`` are now one sampler (
classes/stats/mcmc/rise_dsmh.m;rise_dsmh2is REMOVED – its option names carry over unchanged, so a caller just renames the function). The merged implementation keeps the usrsmplr-compatible interface and fixes/adds the following:Striated-move acceptance is now the equi-energy ratio. The old
rise_dsmh2accepted a resample from the previous stage’s energy band with the plain tempered ratioexp(lambda_i * dlogp), although the proposal is drawn from the PREVIOUS tempered target restricted to the shared striation; the correct Hastings ratio isexp((lambda_i - lambda_{i-1}) * dell)(Kou-Zhou-Wong 2006 / Waggoner-Wu-Zha 2014). Wrong occupancy shares on multimodal targets under the old ratio. Striated jumps now also recycle the stored posterior values, so they cost zero new evaluations.Out-of-bounds proposals are rejected, never clamped (same defect fixed for rwmh/imh/apt on 2026-07-05): with
lb/ubsupplied, the raw proposal is tested BEFORE the frame is applied, which is exact MH on the bound-truncated target even when a legacy clamping frame is passed in.Covariance adaptation stores the lower Cholesky factor. Both old files assigned the upper
cholfactor to the variable used as a lower factor, so the adapted proposal had the right spectrum but a rotated orientation (efficiency loss only – the proposal stays symmetric, so no bias;rise_dramhad the same quirk and is fixed too). Between stages the scalar scale now also adapts toward a 0.234 acceptance target.Likelihood tempering (
logprioroption): the ladder tempers the likelihood only and the prior enters every stage untempered – the Waggoner-Wu-Zha construction; the default remains posterior tempering. The default ladder is now geometric(i/H)^psi(psi = 4, dense near lambda = 0) instead of uniform.Stepping-stone marginal-data-density by-product (
extras.log_mdd): with a lambda = 0 anchor (iid uniform-box draws givenlb/ubunder posterior tempering, or iid prior draws viaprior_samplerunder likelihood tempering) the ladder’s normalizing-constant increments are accumulated into a log-MDD estimate at no extra sampling cost beyond the anchor stage. Each increment is estimated two-sided (forward from the lower rung’s draws, reverse from the upper rung’s, averaged in the log domain), which cancels the leading-order Jensen bias of one-sided stepping-stone estimation.Exact checkpoint/resume (
stop_after_sweeps/stateoptions): the full sampler state including the RNG is returned and can be passed back; a resumed run reproduces the uninterrupted run draw for draw.burnin, previously ignored by both implementations, is now a per-stage burn-in.
All of it is locked by the new suite
RISE-unit-tests/infrastructure/samplers/mcmc/DsmhCorrectnessTest.m(analytic-moment targets, a mode-on-the-bound target with a clamping frame, a 0.7/0.3 bimodal mixture started in the minor mode with a plain-MH control, analytic-evidence checks for both tempering modes, and a draw-for-draw resume test).Fix: ``rise_dram`` adapted proposal used the upper Cholesky factor (same orientation quirk as item 3 above; efficiency, not bias). The adaptation now stores the lower factor, and the delayed-rejection correction
iLis consistent with it.Performance: ``rise_dates.dates`` arithmetic vectorized (
@dates/dateshift,@dates/double).dateshift(called byplus/minus/colon) anddoublelooped element-by-element, invoking MATLAB’s already-vectorizeddatetimedateshift/datenumonce per element. They now batch the underlyingdatetimecall over the whole array. Results are bit-identical across all frequencies (D/W/M/Q/H/Y; char, serial,+,-all verified against the prior implementation) and the 234-testinfrastructure/time_seriesarea stays fully green. Measured ~3.6x faster on a monthly range build +doubleand ~2.7x on a quarterlycolon. First cut of a broader effort to keep dates/time-series off the estimation hot path (the per-evaluation likelihood loop is already double/struct-only; the remaining date cost is one-time data assembly and filter-output serialization).Fix: stochastic regime draws in simulation restored (regression introduced 2026-06-12 by
45255f15, which made the OBC constrained likelihood deterministic by selecting the modal next regime incompute_transition– correct for reproducible mode-finding, but the routine is shared with plain simulation viaforecast_unconstrained, sosimulate()paths silently froze in their initial regime whenever chains are persistent). Regime transitions in simulation are drawn from the transition matrix again. Design: stochastic is now the DEFAULT semantics ofcompute_transition; the constrained/violation-check forecasts (forecast_with_constraints,forecast_with_constraints_functional) explicitly opt out (stochastic=false) to keep the OBC likelihood bit-reproducible (verified). New optionsimul_stochastic_regimes(defaulttrue) lets a user reproduce the deterministic modal path in simulation if desired. Note: GIRFs again average over stochastic regime futures, the textbook definition.Fix: numeric ``simul_regime`` honored again. The documented scalar/vector forms of
simul_regime(impose a regime path in simulation) were silently dropped (simul.load_optionsonly handled the function-handle/switch-rule forms;set_simulation_regimeshad become orphaned). A numeric path is now expanded inset_simulation_initial_conditionsand overrides the regime draw; imposed paths are reproduced exactly.Exact (Durbin-Koopman) diffuse initialization for the Kalman filters, selected by
kf_init_variance='exact'(the same option that takes[]for the unconditional/Lyapunov default or a positive scalar for Harvey big-kappa – exact is a third value there, not a separate option). The diffuse directions are resolved in a short exact diffuse phase, so the likelihood is kappa-free – it drops the spurious-q/2*log(2*pi*kappa)offset that big-kappa carries and that contaminates MLE, marginal data density and model comparison.initialization.mbuildsP_infon the nonstationary (unit-root) subspace of the state transition;imm.mruns a guarded two-covariance diffuse phase; single-regimeexactmodels are routed toimm, so it works for constant-parameter and regime-switching models alike, and pinning a non-immfilter withexactwarns. The default big-kappa path is unchanged (filtering 47/47, obc 23/23). Validatedll_exact = ll_bigk + q/2 log(kappa)at O(1/kappa). See the “Diffuse initialization” docs page and theEstimation/diffuse_initializationtutorial. Exact diffuse is available inimm(default) and in the GPB-family filtersimmn,kim_nelsonandgpbn(pin viakf_user_algo); all four give the same diffuse loglik. Pinning a filter that does not implement it (imm_o, the constrained cell filter) warns and falls back to big-kappa. The diffuse subspace is now the nonstationary invariant subspace (ordered real Schur), correct for multiple and defective unit roots – e.g. a local linear trend ([1 1;0 1]Jordan block, 2-D diffuse subspace, one eigenvector) – not just a random walk;P_infis carried through the filter’s state-space augmentation. Validated across all four filters on random-walk (q=1) and local-linear-trend (q=2) models (testDiffuseInit, 12 cases); default big-kappa path unchanged (GPB suite 13/13). Two guards keep exact diffuse within its valid scope (the regular, regime-invariant diffuse block): if the nonstationary invariant subspace differs across regimes (regime-DEPENDENT diffuse block), or if measurement noiseHis non-diagonal (the univariate diffuse cascade assumes diagonalH),initialization.mwarns and falls back to big-kappa rather than compute a silently-wrong likelihood. Exact-diffuse smoothing is wired into all four filters (imm, immn, kim_nelson, gpbn): the forward pass stores the predictedP_inffor the diffuse steps and the smoother (utils.filtering.gpbn.smootherfor imm/immn/gpbn,utils.filtering.smootherfor kim_nelson) runs a Durbin-Koopman dual-accumulator (r0,``r1``) backward recursion over the diffuse span, so smoothed states over the firstdperiods are exact (previously they were degraded). Guarded: the branch fires only whenP_infstorage is present, so every non-diffuse smoother is byte-identical.simple_kalman_stepnow returns the predictedP_infand the real diffuse-phase innovation. Validated: smoothed states match big-kappa at O(1/kappa) for all four filters (local linear trend, q=2);testDiffuseInit16/16.The default regime-switching filter is now the Interacting Multiple Model (IMM) algorithm (Blom-Bar-Shalom), replacing
constrained_regime_switching_kalman_filter_cellfor order-1 switching models. IMM’s mixing is more general than the Kim-Nelson collapse the old default used. To reach parity, the two features the old filter had andimmlacked were ported intoimmvia new shared helpers so any switching filter can reuse them:rise.engine.filtering.filter_exo_driver(the observed-exogenous time-varying-transition driver, threaded intoevaluate_filter_transition_matrix) andrise.engine.filtering.filter_cond_data(the real-time / observed-expectations conditioning slice, passed as the predict closure’s 5th argument).immalso now readsdata_ywith future pages and the real-timesystfields. Filtering, OBC, regime-switching, estimation (MLE) and dynare-comparison suites are all green under the new default. The GPB-family filters (gpbn,imm_o,immn,kim_nelson) can adopt the same two helpers to gain these features. The previous filter remains available viakf_user_algo.Leads greater than one are now reduced without violating Jensen’s inequality – everywhere, by default. The parser’s lead reduction previously chose between two mechanisms via the
violate_jensen_inequalityoption: the default atomic chain (fast, butE[g(x)] -> g(E[x])for nonlinear leads) or a whole-equation backshift that closed each reduced equation with a martingale rowAUX{t+1}=0. The backshift was Jensen-correct but its martingale row has no contemporaneous variable, makingA0singular and breaking iterative solvers (cyclic reduction; themfioptimal-policy iteration) – so it could never be the default. Both are replaced by a single term-scoped reduction (get_rid_of_excess_leads_preserving_jensen): a term that is NONLINEAR in a forward variable is wrapped in an auxiliaryz{t}=shift(term,-1)and replaced byz{+1}(carrying the whole term’s one-period-ahead conditional expectation,E[g(x)]), while AFFINE leads use the ordinary atomic chain (chain_excess_leads). Every auxiliary keeps a contemporaneous variable, soA0stays non-singular. A recursive-descent classifier decides linearity; a lead scaled by a quantity known attstays affine, a lead inside a function/power or multiplied by another forward is wrapped. Models with no nonlinear leads (all linear models, and derived optimal-policy FOCs such as fm95) are reduced byte-for-byte as before. Theviolate_jensen_inequalityoption and the backshift reduction are removed. Full suite green (1004 tests), including the ten lead/lag regressions the backshift used to break. Details and the closed-formsigma^2derivation:development/jensen_memo/jensen_inequality.pdf.New ``rise.microfound`` – write the economics, not the algebra. A model definition builder: declare the atoms and each agent’s optimization problem (period objective, choice variables, named constraints) in MATLAB, and it derives the first-order conditions symbolically and produces a plain RISE model – no
@optimization_problemblock, nothing hidden. The derivation reuses the FOC engine’s beta-weighted differentiate-then-shift assembly via new shared primitivesrise.engine.foc.{change_symbols,symbolic_differentiate, attach_lagrange_multipliers}(byte-for-byte extracted from the optimal-policy engine). Hand the builder todsge_modelthree ways:dsge_model(p),dsge_model(code(p)), or write a.rswithrs(p,file). microfound builds the DEFINITION only – calibration (solve(m,'parameters',struct)) and solving aredsge_model’s responsibility. Derived multipliers are named<agent>_lambda_<constraint>. Switching parameters in an agent’s problem yield correct probability-weighted switching FOCs automatically. IMPORTANT: the steady state is NOT provided analytically (that would mean hand-deriving what the tool automates) –dsge_modelforms the steady-state system from the derived FOCs and solves it numerically;steady_state(...)supplies only a rough initial guess. Validated by re-authoring 9 gEcon example models (rbc, rbc_hf, rbc_cu, rbc_ts, rbc_ic, home_production, rbc_mc with household-SDF discounting, tc_rs two-country risk sharing, ttb time-to-build) – all derive and solve, and rbc_ts matches gEcon’s own derived steady state. Seeexamples/microfound/. A companion pre-pass auto-reduces lags > 1 before the FOCs (a control at lag k becomes a chain of co-state auxiliary controls with identity constraints; a non-control variable becomes auxiliary endogenous with model identities) so no manual auxiliary chain is needed – e.g. a 2-period time-to-buildK = (1-delta)*K{-1} + I{-2}derives and solves directly. Leads >= 2 in an agent constraint are reduced by a Jensen-preserving subexpression aux: an additive termTat lead >= 2 becomesz{+1}with the identityz = shift(T,-1), sozcarries the WHOLE (possibly nonlinear) term’s one-period-ahead conditional expectation (E[g(x)], notg(E[x])) and the constraint keeps its date (its multiplier coupling intact). Verified to reproduce the direct FOC(1/beta^2)*lambda_{t-2}*(dg/dx)exactly, and a nonlinear lead-2 model at second order matches the analytic Jensen gapexp(0.5*sig^2)(unit testmodels/dsge/microfound/testMicrofoundJensen). A time-varying (stochastic) discount factor is handled correctly: the backward multiplier weight uses the LAGGED one-period discount1/SDF_{t-1,t}(for a constant discount this equals1/beta, so the planner path is unchanged). HANK heterogeneity, Kuhn-Tucker inequality constraints, recursive (Epstein-Zin) preferences, and gEcon-style index-set templates are planned extensions.``fix_point_iterator`` now speaks plain MATLAB option names internally – the last ``fix_point_*`` names are gone. The shared fixed-point engine used to rename incoming options to internal
fix_point_TolFun/fix_point_maxiter/fix_point_verbose/fix_point_explosion_limit/fix_point_valid_funcfields. It now reads plain names directly:TolFun,MaxIter,Display('iter'``|’none’|’final’, with the optimset ``'off'and a logical still accepted for back-compat),ObjectiveLimit,ValidFunc. Behaviour at every call site is unchanged (the options structs already carried the plain names); the internalupdate_fix_point_optionsrename step is removed. Callers that constructedfix_point_*fields (the Maih-Waggonerupdate_options, the LyapunovdoublingFixIter) and the solver unit tests were updated to the plain names. Verified: solvers_generic 195/195, steady-state 262/262, and the Kalman/Lyapunov path (filter) unchanged.New MSRE solver ``mafia`` – Multisecant-Accelerated Functional Iteration Algorithm. The Picard iteration behind
mficonverges only on a contractive fixed-point map; on a genuinely non-contractive map (one Jacobian eigenvalue > 1) it diverges, returning retcode 21, even where a stable solution exists andmnk/mn/fwzfind it.mafia(solve(m,'solver','mafia')) iterates the SAME map asmfi– so it keepsmfi’s tendency to land on the stable minimum-state-variable solution rather than an explosive one – but accelerates it with self-tuning windowed Anderson acceleration (a multisecant/quasi-Newton scheme using only evaluations of the map, no Jacobian, with QR condition-based regularisation and a non-monotone-bounded safeguard). It has NO tuning knobs: one fixed configuration converges the easy-contracting, non-contractive and slow/ill-conditioned regimes alike, so it can be pointed at estimation and adapt per parameter draw without the caller knowing which regime each draw is in. Onfin_frics_switch(Picard-map spectral radius 1.0163) plainmfireturns retcode 21 whilemafiarecovers themnksolution to ~1e-7, and it is faster thanmfion the mapsmfialready solves.mfiandfix_point_iteratorare unchanged (byte-for-byte). Theory and the parameter-free three-regime validation:development/MFI_Anderson_Acceleration_Note.pdf,development/ADAPTIVE_AFI_VALIDATION.md; regression:RISE-unit-tests/.../solvers_generic/testMafiaSolver.m.New option ``sstate_warm_start`` (default ``false``): warm-start the numerical steady-state solve from the last successful solution. Profiling shows the numerical steady-state search (
rise.engine.rstst.solve_blocks) is 42-69% of every model re-solve, and estimation re-solves the steady state from scratch on every draw even though successive draws are ~0.1% apart in parameter space. Withsstate_warm_start=true, the search is seeded with the last successful steady state of the same model (persistent cache keyed by a stable model fingerprint — the object itself is re-created per draw); the current draw’s parameters always prevail, and a failed warm-started solve is automatically retried once from the cold default, so a stale cache entry can never make a solvable draw fail. With a unique steady state the warm and cold solves land on the same solution (verified to <1e-8 in the tests). Measured on 20 jittered re-solves (MCMC-like): fs2000 solves 2.2x faster end-to-end (order 2); models whose cold default already sits at the steady state (e.g. linear switching models with a zero steady state) see no change. Reset the cache withrise.engine.rstst.warm_start_cache('clear'). Default OFF: behavior is unchanged unless the option is set.Retired the global ``fix_point_*`` options (
fix_point_TolFun,fix_point_maxiter,fix_point_verbose,fix_point_explosion_limit,fix_point_valid_func). These were a single shared knob that silently affected every consumer of the internal fixed-point iterator (MSRE solvers, optimal policy, Lyapunov doubling, linear-systems solves, the mdd bridge sampler, …). Each call site now reads its own scoped option with unchanged defaults: the solver options cell (solver={'mfi','MaxIter',...,'TolFun',...};Display='iter'replacesfix_point_verbose),sstate_solver{2},solve_linsyst_solver{2},lyapunov_algo{2}, andmdd_bridge_TolFun/mdd_bridge_maxiterfor the bridge sampler. The discretion iteration cap issolve_discretion_maxiter. Passing afix_point_*option now errors as an unknown option.MCMC correctness fixes (three defects) + a procedural-correctness test suite. An audit of the sampling machinery for algorithmic correctness (acceptance ratios, proposal corrections, bounds handling, sign conventions) found and fixed three defects:
Independence MH drew from the wrong distribution.
one_step_mh(and the delayed-acceptance variant) proposed from a random walk centered at the current state while the acceptance ratio applied the independence-sampler density correction with the fixed proposal mean — the chain’s stationary distribution waspi(x)/q(x)instead ofpi(x)(inflated tails). IMH now draws from the fixed proposal distribution, as the correction assumes.Out-of-bounds proposals were clamped, not rejected. RWMH/IMH/APT proposals were clamped onto
[lb, ub]by the recentering frame and evaluated there, while the acceptance ratio assumed a symmetric, uncorrected proposal — piling probability mass on active bounds. Proposals now use a constraints-only frame (reframestyle 0) and out-of-bounds draws are rejected in the accept/reject step, which is exact MH on the bound-truncated target. Behavior is unchanged when the bounds do not bind (identical RNG stream, no redraws).DRAM’s delayed-rejection stage used a mis-scaled proposal density. In
rise_dram, the second-stage acceptance’sq_ratioomitted the1/adascale^2scaling of the first-stage proposal covariance, so the DR acceptance probability was wrong wheneveradascale ~= 1(the default is2.38/sqrt(d)).
All three are locked by the new suite
RISE-unit-tests/infrastructure/samplers/mcmc/McmcCorrectnessTest.m, which checks each sampler (RWMH, IMH, APT, slice, DRAM with the DR branch actually enabled, shortcut wrappers) against targets with analytic moments, including a target whose mode sits ON a bound.New option: ``sstate_file_is_authoritative`` (default ``false``). When a user-supplied steady-state file (or steady-state model) returns a nonzero retcode — “no solution for these parameters” — the engine used to ignore the verdict and fall through to the full numerical steady-state search, a 1-10 s tax per bad draw during MCMC estimation. With the option set to
true, the file’s verdict is trusted: its retcode propagates immediately and the numerical search is skipped. A large residual with a zero file retcode still triggers the numerical search as before. On thesstate_looppath the verdict is checked at the initial loop point only. Defaultfalsepreserves the previous behavior exactly.New: ``texreport(m)`` — paper-ready LaTeX model appendix (headliner 4, slice 2). One command writes a compilable
.tex(or an\input-ready fragment): the model equations rendered as mathematics by a new RISE-to-LaTeX translator (rise.engine.reporting.eq2tex— a recursive-descent parser emitting\fracfor divisions, braced powers, operator names for known functions, time subscriptsx_{t+1}from the{t+1}/{+1}forms,\bar{x}for{stst}, and greek symbols via the model’s tex names or a built-in table), plus calibration and steady-state tables per regime (booktabs). Auxiliary LEAD_/LAG_ equations omitted by default; translation failures fall back to verbatim rather than dying; multiple parameterizations are sliced into one appendix each;'compile', trueruns pdflatex. Unit-tested down to exact translator strings, all 16 fs2000 equations translate with balanced groups, and the compile test produces a real PDF.New: ``webreport(m)`` — a self-contained interactive HTML model report. One command produces ONE file — no external assets, no server, opens offline in any modern browser, shareable with coauthors who do not have MATLAB. Tabs: overview (regimes, solver, root moduli for constant-parameter models), the model equations, calibration and steady state per regime, an interactive IRF explorer (shock selector, variable picker, one panel per variable with the regimes as series, crosshair hover reading out every regime at the hovered horizon, regime legend and end-of-line labels), and a data-table tab carrying every plotted number. Light and dark mode follow the system preference; charts use a colorblind-validated palette. Engine:
@rise_model/webreport.m+ the template in+rise/+engine/+reporting/webreport_template.html; numbers are embedded as JSON inside the file. First slice of wahoo headliner 4 — posterior/estimation panels and a LaTeX model-appendix export are the planned follow-ups.Failure diagnosis phase D5 — the return-code registry now speaks. The terse registry messages behind
decipherwere rewritten to state the cause and the next step:'System unstable'became “No mean-square-stable solution: the switching system’s second moments explode under the regime process (this is the MSS test, not an eigenvalue count) — run diagnose(m) for the MSS radius and the culprit regime”;'Explosive solution','Nans in solution or no solution', the steady-state and simulation codes similarly. Codes, names and categories are unchanged — only the human-facing text. A Failure diagnosis chapter joins the docs (Working with a model) and a runnable tutorial joins rise-modern-tutorials (three deliberately broken models, the homotopy stall-as-diagnosis, and the MSS-vs-conditional-explosiveness lesson).New: failure diagnosis phase D4b — perfect-foresight shock-size continuation. A new
simul_homotopyoption (default false): when the stacked perfect-foresight solve fails at the full shock size, the exogenous path is walked up from zero (adaptive step, each trial warm-starting the free endogenous cells from the previous step’s path; initial/terminal conditions untouched), so the full-size problem is approached from a nearby solution instead of the steady-state guess it just failed from. If even the ladder stalls, the reached share of the shock size is itself the diagnosis (“solvable up to 60% of the shock — the full-size shock likely pushes the path where no bounded solution exists”), surfaced throughdiagnostics_on_failure. Inner ladder trials keep their per-step diagnostics silent. Also fixes an edge case in the D3 worst-cell analyzer when every period’s residual is non-finite. Honest test note: the mechanism is verified by traced runs and a no-interference test; a genuine cold-failure integration fixture is still missing because the stacked Newton solves the available smooth models even at absurd shock sizes.New: failure diagnosis phase D2 — the stability analyzer, with the right vocabulary per model class. When the solve fails,
diagnose(m)now explains why in the language that applies: for CONSTANT-PARAMETER models, Blanchard-Kahn root counting — “6 stable roots but only 5 predetermined variables: indeterminacy”, with the root table (the failed solve now retains its eigenvalues for exactly this purpose) and a clearly-labeled Taylor-principle heuristic; for REGIME-SWITCHING models, MEAN-SQUARE STABILITY only — the MSS spectral radius vs the criterion, per-regime conditional radii and persistence, and the culprit regime (“regime 2 is conditionally explosive and too persistent”) — never an eigenvalue count. Iterative MSRE solver failures (no convergence / NaNs / divergence) get their own narrative with solver alternatives. Engine:rise.engine.diagnose.stability;model_datagains a template-declareddiagnosticsfield (keeps struct arrays homogeneous under estimation).New: failure diagnosis phases D3+D4a — no more silent NaNs, and a homotopy fix-action.
The silent-NaN era ends. A diverging simulation used to return a NaN-filled database without a word. Now the failure site reports the first non-finite value — period, variable(s), regime — the growth profile of the run-up, and the pruning hint when running above order 1 unpruned (
rise.engine.diagnose.simulation). A newdiagnostics_on_failureoption ({‘warn’} | ‘error’ | ‘off’) governs surfacing everywhere; ‘off’ restores the legacy silence.Perfect-foresight worst cells. When the stacked solver fails to converge, the stacked residual at its final iterate is reshaped to (equation x period) and the worst violations are reported, plus the periods carrying 80% of the residual mass — localizing the failure to, say, “equation 3 around periods 12–15” instead of “did not converge” (
rise.engine.diagnose.perfect_foresight_stack; the existingmultiplicity_diagnosticis suggested for uniqueness questions).Homotopy fix-action. Every successful steady-state solve now remembers its calibration;
[report, mfixed] = diagnose(m, 'fix', "homotopy")walks the parameters gradually from that last working calibration to the failing target, warm-starting each step from the previous solution (with a cold fallback when a warm start is unusable, e.g. a log variable whose steady state went non-positive). On success the model comes back solved at the target; on stall the breakpoint is itself the diagnosis — “solvable up to 47% of the way: the target calibration likely admits no steady state” — and the model is returned solved at the breakpoint mix for inspection.
New: ``diagnose(m)`` — failure diagnosis, phase 1 (steady state). The first slice of the failure-diagnosis suite (
development/FAILURE_DIAGNOSIS_MEMO.md): instead of a bareSteady state could not solve,diagnose(m)reports the worst equations (per-equation residuals against the engine’s 1e-6 acceptance floor, labeled by equation tag or the equation text itself), the variables and parameters entering them, a structural-rank check of the steady-state incidence (a declared-but-never-used variable is named outright), and concrete next steps. Diffuse failures (most equations above the floor) are called out as such — one global culprit is more likely than the top-ranked equations — rather than overattributed. When the steady state is fine but the solve fails, the deciphered return code is surfaced with model-class-aware stability language (eigenvalue/Blanchard-Kahn reasoning only for constant-parameter models; mean-square stability for regime-switching ones); the full stability analyzer is the next phase. Engine:rise.engine.diagnose.steady_state.Fix: ``load_solution`` errored when called with a model object.
rise.engine.dsge_tools.solve.load_solutiondocuments that it accepts a model object or amodel_datastruct, but it readobj.topology(and, further down,obj.exogenous) — fields the class facade does not forward as dependent properties — so a direct call on adsge_modelerrored. The function now normalizes a model object to itsmodel_dataafter the solve step; both calling conventions are regression-tested and deliver identical solutions.New: unified ``sensitivity()`` front-end — one design, three GSA views. A
sensitivitymethod on all model classes (engine:rise.engine.surrogates.sensitivity_report) runs the whole global-sensitivity toolbox on ONE evaluated design: analytic PCE-Sobol’ main/total indices (who drives the variance of the log posterior over the prior), HDMR first-order indices from an independently fitted metamodel on the same design (a built-in cross-check, validated against analytic benchmarks), and Monte-Carlo filtering (who decides whether a draw lands in the top region of the target, measured by per-parameter Kolmogorov-Smirnov separation with asymptotic p-values — the fullmcfobject is returned for its plots). Everything lands in one table; HDMR/MCF recycle the emulation design rather than sampling again. Note: only HDMR’s first-order column is reported — the legacy engine’s level-2+ aggregates were found to carry systematic leakage/overcounting at realistic sample sizes (e.g. reading 0.24 on a null pair, 1.19 on a pure pair with share 1.0); investigating that estimator is left as a follow-up, and interaction/total readings come from the PCE columns.New: adaptive surrogate tools — refinement, EGO mode finding, active subspaces. Three tools close the surrogate lifecycle on the same
fit/predictcontract.refine(s, f, nadd)adds true evaluations where the emulator is least sure (the GP’s pointwise sd; space-filling for the PCE) and refits in rounds.rise.engine.surrogates.ei_maximizeruns the classic EGO loop (Jones-Schonlau-Welch expected improvement) to locate the optimum of an expensive function — e.g. a posterior mode — in a few dozen true evaluations (finds the Branin optimum within a 60-evaluation budget in the test suite), returning the fitted GP for reuse as a delayed-acceptance screen.active_subspace(s)estimates the gradient-covariance eigendecomposition under the prior with gradients taken on the surrogate (zero additional true evaluations), exposing the few linear parameter combinations the posterior actually responds to, plus per-parameter activity scores.New: Gaussian-process (Kriging) emulator with calibrated pointwise uncertainty.
emulate(m, 'method', "gp")— the second emulator on the samefit/predictsurrogate contract. Detrended (universal) Kriging: a polynomial mean (trendoption;emulateuses"quadratic", the natural shape of a log posterior) plus an anisotropic squared-exponential GP on the residuals, inputs standardized by the design data (robust to fat-tailed priors whose mass is a sliver of the bound box), hyperparameters by marginal likelihood with restarts, analytic leave-one-outQ2diagnostics. Unlike the PCE’s single global uncertainty number,predict’s second output is pointwise: small near design points, growing away from them — the ingredient for adaptive designs and surrogate-guided optimization. Onfs2000:Q2~0.92 from 135 design points (order-2 PCE: ~0.81 from 165). No analytic Sobol’ (use the PCE for sensitivity); both emulators work as delayed-acceptance screens.New: chain checkpointing and resume for the MH samplers.
rwmhandimhacceptstore_file/store_every/resume: the complete chain state (draws, current position, rng state, adaptive-tuning state, and the delayed-acceptance surrogate value when active) is checkpointed atomically to MAT v7.3 (HDF5), so long runs survive crashes. Resuming restores the rng and tuning states exactly, making the continued chain bit-for-bit identical to the uninterrupted one (unit-tested); a completed run can be extended by resuming with a largerN. Withnchain > 1a per-chain suffix is appended.apt/slicereject the option explicitly.New: delayed-acceptance MCMC — exact posterior at a fraction of the true-likelihood cost. The Metropolis-Hastings samplers (
rwmh,imh) gained ada_surrogateproperty: every proposal is first screened with a cheap surrogate of the log posterior (e.g.@(x)predict(s,x)fromemulate); only survivors pay for a true posterior evaluation, and a second accept/reject step (Christen & Fox, 2005) keeps the chain’s stationary distribution exactly the true posterior — a poor surrogate costs acceptance rate, never correctness (unit-tested with a deliberately biased surrogate). Composes with the existing adaptive tuning and constraint machinery, andstats.funevalsreports the true evaluations actually paid for. Onfs2000, ~70% of iterations are settled by the screen and wall time drops ~3x with indistinguishable posteriors. A surrogate that fails at the chain’s initial point disables the screen for that chain (with a warning) instead of freezing it; theaptandslicesamplers reject the option explicitly. Example:rise-modern-tutorials/Estimation/surrogate_accelerated_mcmc/howto.m; docs: the Surrogates page. Phase 1b of the surrogate-accelerated-estimation roadmap.New: surrogate (emulator) layer — polynomial chaos with analytic Sobol’ indices. A new
rise.engine.surrogatespackage (pce,doe, abstractsurrogatecontract) plus anemulatemethod on all model classes. One call —[s, design] = emulate(m)— fits a polynomial-chaos expansion of the log posterior kernel (or of any user function of the parameters) from a few hundred true evaluations; afterwardspredict(s, theta)approximates it in microseconds (~1500x per evaluation onfs2000). The polynomial basis is derived per parameter from its prior (Hermite for normal priors, cdf-transformed Legendre otherwise), the design of experiments is drawn from the priors’ inverse cdfs, and — because the basis is orthonormal under the prior —sobol(s)returns the exact variance decomposition of the emulated quantity (main and total Sobol’ indices) analytically from the fitted coefficients, with no additional sampling. Leave-one-out cross-validatedQ2and hold-out validation helpers included. Example:rise-modern-tutorials/GSAandUQ/UQ_surrogates/howto.m; docs: the Surrogates page of the GSA/UQ chapter. This is Phase 1a of the surrogate-accelerated-estimation roadmap (development/WAHOO_ROADMAP.md); delayed-acceptance MCMC and a Gaussian-process emulator on the same contract come next.Perfect-foresight solver: faster and more robust. The deterministic (
perfect_foresight) solver gained several improvements, all behaviour-preserving at their defaults:the default
sparsesolver (rise_newton) now judges convergence on the max-abs (infinity-norm) residual instead of the 2-norm. The 2-norm of a stacked residual grows likesqrt(periods*endo_nbr), so a fixed tolerance was unreachable for long horizons even when every equation residual was tiny; the max-abs criterion is per-equation and size-independent.a new ``JacobianUpdate`` option on the
sparsesolver enables modified-Newton factorization reuse: factorize the stacked Jacobian once and reuse the LU factors (inf), or refresh everyKiterations (K>1);1(default) is full Newton. Reuse refreshes adaptively when the line search backtracks, and retries with a fresh factorization on a stall. On transition problems this leaves the solution unchanged while substantially cutting the time spent in the linear solve.the per-period Jacobian block is now assembled directly as sparse (dropping a dense scatter +
find), and the line search no longer rebuilds the Jacobian on every backtracking trial.a new ``simul_reuse_sstate`` option (default
false) skips the internal steady-state re-solve when the model is already solved, for pre-solved / repeated / extended-path runs.
Steady-state solve: false “could not solve” failures fixed. The steady-state success verdict compared the residual against the solver’s iterate tolerance (
sqrt(eps)), which sits right at the achievable residual floor — so well-converged solutions on some models (e.g. the canonicalramst) were falsely reported as failures. The acceptance verdict is now decoupled from the solver tolerance via a floor (1e-6, still tighter than typical practice and well above the floor), judged on the max-abs residual; a deliberately looser solver tolerance is still honoured, and achieved precision is unchanged. Separately, the engine now refreshes the parameter / definition slots from the current values on every solve, so parameter changes (estimation, calibration) correctly propagate into the steady-state solve — fixing a wrong-likelihood regression in estimation. A parameter solved in the steady state (sstate_endo_param_swap) is now seeded with a finite initial guess instead of leaving its NaN placeholder, and the parameter refresh skips NaN entries, so the solve no longer fails with “Objective function is returning Inf or NaN values at initial point” on parameter-constraint models (e.g. the canonicalramst).Steady-state engine replaced. The previous (legacy) steady-state engine has been removed in favor of a new constraint-based engine built on the
+rstst/package. The new engine was validated against the legacy one over: 55 example/OBC models (rise+solve, 0 rstst-only failures), order {1, 2, 3} perturbation (18/18 byte-identical), optimal policy including MPE (byte-identical where models parse), and end-to-endhowto.mscripts (5/7 pass; the 2 failures are pre-existing bugs in the howto scripts themselves and fail identically under the old engine). The transientsstate_engineoption that selected between the two during validation has been removed; there is now only one engine. User-visible behavior should be byte-identical for working models; the new engine additionally handles occasionally-binding constraints combined withsstate_imposedcorrectly (the previous engine required a workaround).
3.3. Where the canonical changelog lives
The single source of truth for these release notes is CHANGELOG.md
in the modern toolbox root, next to README.md. This page is
rendered from it. New entries are added to CHANGELOG.md first, then
reflected here – not the other way around, and never into
rise-stable-docs, which documents the legacy toolbox.
In MATLAB, the most recent entries are surfaced by rise.whatsnew
(or its shortcut rise_whatsnew):
rise.whatsnew % print the entire CHANGELOG
rise.whatsnew(3) % print the 3 most recent dated/Unreleased sections
rise_whatsnew(1) % shortcut, same semantics
The function lives at +rise/whatsnew.m in the toolbox; the shortcut
at m/shortcuts/rise_whatsnew.m mirrors the pattern of rise_demo
/ rise_doc.