From a4b3207a8c3abe997b4488dd74ca20a66fff8330 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Wed, 22 Jul 2026 10:06:06 +0200 Subject: [PATCH 01/24] Add particle filter (bootstrap, auxiliary, tempered) with measurement error Add `filter = :particle` to `get_loglikelihood` for all perturbation orders (:first_order through :pruned_third_order), integrating out the structural shocks by Monte Carlo. Three variants selectable via `particle_filter_algorithm`: :bootstrap (Dynare-style sequential-importance-resampling), :auxiliary (Pitt-Shephard), and :tempered (Herbst-Schorfheide). Includes a resampling suite (systematic/stratified/multinomial/residual), adaptive ESS resampling, Lyapunov-based initial cloud, and a seeded `rng` for reproducibility. Generalize measurement error (`measurement_error_std`) to the filter-based likelihood path, including the Kalman filter (forward pass in src and the ForwardDiff extension); it reduces exactly to the previous behaviour when zero. The particle filter is a stochastic, non-differentiable estimator and errors clearly under reverse-mode AD (Zygote/Mooncake) and ForwardDiff, steering users to gradient-free samplers (Pigeons, nested sampling). Validated that all three variants reproduce the exact Kalman log-likelihood on a small RBC model and on the Smets-Wouters (2007) linear model / US data, up to the expected Var/2 finite-particle bias. New test set `particle_filter` (test_particle_filter.jl + test_particle_filter_sw07.jl), wired into CI. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HhhiqdZNkzbGZKJ8oSeoTp --- .github/workflows/ci.yml | 4 + docs/src/tutorials/estimation.md | 34 ++ ext/ForwardDiffExt.jl | 8 + src/MacroModelling.jl | 16 +- src/common_docstrings.jl | 2 +- src/default_options.jl | 14 + src/filter/kalman.jl | 29 +- src/filter/particle.jl | 654 ++++++++++++++++++++++++++++++ src/get_functions.jl | 155 ++++++- src/rrules.jl | 22 +- test/runtests.jl | 5 +- test/test_particle_filter.jl | 159 ++++++++ test/test_particle_filter_sw07.jl | 51 +++ 13 files changed, 1131 insertions(+), 22 deletions(-) create mode 100644 src/filter/particle.jl create mode 100644 test/test_particle_filter.jl create mode 100644 test/test_particle_filter_sw07.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbe5224eb..205e7a905 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,6 +173,10 @@ jobs: os: ubuntu-latest arch: x64 test_set: "system_prior_estimation" + - version: '1' + os: ubuntu-latest + arch: x64 + test_set: "particle_filter" steps: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v2 diff --git a/docs/src/tutorials/estimation.md b/docs/src/tutorials/estimation.md index 5ca35f258..56bca2e3a 100644 --- a/docs/src/tutorials/estimation.md +++ b/docs/src/tutorials/estimation.md @@ -254,3 +254,37 @@ plot_model_estimates(FS2000, data) ![Model estimates](../assets/estimates__FS2000__3.png) shows the variables of the model (blue), data (red), the shock decomposition for each endogenous variable and in the last panel the estimated shocks used to estimate the model. + +## Nonlinear estimation with the particle filter + +For genuinely nonlinear models the structural shocks can be integrated out by Monte Carlo using the particle filter (`filter = :particle`). It works for every perturbation order (`:first_order` through `:pruned_third_order`), requires measurement error on the observables (`measurement_error_std`), and is selected via `particle_filter_algorithm`: + +- `:bootstrap` — the sequential-importance-resampling filter (as in Dynare), +- `:auxiliary` — the Pitt–Shephard auxiliary particle filter, +- `:tempered` — the Herbst–Schorfheide tempered particle filter, which yields a much lower-variance likelihood estimate for the same number of particles. + +The particle-filter likelihood is a stochastic estimator and is **not** differentiable (resampling is discontinuous), so it must be used with gradient-free samplers such as the slice sampler in `Pigeons.jl` or nested sampling. Pass a seeded `rng` for reproducibility. + +```julia +using MacroModelling +import Pigeons, Random + +Random.seed!(1) + +Turing.@model function FS2000_particle(data, m) + parameters ~ Turing.product_distribution(prior_distributions) + Turing.@addlogprob! get_loglikelihood(m, data, parameters; + algorithm = :pruned_second_order, + filter = :particle, + particle_filter_algorithm = :tempered, + n_particles = 5000, + measurement_error_std = 1e-3, + rng = Random.Xoshiro(1), + on_failure_loglikelihood = -1e12) +end + +pt = Pigeons.pigeons(target = Pigeons.TuringLogPotential(FS2000_particle(data, FS2000)), + n_rounds = 8) +``` + +Because the likelihood is noisy, set `on_failure_loglikelihood` to a finite value (as above) so the sampler tolerates occasional failed evaluations, and prefer a larger `n_particles` (and the `:tempered` variant) to reduce the estimate's variance. diff --git a/ext/ForwardDiffExt.jl b/ext/ForwardDiffExt.jl index 72b3481b5..9b6892cb3 100644 --- a/ext/ForwardDiffExt.jl +++ b/ext/ForwardDiffExt.jl @@ -1009,6 +1009,7 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::ℱ.Dual{Z,S,N} where {Z,S,N,R <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -1072,6 +1073,13 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, ℒ.mul!(CP, C, P) ℒ.mul!(F_buf, CP, C') + # Add the diagonal measurement-error covariance H: F = C P C' + H. + if measurement_error_variances !== nothing + @inbounds for i in 1:no + F_buf[i, i] += measurement_error_variances[i] + end + end + luF = ℒ.lu(F_buf, check = false) if !ℒ.issuccess(luF) if opts.verbose println("KF factorisation failed step $t") end diff --git a/src/MacroModelling.jl b/src/MacroModelling.jl index f10ccf6b0..065f6fa9d 100644 --- a/src/MacroModelling.jl +++ b/src/MacroModelling.jl @@ -59,6 +59,9 @@ import MatrixEquations # good overview: https://cscproxy.mpi-magdeburg.mpg.de/mp # using NamedArrays import AxisKeys +import Random +import Random: AbstractRNG + import ChainRulesCore: rrule, NoTangent, @thunk, ProjectTo, unthunk, AbstractZero # import RecursiveFactorization as RF @@ -188,6 +191,7 @@ include("./algorithms/quadratic_matrix_equation.jl") include("./filter/find_shocks.jl") include("./filter/inversion.jl") include("./filter/kalman.jl") +include("./filter/particle.jl") export @model, @parameters, solve! @@ -393,7 +397,7 @@ function normalize_filtering_options(filter::Symbol, shock_decomposition::Bool, warmup_iterations::Int; maxlog::Int = DEFAULT_MAXLOG) - @assert filter ∈ [:kalman, :inversion] "Currently only the kalman filter (:kalman) for linear models and the inversion filter (:inversion) for linear and nonlinear models are supported." + @assert filter ∈ [:kalman, :inversion, :particle] "Currently only the Kalman filter (:kalman) for linear models, the inversion filter (:inversion) for linear and nonlinear models, and the particle filter (:particle) for linear and nonlinear models are supported." pruning = algorithm ∈ (:pruned_second_order, :pruned_third_order) @@ -402,8 +406,10 @@ function normalize_filtering_options(filter::Symbol, shock_decomposition = false end - if algorithm != :first_order && filter != :inversion - @info "Higher order solution algorithms only support the inversion filter. Setting `filter = :inversion`." maxlog = maxlog + # Higher-order solutions are handled by the inversion filter by default, but + # the particle filter (`:particle`) is explicitly valid at every order too. + if algorithm != :first_order && filter ∉ (:inversion, :particle) + @info "Higher order solution algorithms only support the inversion and particle filters. Setting `filter = :inversion`." maxlog = maxlog filter = :inversion end @@ -413,8 +419,8 @@ function normalize_filtering_options(filter::Symbol, end if warmup_iterations > 0 - if filter == :kalman - @info "`warmup_iterations` is not a valid argument for the Kalman filter. Ignoring input for `warmup_iterations`." maxlog = maxlog + if filter ∈ (:kalman, :particle) + @info "`warmup_iterations` is not a valid argument for the $(filter == :kalman ? "Kalman" : "particle") filter. Ignoring input for `warmup_iterations`." maxlog = maxlog warmup_iterations = 0 end end diff --git a/src/common_docstrings.jl b/src/common_docstrings.jl index ca39468f9..2a966e253 100644 --- a/src/common_docstrings.jl +++ b/src/common_docstrings.jl @@ -13,7 +13,7 @@ const GENERALISED_IRF® = "`generalised_irf` [Default: `$(DEFAULT_GENERALISED_IR const GENERALISED_IRF_WARMUP_ITERATIONS® = "`generalised_irf_warmup_iterations` [Default: `$(DEFAULT_GENERALISED_IRF_WARMUP)`, Type: `Int`]: number of warm-up iterations used to draw the baseline paths in the generalised IRF simulation. Only applied when `generalised_irf = true`." const GENERALISED_IRF_DRAWS® = "`generalised_irf_draws` [Default: `$(DEFAULT_GENERALISED_IRF_DRAWS)`, Type: `Int`]: number of Monte Carlo draws used to compute the generalised IRF. Only applied when `generalised_irf = true`." const ALGORITHM® = "`algorithm` [Default: `$(DEFAULT_ALGORITHM)`, Type: `Symbol`]: algorithm to solve for the dynamics of the model. Available algorithms: `:first_order`, `:second_order`, `:pruned_second_order`, `:third_order`, `:pruned_third_order`" -const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter only works for linear problems, whereas the inversion filter (`:inversion`) works for linear and nonlinear models. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically." +const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) only works for linear problems; the inversion filter (`:inversion`) works for linear and nonlinear models; the particle filter (`:particle`) works for linear and nonlinear models and integrates out the structural shocks by Monte Carlo (it requires measurement error via `measurement_error_std`, is selected via `particle_filter_algorithm`, and is a stochastic, non-differentiable estimator suited to gradient-free samplers). If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically." const LEVELS® = "return levels or absolute deviations from the relevant steady state corresponding to the solution algorithm (e.g. stochastic steady state for higher order solution algorithms)." const CONDITIONS® = "`conditions` [Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}}`]: conditions for which to find the corresponding shocks. The input can have multiple formats, but for all types of entries, the first dimension corresponds to variables and the second dimension to the number of periods. The conditions can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the conditions are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as conditions. Note that conditioning variables to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input conditions is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as conditions and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of variables (of type `Symbol` or `String`) for which conditions are specified can be included and all other variables are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the conditions for the specified variables bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const SHOCK_CONDITIONS® = "`shocks` [Default: `nothing`, Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}, Nothing}`]: known values of shocks. This argument allows including certain shock values. By entering restrictions on the shocks in this way the problem to match the conditions on endogenous variables is restricted to the remaining free shocks in the respective period. The input can have multiple formats, but for all types of entries, the first dimension corresponds to shocks and the second dimension to the number of periods. `shocks` can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the shocks are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as certain shock values. Note that conditioning shocks to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input known shocks is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as known shocks and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of shocks (of type `Symbol` or `String`) for which values are specified can be included and all other shocks are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the values for the specified shocks bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." diff --git a/src/default_options.jl b/src/default_options.jl index 8ccc061f4..9f2af1b9b 100644 --- a/src/default_options.jl +++ b/src/default_options.jl @@ -8,6 +8,20 @@ const DEFAULT_SHOCK_DECOMPOSITION_SELECTOR = algorithm -> algorithm ∉ (:second const DEFAULT_SMOOTH_SELECTOR = filter -> filter == :kalman const DEFAULT_WARMUP_ITERATIONS = 0 const DEFAULT_PRESAMPLE_PERIODS = 0 +const DEFAULT_MEASUREMENT_ERROR_STD = 0.0 + +# Particle filter defaults (see `src/filter/particle.jl`) +const DEFAULT_N_PARTICLES = 1000 +const DEFAULT_PARTICLE_FILTER_ALGORITHM = :bootstrap +const DEFAULT_RESAMPLING = :systematic +const DEFAULT_RESAMPLING_THRESHOLD = 0.5 +const DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR = 1.0 +# Tempered particle filter (Herbst & Schorfheide, 2019) controls +const DEFAULT_TEMPERING_TARGET_RATIO = 2.0 +const DEFAULT_TEMPERING_MH_STEPS = 1 +const DEFAULT_TEMPERING_MAX_STAGES = 100 +const DEFAULT_TEMPERING_MH_SCALE = 0.3 + const DEFAULT_DATA_IN_LEVELS = true const DEFAULT_LEVELS = true const DEFAULT_CONDITIONS_IN_LEVELS = true diff --git a/src/filter/kalman.jl b/src/filter/kalman.jl index 1587e507f..1c2a1795b 100644 --- a/src/filter/kalman.jl +++ b/src/filter/kalman.jl @@ -16,6 +16,7 @@ function calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -60,8 +61,8 @@ function calculate_loglikelihood(::Val{:kalman}, # initial_state at the get_loglikelihood level. u₀ = state[1][observables_and_states] - return run_kalman_iterations(A, 𝐁, C, P, data_in_deviations, kalman_ws, u₀, presample_periods = presample_periods, verbose = opts.verbose, on_failure_loglikelihood = on_failure_loglikelihood) - # timer = timer, + return run_kalman_iterations(A, 𝐁, C, P, data_in_deviations, kalman_ws, u₀, presample_periods = presample_periods, verbose = opts.verbose, on_failure_loglikelihood = on_failure_loglikelihood, measurement_error_variances = measurement_error_variances) + # timer = timer, end @@ -80,6 +81,7 @@ function calculate_loglikelihood_with_missing(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -110,7 +112,8 @@ function calculate_loglikelihood_with_missing(::Val{:kalman}, obs_idx_per_t, kalman_ws, u₀, presample_periods = presample_periods, verbose = opts.verbose, - on_failure_loglikelihood = on_failure_loglikelihood) + on_failure_loglikelihood = on_failure_loglikelihood, + measurement_error_variances = measurement_error_variances) end # Specialization for :theoretical @@ -150,6 +153,7 @@ function run_kalman_iterations(A::Matrix{S}, u₀::AbstractVector{V}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, # timer::TimerOutput = TimerOutput(), verbose::Bool = false) where {S <: Real, R <: Real, V <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -207,6 +211,15 @@ function run_kalman_iterations(A::Matrix{S}, ℒ.mul!(Ctmp, C, Pwork) # Ctmp = C * P ℒ.mul!(F, Ctmp, C') # F = C * P * C' + # Add the diagonal measurement-error covariance H: F = C P C' + H. + # `measurement_error_variances` holds the per-observable variances in the + # innovation (data-row) order, which matches F's rows. + if measurement_error_variances !== nothing + @inbounds for i in 1:n_obs + F[i, i] += measurement_error_variances[i] + end + end + if T === Float64 ws.fast_lu_ws_f, ws.fast_lu_dims_f, solved_F, luF = factorize_lu!(Val(:FastLapack), F, ws.fast_lu_ws_f, @@ -296,9 +309,10 @@ function run_kalman_iterations_missing(A::Matrix{S}, data_in_deviations::Matrix{S}, obs_idx_per_t::Vector{Vector{Int}}, ws::kalman_workspace, - u₀::AbstractVector{<:Real}; + u₀::AbstractVector{<:Real}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, verbose::Bool = false)::S where {S <: Float64, R <: Real, U <: AbstractFloat} n_obs = size(C, 1) @@ -360,6 +374,13 @@ function run_kalman_iterations_missing(A::Matrix{S}, ℒ.mul!(Ctv, Cv, P) # Ctv = C[idx,:] * P ℒ.mul!(Fv, Ctv, Cv') # Fv = C[idx,:] * P * C[idx,:]' + # Add the diagonal measurement-error covariance for the observed rows. + if measurement_error_variances !== nothing + @inbounds for i in 1:m + Fv[i, i] += measurement_error_variances[idx[i]] + end + end + ws.fast_lu_ws_f, ws.fast_lu_dims_f, solved_F, luF = factorize_lu!(Val(:Julia), Fv, ws.fast_lu_ws_f, ws.fast_lu_dims_f) diff --git a/src/filter/particle.jl b/src/filter/particle.jl new file mode 100644 index 000000000..cd19bbcc5 --- /dev/null +++ b/src/filter/particle.jl @@ -0,0 +1,654 @@ +@stable default_mode = "disable" begin + +# Particle filters for the (possibly nonlinear) DSGE state-space representation. +# +# The measurement equation is yₜ = full_stateₜ[observables] + ηₜ, ηₜ ~ N(0, H), +# with H a diagonal matrix of measurement-error variances. The structural shocks +# are i.i.d. standard normal (their standard deviations are baked into the +# solution matrices 𝐒), and the state transition is the perturbation solution's +# `state_update` (first order through pruned third order). +# +# Three variants are provided, selected by `particle_filter_algorithm`: +# :bootstrap — sequential-importance-resampling (as in Dynare 7's +# `sequential_importance_particle_filter.m`) +# :auxiliary — Pitt & Shephard (1999) auxiliary particle filter +# :tempered — Herbst & Schorfheide (2019) tempered particle filter +# +# The particle filter is a stochastic likelihood estimator and is **not** +# differentiable (resampling is discontinuous); it is intended for use with +# gradient-free samplers (e.g. Pigeons slice sampling, nested sampling). + + +# ── Resampling schemes ─────────────────────────────────────────────────────── +# Each returns a length-N vector of ancestor indices drawn from the normalised +# weights `W` (which must sum to one). Systematic/stratified have lower variance +# than multinomial and are the recommended defaults. + +# Effective sample size 1 / Σ Wᵢ². +effective_sample_size(W::AbstractVector{<:Real}) = 1.0 / sum(abs2, W) + +function systematic_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + idxs = Vector{Int}(undef, N) + u0 = rand(rng) / N + c = W[1] + i = 1 + @inbounds for j in 1:N + u = u0 + (j - 1) / N + while u > c && i < N + i += 1 + c += W[i] + end + idxs[j] = i + end + return idxs +end + +function stratified_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + idxs = Vector{Int}(undef, N) + c = W[1] + i = 1 + @inbounds for j in 1:N + u = (j - 1 + rand(rng)) / N + while u > c && i < N + i += 1 + c += W[i] + end + idxs[j] = i + end + return idxs +end + +function multinomial_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + c = cumsum(W) + c[end] = one(eltype(c)) # guard against round-off so rand() ≤ c[end] + idxs = Vector{Int}(undef, N) + @inbounds for j in 1:N + idxs[j] = searchsortedfirst(c, rand(rng)) + end + return idxs +end + +function residual_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + idxs = Vector{Int}(undef, N) + counts = floor.(Int, N .* W) + k = 0 + @inbounds for i in 1:N + for _ in 1:counts[i] + k += 1 + idxs[k] = i + end + end + R = N - k + if R > 0 + resid = N .* W .- counts + s = sum(resid) + if s <= 0 # numerical degeneracy: fall back to multinomial + c = cumsum(W); c[end] = one(eltype(c)) + @inbounds for _ in 1:R + k += 1 + idxs[k] = searchsortedfirst(c, rand(rng)) + end + else + resid ./= s + c = cumsum(resid); c[end] = one(eltype(c)) + @inbounds for _ in 1:R + k += 1 + idxs[k] = searchsortedfirst(c, rand(rng)) + end + end + end + return idxs +end + +function particle_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) + if scheme == :systematic + return systematic_resample_indices(rng, W) + elseif scheme == :stratified + return stratified_resample_indices(rng, W) + elseif scheme == :multinomial + return multinomial_resample_indices(rng, W) + elseif scheme == :residual + return residual_resample_indices(rng, W) + else + error("Unknown resampling scheme `:$scheme`. Choose from `:systematic`, `:stratified`, `:multinomial`, `:residual`.") + end +end + + +# ── Shared setup ───────────────────────────────────────────────────────────── + +# Covariance used to spread the initial particle cloud over the full state. +# `:theoretical` (default) uses the first-order ergodic (unconditional) state +# covariance Σ solving the discrete Lyapunov equation Σ = A Σ A' + B B' (built +# from the cached first-order solution, as in Dynare); `:diagonal` uses 10·I; an +# nVars×nVars matrix is used directly. +function particle_initial_state_covariance(𝓂::ℳ, T, opts::CalculationOptions, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}}) + nVars = T.nVars + + if initial_covariance isa AbstractMatrix && size(initial_covariance) == (nVars, nVars) + return Matrix{Float64}(initial_covariance), true + elseif initial_covariance === :diagonal + return Matrix{Float64}(10.0 * ℒ.I(nVars)), true + end + + nPast = T.nPast_not_future_and_mixed + past_idx = T.past_not_future_and_mixed_idx + + S₁ = 𝓂.caches.first_order_solution_matrix + + A_full = zeros(Float64, nVars, nVars) + @views A_full[:, past_idx] .= S₁[:, 1:nPast] + B_full = @views Matrix{Float64}(S₁[:, nPast+1:end]) + 𝐁 = B_full * B_full' + + lyap_ws = ensure_lyapunov_workspace!(𝓂.workspaces, nVars, :first_order) + Σ, solved = solve_lyapunov_equation(A_full, 𝐁, lyap_ws, + lyapunov_algorithm = opts.lyapunov_algorithm, + tol = opts.tol.first_order.lyapunov, + verbose = opts.verbose) + + return solved ? Matrix{Float64}(Σ) : Matrix{Float64}(ℒ.I(nVars)), solved +end + +# Lower-triangular factor L (L Lᵀ ≈ scaling·Σ) for sampling the initial cloud. +# Falls back to a diagonal factor if Σ is not numerically positive definite. +function particle_initial_cloud_factor(Σ::Matrix{Float64}, scaling::Float64) + nVars = size(Σ, 1) + Σs = ℒ.Symmetric(scaling .* Σ) + jitter = 1e-12 * (ℒ.tr(Σs) / max(nVars, 1) + 1.0) + chol = ℒ.cholesky(Σs + jitter * ℒ.I(nVars), check = false) + if ℒ.issuccess(chol) + return Matrix{Float64}(chol.L) + else + return ℒ.diagm(sqrt.(max.(scaling .* ℒ.diag(Σ), 0.0))) + end +end + +# Build the initial particle cloud. Each particle carries the same representation +# `state_update` consumes: a flat `Vector` for non-pruned orders, or a +# `Vector{Vector}` (first-order + higher-order components) for pruned orders. The +# first-order part is randomised around the initial mean with covariance +# `scaling·Σ`; higher-order pruned components are initialised deterministically. +function initialise_particles(rng::Random.AbstractRNG, state, pruning::Bool, + L::Matrix{Float64}, n_particles::Int, nVars::Int) + if pruning + mean1 = Vector{Float64}(state[1]) + rest = [Vector{Float64}(state[c]) for c in 2:length(state)] + return [vcat([mean1 .+ L * randn(rng, nVars)], [copy(r) for r in rest]) for _ in 1:n_particles] + else + mean1 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) + return [mean1 .+ L * randn(rng, nVars) for _ in 1:n_particles] + end +end + +# Full (summed) model state a particle reports to the measurement equation. +@inline particle_full_state(p, pruning::Bool) = pruning ? sum(p) : p + +# Log Gaussian measurement density of the observed rows for one particle's +# predicted observables, with diagonal measurement-error variances `me_var`. +# `rows` indexes the observed observables at the current period; returns -Inf on +# a non-finite prediction. +@inline function particle_log_measurement_density(full::AbstractVector, data_col, observables_index, + me_var, rows, log2pi::Float64) + q = 0.0 + @inbounds for r in rows + f = full[observables_index[r]] + isfinite(f) || return -Inf + v = data_col[r] - f + q += v * v / me_var[r] + log2pi + log(me_var[r]) + end + return -0.5 * q +end + + +# ── Bootstrap (sequential importance resampling) particle filter ───────────── + +function run_particle_filter(::Val{algo}, + ::Val{:bootstrap}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + + state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + particles = initialise_particles(rng, state, pruning, L, n_particles, nVars) + + W = fill(1.0 / n_particles, n_particles) + logdens = Vector{Float64}(undef, n_particles) + loglik = 0.0 + + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + + if isempty(rows) + # No observation this period: propagate only, weights unchanged. + @inbounds for p in 1:n_particles + particles[p] = state_update(particles[p], randn(rng, nExo)) + end + continue + end + + @inbounds for p in 1:n_particles + particles[p] = state_update(particles[p], randn(rng, nExo)) + full = particle_full_state(particles[p], pruning) + logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) + end + + m = maximum(logdens) + if !isfinite(m) + return Float64(on_failure_loglikelihood) + end + + s = 0.0 + @inbounds for p in 1:n_particles + s += W[p] * exp(logdens[p] - m) + end + if s <= 0 || !isfinite(s) + return Float64(on_failure_loglikelihood) + end + + ll_t = m + log(s) + if t > presample_periods + loglik += ll_t + end + + @inbounds for p in 1:n_particles + W[p] = W[p] * exp(logdens[p] - m) / s + end + + if effective_sample_size(W) < resampling_threshold * n_particles + idxs = particle_resample_indices(rng, W, resampling) + # `state_update` never mutates its argument in place, so sharing the + # underlying arrays across duplicated ancestors is safe (each slot is + # reassigned, never mutated, on the next propagation). + particles = particles[idxs] + fill!(W, 1.0 / n_particles) + end + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + + +# ── Shared measurement helpers for the auxiliary and tempered filters ──────── + +# Quadratic form eᵀH⁻¹e over the observed rows, with diagonal H (variances +# `me_var`). Returns Inf on a non-finite prediction (an impossible particle). +@inline function particle_quadratic_form(full::AbstractVector, data_col, observables_index, me_var, rows) + q = 0.0 + @inbounds for r in rows + f = full[observables_index[r]] + isfinite(f) || return Inf + v = data_col[r] - f + q += v * v / me_var[r] + end + return q +end + +# Log normalising constant of the Gaussian measurement density over the observed +# rows: -½(dₒ·log2π + Σ log me_var[r]). +@inline function particle_measurement_logZ(me_var, rows, log2pi::Float64) + z = 0.0 + @inbounds for r in rows + z += log2pi + log(me_var[r]) + end + return -0.5 * z +end + + +# ── Auxiliary particle filter (Pitt & Shephard, 1999) ──────────────────────── +# A look-ahead stage reweights ancestors by the predictive likelihood evaluated +# at the transition mean (zero shock) before propagating, reducing variance when +# the signal is informative. The likelihood estimate remains unbiased. + +function run_particle_filter(::Val{algo}, + ::Val{:auxiliary}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + + state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + + # One-step-ahead predictive variance of each observable due to the structural + # shocks (diagonal of Cₒ BBᵀ Cₒᵀ from the first-order shock loading), used to + # scale the auxiliary first-stage weights. Evaluating the predictive density + # at the transition mean alone would be near-degenerate when the observable is + # shock-driven; inflating by the shock spread keeps the proxy well-conditioned. + nPast = T.nPast_not_future_and_mixed + S₁cache = 𝓂.caches.first_order_solution_matrix + pred_var = Float64[sum(abs2, @view S₁cache[observables_index[i], nPast+1:end]) for i in eachindex(observables_index)] .+ me_var + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + particles = initialise_particles(rng, state, pruning, L, n_particles, nVars) + + zero_shock = zeros(Float64, nExo) + W = fill(1.0 / n_particles, n_particles) + logg̃ = Vector{Float64}(undef, n_particles) # first-stage predictive log-density + logw = Vector{Float64}(undef, n_particles) # second-stage log-weight + loglik = 0.0 + + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + + # First stage: predictive density at the transition mean (zero shock), + # spread by the shock-induced predictive variance `pred_var`. + @inbounds for p in 1:n_particles + μ = particle_full_state(state_update(particles[p], zero_shock), pruning) + logg̃[p] = particle_log_measurement_density(μ, data_col, observables_index, pred_var, rows, log2pi) + end + + # First-stage (auxiliary) weights λ ∝ W · g̃, and κ = Σ W·g̃. + mλ = -Inf + @inbounds for p in 1:n_particles + lλ = log(W[p]) + logg̃[p] + mλ = lλ > mλ ? lλ : mλ + end + if !isfinite(mλ) + return Float64(on_failure_loglikelihood) + end + sλ = 0.0 + @inbounds for p in 1:n_particles + sλ += exp(log(W[p]) + logg̃[p] - mλ) + end + logκ = mλ + log(sλ) + λ = Vector{Float64}(undef, n_particles) + @inbounds for p in 1:n_particles + λ[p] = exp(log(W[p]) + logg̃[p] - logκ) + end + + # Resample ancestors ∝ λ, propagate with fresh shocks, second-stage weight + # w = g(yₜ|xₜ) / g̃(ancestor). + idx = particle_resample_indices(rng, λ, resampling) + newparts = Vector{eltype(particles)}(undef, n_particles) + @inbounds for j in 1:n_particles + a = idx[j] + newparts[j] = state_update(particles[a], randn(rng, nExo)) + full = particle_full_state(newparts[j], pruning) + logw[j] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) - logg̃[a] + end + + mw = maximum(logw) + if !isfinite(mw) + return Float64(on_failure_loglikelihood) + end + sw = 0.0 + @inbounds for j in 1:n_particles + sw += exp(logw[j] - mw) + end + if sw <= 0 || !isfinite(sw) + return Float64(on_failure_loglikelihood) + end + + ll_t = logκ + (mw + log(sw) - log(n_particles)) + if t > presample_periods + loglik += ll_t + end + + logsw = mw + log(sw) + @inbounds for j in 1:n_particles + W[j] = exp(logw[j] - logsw) + end + particles = newparts + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + + +# ── Tempered particle filter (Herbst & Schorfheide, 2019) ──────────────────── +# Within each period the measurement information is introduced gradually through +# a bridging sequence 0 = φ₀ < φ₁ < … < φ_N = 1 (the measurement covariance is +# inflated to H/φ). Each stage reweights by the tempered density increment, +# resamples, and mutates the particles' shocks with a random-walk Metropolis step +# targeting the stage-φ posterior. This dramatically lowers the variance of the +# likelihood estimate relative to the bootstrap filter at equal particle count. + +# Inefficiency ratio N·Σ(wᵖ)² / (Σwᵖ)² for the incremental weights +# wᵖ = exp(-(φ-φ_old)/2 · dᵖ). Increasing in φ, equal to 1 at φ = φ_old. +function tempered_inefficiency(φ::Float64, φ_old::Float64, d::Vector{Float64}, n_particles::Int) + Δ = (φ - φ_old) / 2 + maxla = -Inf + @inbounds for p in 1:n_particles + la = isfinite(d[p]) ? -Δ * d[p] : -Inf + maxla = la > maxla ? la : maxla + end + isfinite(maxla) || return Inf + S1 = 0.0 + S2 = 0.0 + @inbounds for p in 1:n_particles + if isfinite(d[p]) + e = exp(-Δ * d[p] - maxla) + S1 += e + S2 += e * e + end + end + return S1 > 0 ? n_particles * S2 / (S1 * S1) : Inf +end + +# Next tempering level in (φ_old, 1] targeting inefficiency `r_star` by bisection. +function tempered_next_phi(φ_old::Float64, d::Vector{Float64}, r_star::Float64, n_particles::Int) + if tempered_inefficiency(1.0, φ_old, d, n_particles) <= r_star + return 1.0 + end + lo = φ_old + hi = 1.0 + for _ in 1:100 + mid = 0.5 * (lo + hi) + if tempered_inefficiency(mid, φ_old, d, n_particles) < r_star + lo = mid + else + hi = mid + end + hi - lo < 1e-8 && break + end + return 0.5 * (lo + hi) +end + +function run_particle_filter(::Val{algo}, + ::Val{:tempered}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + + r_star = Float64(tempering_target_ratio) + c = Float64(tempering_mh_scale) + n_mh = tempering_mh_steps + max_stages = tempering_max_stages + + state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + prev_states = initialise_particles(rng, state, pruning, L, n_particles, nVars) + + logw = Vector{Float64}(undef, n_particles) + loglik = 0.0 + + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + d_obs = length(rows) + + # Bootstrap proposal: propagate every ancestor with a fresh shock. + ancestors = prev_states + shocks = [randn(rng, nExo) for _ in 1:n_particles] + states = Vector{eltype(prev_states)}(undef, n_particles) + dvec = Vector{Float64}(undef, n_particles) + @inbounds for p in 1:n_particles + states[p] = state_update(ancestors[p], shocks[p]) + dvec[p] = particle_quadratic_form(particle_full_state(states[p], pruning), data_col, observables_index, me_var, rows) + end + + if all(!isfinite, dvec) + return Float64(on_failure_loglikelihood) + end + + period_ll = 0.0 + φ_old = 0.0 + stage = 0 + while φ_old < 1.0 - 1e-12 && stage < max_stages + stage += 1 + φ_new = tempered_next_phi(φ_old, dvec, r_star, n_particles) + + # Incremental (tempered) log-weights. + if φ_old == 0.0 + logZ = particle_measurement_logZ(me_var, rows, log2pi) + @inbounds for p in 1:n_particles + logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dvec[p] + end + else + lr = 0.5 * d_obs * (log(φ_new) - log(φ_old)) + @inbounds for p in 1:n_particles + logw[p] = lr - 0.5 * (φ_new - φ_old) * dvec[p] + end + end + + m = maximum(logw) + if !isfinite(m) + return Float64(on_failure_loglikelihood) + end + s = 0.0 + @inbounds for p in 1:n_particles + s += exp(logw[p] - m) + end + if s <= 0 || !isfinite(s) + return Float64(on_failure_loglikelihood) + end + period_ll += m + log(s) - log(n_particles) + + # Normalise and resample. + logsw = m + log(s) + Wn = Vector{Float64}(undef, n_particles) + @inbounds for p in 1:n_particles + Wn[p] = exp(logw[p] - logsw) + end + idx = particle_resample_indices(rng, Wn, resampling) + ancestors = ancestors[idx] + shocks = shocks[idx] + states = states[idx] + dvec = dvec[idx] + + # Mutation: random-walk Metropolis on the shocks, targeting the + # stage-φ posterior π(ε) ∝ N(ε;0,I) · exp(-φ/2 · e(ε)ᵀH⁻¹e(ε)). + @inbounds for p in 1:n_particles + for _ in 1:n_mh + εp = shocks[p] + εprop = εp .+ c .* randn(rng, nExo) + sprop = state_update(ancestors[p], εprop) + dprop = particle_quadratic_form(particle_full_state(sprop, pruning), data_col, observables_index, me_var, rows) + logα = -0.5 * ((sum(abs2, εprop) - sum(abs2, εp)) + φ_new * (dprop - dvec[p])) + if log(rand(rng)) < logα + shocks[p] = εprop + states[p] = sprop + dvec[p] = dprop + end + end + end + + φ_old = φ_new + end + + if t > presample_periods + loglik += period_ll + end + prev_states = states + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + +end # @stable diff --git a/src/get_functions.jl b/src/get_functions.jl index a2cf09c4c..24202064d 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -4297,6 +4297,29 @@ function get_statistics(𝓂::ℳ, return ret end +# Validate `measurement_error_std` supplied to the filter-based `get_loglikelihood` +# path and return the per-observable measurement-error variances (in the observable +# / data-row order), or `nothing` when no measurement error is active (all zero). +# A scalar is broadcast to all observables; a per-observable vector is used as is. +# Time-varying (matrix) measurement error is not yet supported on this path. +function build_filter_measurement_error_variances(measurement_error_std, n_obs::Int) + if measurement_error_std isa AbstractMatrix + error("Time-varying (matrix) `measurement_error_std` is not yet supported on the filter-based `get_loglikelihood` path (`filter = :kalman` / `:particle`); provide a scalar or a per-observable vector.") + end + + stds = measurement_error_std isa AbstractVector ? collect(float.(measurement_error_std)) : fill(float(measurement_error_std), n_obs) + + @assert length(stds) == n_obs || length(stds) == 1 "`measurement_error_std` vector must have one entry per observable (got $(length(stds)), expected $n_obs) or a single entry that is broadcast to all observables." + + if length(stds) == 1 && n_obs > 1 + stds = fill(stds[1], n_obs) + end + + @assert all(s -> isfinite(s) && s >= 0, stds) "`measurement_error_std` entries must be finite and non-negative." + + return any(s -> s > 0, stds) ? stds .^ 2 : nothing +end + """ $(SIGNATURES) Return the loglikelihood of the model given the data and parameters provided. The loglikelihood is either calculated based on the inversion or the Kalman filter (depending on the `filter` keyword argument). By default the package selects the Kalman filter for first order solutions and the inversion filter for nonlinear (higher order) solution algorithms. The data must be provided as a `KeyedArray{Float64}` with the names of the variables to be matched in rows and the periods in columns. The `KeyedArray` type is provided by the `AxisKeys` package. @@ -4318,6 +4341,13 @@ If occasionally binding constraints are present in the model, they are not taken - `initial_covariance` [Default: `:theoretical`, Type: `Union{Symbol,AbstractMatrix{<:Real}}`]: defines the method to initialise the Kalman filters covariance matrix. It can be initialised with the theoretical long run values (option `:theoretical`), large values (10.0) along the diagonal (option `:diagonal`), or a user-supplied matrix of appropriate size (number of observables and states). - $INITIAL_STATE® - `on_failure_loglikelihood` [Default: `-Inf`, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails. Setting this to a finite value can avoid errors in codes that rely on finite loglikelihood values, such as e.g. slice samplers (in Pigeons.jl). +- `measurement_error_std` [Default: `0.0`, Type: `Union{Real,AbstractVector{<:Real}}`]: standard deviation of Gaussian measurement error on the observables. A scalar is broadcast to all observables; a vector supplies one entry per observable. The default `0.0` disables measurement error (the previous behaviour). Measurement error is supported by the Kalman filter (`filter = :kalman`) and is required by the particle filter (`filter = :particle`); it is not available for the inversion filter. +- `n_particles` [Default: `1000`, Type: `Int`]: number of particles used when `filter = :particle`. +- `particle_filter_algorithm` [Default: `:bootstrap`, Type: `Symbol`]: particle filter variant when `filter = :particle`. One of `:bootstrap` (sequential-importance-resampling, as in Dynare), `:auxiliary` (Pitt–Shephard auxiliary particle filter), or `:tempered` (Herbst–Schorfheide tempered particle filter). +- `resampling` [Default: `:systematic`, Type: `Symbol`]: resampling scheme for the particle filter. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`. +- `resampling_threshold` [Default: `0.5`, Type: `Real`]: the particle filter resamples whenever the effective sample size falls below `resampling_threshold * n_particles`. +- `initial_state_prior_scaling_factor` [Default: `1.0`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state. +- `rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filter (pass a seeded RNG for reproducibility). - $QME® - $SYLVESTER® - $LYAPUNOV® @@ -4325,7 +4355,7 @@ If occasionally binding constraints are present in the model, they are not taken - $VERBOSE® # Returns -- `<:AbstractFloat` loglikelihood +- `<:AbstractFloat` loglikelihood # Examples ```jldoctest @@ -4362,13 +4392,24 @@ function get_loglikelihood(𝓂::ℳ, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), on_failure_loglikelihood::U = -Inf, - warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, + warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - tol::Tolerances = Tolerances(), - quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), - lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, + measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_filter_algorithm::Symbol = DEFAULT_PARTICLE_FILTER_ALGORITHM, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + tol::Tolerances = Tolerances(), + quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), + lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, sylvester_algorithm::Union{Symbol,Vector{Symbol},Tuple{Symbol,Vararg{Symbol}}} = DEFAULT_SYLVESTER_SELECTOR(𝓂), verbose::Bool = DEFAULT_VERBOSE, caching::Bool = DEFAULT_CACHING, @@ -4385,6 +4426,17 @@ function get_loglikelihood(𝓂::ℳ, presample_periods = presample_periods, initial_covariance = initial_covariance, filter_algorithm = filter_algorithm, + measurement_error_std = measurement_error_std, + n_particles = n_particles, + particle_filter_algorithm = particle_filter_algorithm, + resampling = resampling, + resampling_threshold = resampling_threshold, + initial_state_prior_scaling_factor = initial_state_prior_scaling_factor, + rng = rng, + tempering_target_ratio = tempering_target_ratio, + tempering_mh_steps = tempering_mh_steps, + tempering_max_stages = tempering_max_stages, + tempering_mh_scale = tempering_mh_scale, tol = tol, quadratic_matrix_equation_algorithm = quadratic_matrix_equation_algorithm, lyapunov_algorithm = lyapunov_algorithm, @@ -4406,6 +4458,17 @@ function get_loglikelihood(𝓂::ℳ, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, + measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_filter_algorithm::Symbol = DEFAULT_PARTICLE_FILTER_ALGORITHM, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, @@ -4540,9 +4603,89 @@ function get_loglikelihood(𝓂::ℳ, return zero(S) end + # Diagonal Gaussian measurement-error variances (per observable, in data-row + # order), or `nothing` when no measurement error is active. Supported by the + # Kalman and particle filters; the inversion filter recovers shocks exactly + # and does not admit measurement error. + measurement_error_variances = build_filter_measurement_error_variances(measurement_error_std, size(data_in_deviations, 1)) + + if filter == :inversion && measurement_error_variances !== nothing + error("`measurement_error_std` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or `filter = :particle`.") + end + if filter == :particle + if measurement_error_variances === nothing + error("The particle filter (`filter = :particle`) requires measurement error; set `measurement_error_std` to a positive value (scalar or per-observable vector).") + end + # The particle filter evaluates in Float64 and is not differentiable; a + # forward-mode `Dual` parameter type would silently yield a zero gradient. + if !(S <: AbstractFloat) + error("The particle filter (`filter = :particle`) is not differentiable and cannot be used with automatic differentiation. Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") + end + end + # @timeit_debug timer "Filter" begin - llh = if has_missing + llh = if filter == :particle + run_particle_filter(Val(algorithm), + Val(particle_filter_algorithm), + obs_indices, + 𝐒, + data_in_deviations, + constants_obj, + state, + 𝓂, + measurement_error_variances, + obs_idx_per_t, + has_missing; + n_particles = n_particles, + resampling = resampling, + resampling_threshold = resampling_threshold, + initial_state_prior_scaling_factor = initial_state_prior_scaling_factor, + rng = rng, + presample_periods = presample_periods, + initial_covariance = initial_covariance, + on_failure_loglikelihood = on_failure_loglikelihood, + tempering_target_ratio = tempering_target_ratio, + tempering_mh_steps = tempering_mh_steps, + tempering_max_stages = tempering_max_stages, + tempering_mh_scale = tempering_mh_scale, + opts = opts) + elseif filter == :kalman + if has_missing + calculate_loglikelihood_with_missing(Val(:kalman), + Val(algorithm), + obs_indices, + 𝐒, + data_in_deviations, + constants_obj, + state, + 𝓂.workspaces, + obs_idx_per_t, + warmup_iterations = warmup_iterations, + presample_periods = presample_periods, + initial_covariance = initial_covariance, + filter_algorithm = filter_algorithm, + measurement_error_variances = measurement_error_variances, + opts = opts, + on_failure_loglikelihood = on_failure_loglikelihood) + else + calculate_loglikelihood(Val(:kalman), + Val(algorithm), + obs_indices, + 𝐒, + data_in_deviations, + constants_obj, + state, + 𝓂.workspaces, + warmup_iterations = warmup_iterations, + presample_periods = presample_periods, + initial_covariance = initial_covariance, + filter_algorithm = filter_algorithm, + measurement_error_variances = measurement_error_variances, + opts = opts, + on_failure_loglikelihood = on_failure_loglikelihood) + end + elseif has_missing calculate_loglikelihood_with_missing(Val(filter), Val(algorithm), obs_indices, diff --git a/src/rrules.jl b/src/rrules.jl index 1e2aedaf0..ba3ee74f7 100644 --- a/src/rrules.jl +++ b/src/rrules.jl @@ -1850,11 +1850,27 @@ function rrule(::typeof(get_loglikelihood), presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, + measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, sylvester_algorithm::Union{Symbol,Vector{Symbol},Tuple{Symbol,Vararg{Symbol}}} = DEFAULT_SYLVESTER_SELECTOR(𝓂), - verbose::Bool = DEFAULT_VERBOSE) where {S <: Real, V <: Real, U <: AbstractFloat} + verbose::Bool = DEFAULT_VERBOSE, + kwargs...) where {S <: Real, V <: Real, U <: AbstractFloat} + + estimation = true + + filter, _, algorithm, _, _, warmup_iterations = normalize_filtering_options(filter, false, algorithm, false, warmup_iterations) + + # The particle filter is a stochastic, non-differentiable estimator and the + # Kalman likelihood with measurement error does not yet ship an analytical + # reverse-mode rule. Fail loudly rather than return an incorrect gradient. + if filter == :particle + error("The particle filter (`filter = :particle`) is not differentiable and cannot be used with reverse-mode automatic differentiation (Zygote/Mooncake). Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") + end + if measurement_error_std isa AbstractArray ? any(x -> x != 0, measurement_error_std) : measurement_error_std != 0 + error("Reverse-mode automatic differentiation of the Kalman likelihood with measurement error (`measurement_error_std`) is not yet supported. Use forward-mode AD (e.g. `AutoForwardDiff`) or a gradient-free sampler.") + end opts = merge_calculation_options(tol = tol, verbose = verbose, quadratic_matrix_equation_algorithm = quadratic_matrix_equation_algorithm, @@ -1862,10 +1878,6 @@ function rrule(::typeof(get_loglikelihood), sylvester_algorithm³ = (isa(sylvester_algorithm, Symbol) || length(sylvester_algorithm) < 2) ? sum(k * (k + 1) ÷ 2 for k in 1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed + 1 + 𝓂.constants.post_model_macro.nExo) > DEFAULT_SYLVESTER_THRESHOLD ? DEFAULT_LARGE_SYLVESTER_ALGORITHM : DEFAULT_SYLVESTER_ALGORITHM : sylvester_algorithm[2], lyapunov_algorithm = lyapunov_algorithm) - estimation = true - - filter, _, algorithm, _, _, warmup_iterations = normalize_filtering_options(filter, false, algorithm, false, warmup_iterations) - observables = get_and_check_observables(𝓂.constants.post_model_macro, data) solve!(𝓂, opts = opts, steady_state_function = steady_state_function, algorithm = algorithm) diff --git a/test/runtests.jl b/test/runtests.jl index 9c6e1447d..2709478c9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,7 +2,7 @@ # using Revise test_set = ENV["TEST_SET"] using Preferences: set_preferences! -set_preferences!("MacroModelling", "dispatch_doctor_mode" => test_set in ["estimate_sw07", "estimate_sw07_nested_sampling", "estimation", "1st_order_inversion_estimation", "pruned_2nd_order_estimation", "2nd_order_estimation", "pruned_3rd_order_estimation", "3rd_order_estimation", "estimation_pigeons", "1st_order_inversion_estimation_pigeons", "2nd_order_estimation_pigeons", "pruned_2nd_order_estimation_pigeons", "3rd_order_estimation_pigeons", "pruned_3rd_order_estimation_pigeons", "system_prior_estimation", "gradient_checks", "missing_data", "jet", "jet_hot_paths" +set_preferences!("MacroModelling", "dispatch_doctor_mode" => test_set in ["estimate_sw07", "estimate_sw07_nested_sampling", "estimation", "1st_order_inversion_estimation", "pruned_2nd_order_estimation", "2nd_order_estimation", "pruned_3rd_order_estimation", "3rd_order_estimation", "estimation_pigeons", "1st_order_inversion_estimation_pigeons", "2nd_order_estimation_pigeons", "pruned_2nd_order_estimation_pigeons", "3rd_order_estimation_pigeons", "pruned_3rd_order_estimation_pigeons", "system_prior_estimation", "gradient_checks", "missing_data", "jet", "jet_hot_paths", "particle_filter" ] ? "disable" : "error") set_preferences!("MacroModelling", "dispatch_doctor_union_limit" => 4) @@ -69,6 +69,9 @@ elseif test_set == "update_equations" include("test_update_equations.jl") elseif test_set == "jet_hot_paths" include("test_jet_hot_paths.jl") +elseif test_set == "particle_filter" + include("test_particle_filter.jl") + include("test_particle_filter_sw07.jl") elseif test_set == "dynare_comparison" # Dynare comparison runs as a standalone 3-phase pipeline (see CI workflow). # If output/ exists with results, run the comparison script directly. diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl new file mode 100644 index 000000000..79cae6092 --- /dev/null +++ b/test/test_particle_filter.jl @@ -0,0 +1,159 @@ +using MacroModelling +using Test +import Random +import Statistics +import AxisKeys: KeyedArray +import ForwardDiff +import Zygote + +# A small RBC model with two shocks and two observables, so the (bootstrap) +# particle filter is non-degenerate and can be validated against the exact +# Kalman likelihood on the linear (first-order) solution. +@model RBC_pf begin + 1 / c[0] = (β / c[1]) * (α * exp(z[1]) * k[0]^(α - 1) + (1 - δ)) + c[0] + k[0] = (1 - δ) * k[-1] + q[0] + q[0] = exp(z[0]) * k[-1]^α * exp(g[0]) + z[0] = ρz * z[-1] + std_z * eps_z[x] + g[0] = ρg * g[-1] + std_g * eps_g[x] +end + +@parameters RBC_pf begin + std_z = 0.01 + std_g = 0.01 + ρz = 0.4 + ρg = 0.6 + δ = 0.02 + α = 0.5 + β = 0.95 +end + +Random.seed!(12345) +sim = simulate(RBC_pf, periods = 40) +data = sim([:c, :q], :, :simulate) +p = RBC_pf.parameter_values +me = 0.002 + +threw(f) = try; f(); false; catch; true; end + +@testset "Particle filter" begin + + @testset "Measurement error on the Kalman filter" begin + llk_no = get_loglikelihood(RBC_pf, data, p; filter = :kalman) + # measurement_error_std = 0 must reduce exactly to the no-ME likelihood + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = 0.0) == llk_no + # a positive measurement error changes the likelihood and stays finite + llk_me = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + @test isfinite(llk_me) + @test llk_me != llk_no + # scalar broadcast equals the equivalent per-observable vector + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = [me, me]) ≈ llk_me + # ForwardDiff flows through the Kalman likelihood with measurement error + g = ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, measurement_error_std = me), p) + @test all(isfinite, g) + end + + kal = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + + @testset "Bootstrap PF converges to the Kalman likelihood" begin + # The bootstrap particle-filter likelihood estimator is unbiased for the + # true likelihood, so log L̂ is downward biased by ≈ Var(log L̂)/2 and both + # the bias and the variance shrink with the number of particles. + pf(N, s) = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, + measurement_error_std = me, n_particles = N, rng = Random.Xoshiro(s)) + nseeds = 24 + ll_small = [pf(2_000, 100 + s) for s in 1:nseeds] + ll_large = [pf(16_000, 200 + s) for s in 1:nseeds] + + m_small, v_small = Statistics.mean(ll_small), Statistics.var(ll_small) + m_large, v_large = Statistics.mean(ll_large), Statistics.var(ll_large) + + @test all(isfinite, ll_small) + @test all(isfinite, ll_large) + # variance decreases with the number of particles + @test v_large < v_small + # bias ≈ Var/2: the bias-corrected estimate is close to the Kalman value + @test isapprox(m_large + v_large / 2, kal, atol = 1.0) + # the estimate itself lands near the Kalman value at the larger N + @test abs(kal - m_large) < 2.0 + end + + @testset "Variants: correct and ordered by efficiency" begin + variant(pfa, N, s) = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, + particle_filter_algorithm = pfa, measurement_error_std = me, + n_particles = N, rng = Random.Xoshiro(s)) + nseeds = 16 + boot = [variant(:bootstrap, 3_000, 300 + s) for s in 1:nseeds] + aux = [variant(:auxiliary, 3_000, 300 + s) for s in 1:nseeds] + temp = [variant(:tempered, 3_000, 300 + s) for s in 1:nseeds] + + for v in (boot, aux, temp) + @test all(isfinite, v) + @test abs(kal - Statistics.mean(v)) < 6.0 # all centred near the truth + end + # the tempered filter has markedly lower variance than the bootstrap filter + @test Statistics.std(temp) < Statistics.std(boot) + end + + @testset "Resampling schemes" begin + rng = Random.Xoshiro(1) + W = rand(rng, 200); W ./= sum(W) + for scheme in (:systematic, :stratified, :multinomial, :residual) + idx = MacroModelling.particle_resample_indices(rng, W, scheme) + @test length(idx) == length(W) + @test all(i -> 1 <= i <= length(W), idx) + end + # a degenerate weight vector (all mass on one particle) selects only it + Wdeg = zeros(50); Wdeg[7] = 1.0 + for scheme in (:systematic, :stratified, :multinomial, :residual) + @test all(==(7), MacroModelling.particle_resample_indices(rng, Wdeg, scheme)) + end + @test MacroModelling.effective_sample_size(fill(1 / 100, 100)) ≈ 100.0 + @test threw(() -> MacroModelling.particle_resample_indices(rng, W, :nonexistent)) + end + + @testset "Higher-order algorithms run" begin + for algo in (:first_order, :second_order, :pruned_second_order, :third_order, :pruned_third_order) + llh = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = algo, + measurement_error_std = me, n_particles = 3_000, rng = Random.Xoshiro(7)) + @test isfinite(llh) + end + # every variant runs at a pruned nonlinear order + for pfa in (:bootstrap, :auxiliary, :tempered) + llh = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :pruned_second_order, + particle_filter_algorithm = pfa, measurement_error_std = me, + n_particles = 3_000, rng = Random.Xoshiro(7)) + @test isfinite(llh) + end + end + + @testset "Missing observations" begin + raw = Array{Union{Missing,Float64}}(collect(data)) + raw[1, 3:5] .= missing + raw[2, 20] = missing + datam = KeyedArray(raw, Variable = [:c, :q], Time = 1:size(raw, 2)) + kal_m = get_loglikelihood(RBC_pf, datam, p; filter = :kalman, measurement_error_std = me) + pf_m = get_loglikelihood(RBC_pf, datam, p; filter = :particle, algorithm = :first_order, + measurement_error_std = me, n_particles = 16_000, rng = Random.Xoshiro(9)) + @test isfinite(kal_m) + @test isfinite(pf_m) + @test abs(kal_m - pf_m) < 3.0 + end + + @testset "Error guards" begin + # measurement error is not available for the inversion filter + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :inversion, measurement_error_std = me)) + # the particle filter requires measurement error + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order)) + # the particle filter is not differentiable (forward or reverse mode) + @test threw(() -> ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :particle, + algorithm = :first_order, measurement_error_std = me, n_particles = 500, + rng = Random.Xoshiro(1)), p)) + @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :particle, + algorithm = :first_order, measurement_error_std = me, n_particles = 500, + rng = Random.Xoshiro(1)), p)) + # reverse-mode AD of the Kalman likelihood with measurement error is guarded + @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, + measurement_error_std = me), p)) + end + +end diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl new file mode 100644 index 000000000..0c804914a --- /dev/null +++ b/test/test_particle_filter_sw07.jl @@ -0,0 +1,51 @@ +using MacroModelling +using Test +import Random +import Statistics +using DelimitedFiles, AxisKeys + +# Validate the particle filter on the Smets & Wouters (2007) linear model and the +# US data used by the estimation tests: in the linear (first-order) case every +# particle-filter variant must reproduce the exact Kalman log-likelihood. +# +# SW07 has 7 observables and 7 shocks, so with small measurement error the +# importance weights are very peaked (curse of dimensionality). We therefore use +# a moderately large measurement error (2·data-std), which keeps the estimator +# variance low so the Monte-Carlo mean lands on the Kalman value (up to the +# expected Var/2 finite-particle bias). The generous tolerance is meant to catch +# an incorrect filter (which would be off by hundreds of log-points), not to pin +# the value to the last digit. + +@testset "SW07 linear: particle filter matches Kalman" begin + dat, header = readdlm(joinpath(@__DIR__, "data", "usmodel.csv"), ',', header = true) + dat = Float64.(dat) + csv_names = vec(Symbol.(strip.(header))) + data = KeyedArray(dat', Variable = csv_names, Time = axes(dat, 1)) + data = data([:dy, :dc, :dinve, :labobs, :pinfobs, :dw, :robs], 47:230) + observables = [:dy, :dc, :dinve, :labobs, :pinfobs, :dwobs, :robs] + data = rekey(data, :Variable => observables) + + include("../models/Smets_Wouters_2007_linear.jl") + SS(Smets_Wouters_2007_linear, parameters = [:crhoms => 0.01, :crhopinf => 0.01, :crhow => 0.01, :cmap => 0.01, :cmaw => 0.01]) + m = Smets_Wouters_2007_linear + p = m.parameter_values + + me = 2.0 .* [Statistics.std(collect(data(o))) for o in observables] + + # Compare both filters from the same initial state distribution: the particle + # cloud represents the ergodic distribution, matching `initial_covariance = :theoretical`. + kal = get_loglikelihood(m, data(observables), p; filter = :kalman, + presample_periods = 4, initial_covariance = :theoretical, + measurement_error_std = me) + @test isfinite(kal) + + for (pfa, N) in ((:bootstrap, 20_000), (:auxiliary, 20_000), (:tempered, 8_000)) + lls = [get_loglikelihood(m, data(observables), p; filter = :particle, algorithm = :first_order, + presample_periods = 4, initial_covariance = :theoretical, + measurement_error_std = me, particle_filter_algorithm = pfa, + n_particles = N, rng = Random.Xoshiro(1000 + s)) for s in 1:6] + @test all(isfinite, lls) + # the Monte-Carlo mean matches the Kalman value up to the (downward) Var/2 bias + @test abs(kal - Statistics.mean(lls)) < 15 + end +end From fa9ba4143d0ee2a22225e84649fe70fec78ee00e Mon Sep 17 00:00:00 2001 From: thorek1 Date: Wed, 22 Jul 2026 10:43:26 +0200 Subject: [PATCH 02/24] Optimise first-order particle filters: batched BLAS, allocation-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrite the first-order (linear) path of all three particle-filter variants (bootstrap, auxiliary, tempered) for speed. Particles are stored as the columns of an nVars×N matrix and the whole swarm is propagated with two BLAS gemm calls (Xₜ = A·Xₜ₋₁ + B·Eₜ) instead of N type-unstable, per-call-allocating closure invocations. Particle pools are double-buffered so resampling and the tempered Metropolis mutation run fully in place; the mutation caches Base = A·Anc once per stage and only recomputes the batched shock term B·Eprop per proposal. Resampling uses preallocated index/cumulative-weight buffers (in-place `*_resample_indices!`), following LowLevelParticleFilters.jl; inverse measurement-error variances and the log-normaliser are cached. On the Smets-Wouters (2007) linear model (nVars=40, N=10k-20k) this cuts a tempered-filter likelihood evaluation from ~192.7M allocations / 11.75 GiB to ~630 allocations / ~20 MiB (≈300,000× fewer allocations), and speeds up the filters by roughly 11× (bootstrap/auxiliary) and 5-6× (tempered). All variants still reproduce the exact Kalman likelihood. Higher orders continue to use the generic methods. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HhhiqdZNkzbGZKJ8oSeoTp --- src/filter/particle.jl | 553 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 553 insertions(+) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index cd19bbcc5..70a4fc4c1 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -651,4 +651,557 @@ function run_particle_filter(::Val{algo}, return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) end + +# ── Optimised first-order (linear) fast paths ──────────────────────────────── +# For the linear state space the transition is xₜ = A·xₜ₋₁[past] + B·εₜ. These +# `::Val{:first_order}` methods replace the type-unstable, per-call-allocating +# `state_update` closure with a typed, BLAS-backed (`mul!`), fully preallocated +# implementation. Particle pools are double-buffered so resampling and the +# tempered Metropolis mutation run in place, with no heap allocation in the hot +# loop. (Higher orders use the generic methods above.) Buffer-reuse for the +# resampling index/cumulative arrays follows LowLevelParticleFilters.jl. + +# In-place resampling: ancestor indices are written into `idx`; `bins` is a +# cumulative-weight scratch used by the multinomial/residual schemes. +function systematic_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + u0 = rand(rng) / N + c = W[1]; i = 1 + @inbounds for j in 1:N + u = u0 + (j - 1) / N + while u > c && i < N; i += 1; c += W[i]; end + idx[j] = i + end + return idx +end + +function stratified_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + c = W[1]; i = 1 + @inbounds for j in 1:N + u = (j - 1 + rand(rng)) / N + while u > c && i < N; i += 1; c += W[i]; end + idx[j] = i + end + return idx +end + +function multinomial_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + cumsum!(bins, W); bins[N] = one(eltype(bins)) + @inbounds for j in 1:N + idx[j] = searchsortedfirst(bins, rand(rng)) + end + return idx +end + +function residual_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) + N = length(W) + k = 0 + @inbounds for i in 1:N + ni = floor(Int, N * W[i]) + for _ in 1:ni; k += 1; idx[k] = i; end + end + R = N - k + if R > 0 + s = 0.0 + @inbounds for i in 1:N + bins[i] = N * W[i] - floor(N * W[i]); s += bins[i] + end + if s <= 0 + cumsum!(bins, W) + else + @inbounds for i in 1:N; bins[i] /= s; end + cumsum!(bins, bins) + end + bins[N] = one(eltype(bins)) + @inbounds for _ in 1:R + k += 1; idx[k] = searchsortedfirst(bins, rand(rng)) + end + end + return idx +end + +@inline function particle_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) + if scheme == :systematic + systematic_resample_indices!(idx, rng, W) + elseif scheme == :stratified + stratified_resample_indices!(idx, rng, W) + elseif scheme == :multinomial + multinomial_resample_indices!(idx, bins, rng, W) + elseif scheme == :residual + residual_resample_indices!(idx, bins, rng, W) + else + error("Unknown resampling scheme `:$scheme`. Choose from `:systematic`, `:stratified`, `:multinomial`, `:residual`.") + end + return idx +end + +# Typed, preallocated first-order transition xₜ = A·xₜ₋₁[past] + B·εₜ. +# Particles are stored as the columns of an nVars × N matrix so that the whole +# swarm is propagated with two BLAS gemm calls (Xₜ = A·Xₜ₋₁ + B·Eₜ) instead of +# N small gemv calls. `A` is the full nVars × nVars one-step transition (zero +# outside the predetermined-state columns), `B` the nVars × nExo shock loading. +struct LinearParticleTransition + A::Matrix{Float64} + B::Matrix{Float64} +end + +function build_linear_particle_transition(𝐒::AbstractMatrix, T) + nVars = T.nVars + nPast = T.nPast_not_future_and_mixed + S₁ = Matrix{Float64}(𝐒) + A = zeros(Float64, nVars, nVars) + @views A[:, T.past_not_future_and_mixed_idx] .= S₁[:, 1:nPast] + B = Matrix{Float64}(@view S₁[:, nPast+1:end]) + return LinearParticleTransition(A, B) +end + +# X₂ = A·X + B·E for the whole swarm at once (two gemm). Columns are particles. +@inline function propagate_batch!(X2::Matrix{Float64}, tr::LinearParticleTransition, X::Matrix{Float64}, E::Matrix{Float64}) + ℒ.mul!(X2, tr.A, X) + ℒ.mul!(X2, tr.B, E, 1.0, 1.0) + return X2 +end + +# Base = A·Anc (shock-independent part of the transition, one gemm; reused across +# Metropolis proposals which only vary the shock B·E term). +@inline function base_batch!(Base::Matrix{Float64}, tr::LinearParticleTransition, Anc::Matrix{Float64}) + ℒ.mul!(Base, tr.A, Anc) + return Base +end + +# Quadratic form eᵀH⁻¹e over the observed rows for particle column `p`. +@inline function linear_quadform_col(X::Matrix{Float64}, p::Int, data_col, observables_index, inv_me_var, rows) + q = 0.0 + @inbounds for k in eachindex(rows) + r = rows[k] + f = X[observables_index[r], p] + isfinite(f) || return Inf + v = data_col[r] - f + q += v * v * inv_me_var[r] + end + return q +end + +# Copy column `src` of `X` into column `dst` of `Y` (contiguous, allocation-free). +@inline function copy_col!(Y::Matrix{Float64}, dst::Int, X::Matrix{Float64}, src::Int) + @inbounds for i in axes(X, 1) + Y[i, dst] = X[i, src] + end + return Y +end + +# Draw the initial cloud into the columns of X (nVars × N): X = mean0 .+ L·Z. +function init_linear_particles!(X::Matrix{Float64}, rng::Random.AbstractRNG, + mean0::AbstractVector{Float64}, L::Matrix{Float64}, Z::Matrix{Float64}) + Random.randn!(rng, Z) + ℒ.mul!(X, L, Z) + @inbounds for p in axes(X, 2), i in axes(X, 1) + X[i, p] += mean0[i] + end + return X +end + +function run_particle_filter(::Val{:first_order}, + ::Val{:bootstrap}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + inv_me_var = 1.0 ./ me_var + + tr = build_linear_particle_transition(𝐒, T) + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + + X = Matrix{Float64}(undef, nVars, n_particles) + X2 = Matrix{Float64}(undef, nVars, n_particles) + E = Matrix{Float64}(undef, nExo, n_particles) + Z = Matrix{Float64}(undef, nVars, n_particles) + W = fill(1.0 / n_particles, n_particles) + logdens = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) + + mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) + init_linear_particles!(X, rng, mean0, L, Z) + + loglik = 0.0 + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + + Random.randn!(rng, E) + propagate_batch!(X2, tr, X, E) + X, X2 = X2, X + + isempty(rows) && continue + + logZ = particle_measurement_logZ(me_var, rows, log2pi) + @inbounds for p in 1:n_particles + logdens[p] = logZ - 0.5 * linear_quadform_col(X, p, data_col, observables_index, inv_me_var, rows) + end + + m = maximum(logdens) + if !isfinite(m) + return Float64(on_failure_loglikelihood) + end + s = 0.0 + @inbounds for p in 1:n_particles + s += W[p] * exp(logdens[p] - m) + end + if s <= 0 || !isfinite(s) + return Float64(on_failure_loglikelihood) + end + + ll_t = m + log(s) + if t > presample_periods + loglik += ll_t + end + + @inbounds for p in 1:n_particles + W[p] = W[p] * exp(logdens[p] - m) / s + end + + if effective_sample_size(W) < resampling_threshold * n_particles + particle_resample_indices!(idx, bins, rng, W, resampling) + @inbounds for j in 1:n_particles + copy_col!(X2, j, X, idx[j]) + end + X, X2 = X2, X + fill!(W, 1.0 / n_particles) + end + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + + +function run_particle_filter(::Val{:first_order}, + ::Val{:auxiliary}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + inv_me_var = 1.0 ./ me_var + + tr = build_linear_particle_transition(𝐒, T) + + # Predictive variance of each observable (shock spread + measurement error). + pred_var = Vector{Float64}(undef, length(observables_index)) + @inbounds for i in eachindex(observables_index) + pred_var[i] = sum(abs2, @view tr.B[observables_index[i], :]) + me_var[i] + end + inv_pred_var = 1.0 ./ pred_var + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + + X = Matrix{Float64}(undef, nVars, n_particles) + X2 = Matrix{Float64}(undef, nVars, n_particles) + AncX = Matrix{Float64}(undef, nVars, n_particles) + E = Matrix{Float64}(undef, nExo, n_particles) + W = fill(1.0 / n_particles, n_particles) + logg̃ = Vector{Float64}(undef, n_particles) + logw = Vector{Float64}(undef, n_particles) + lam = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) + + mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) + init_linear_particles!(X, rng, mean0, L, AncX) + + loglik = 0.0 + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + logZ = particle_measurement_logZ(me_var, rows, log2pi) + logZp = particle_measurement_logZ(pred_var, rows, log2pi) + + # First stage: predictive density at the transition mean μ = A·X (one gemm). + base_batch!(X2, tr, X) + @inbounds for p in 1:n_particles + logg̃[p] = logZp - 0.5 * linear_quadform_col(X2, p, data_col, observables_index, inv_pred_var, rows) + end + + mλ = -Inf + @inbounds for p in 1:n_particles + lλ = log(W[p]) + logg̃[p] + mλ = lλ > mλ ? lλ : mλ + end + if !isfinite(mλ) + return Float64(on_failure_loglikelihood) + end + sλ = 0.0 + @inbounds for p in 1:n_particles + sλ += exp(log(W[p]) + logg̃[p] - mλ) + end + logκ = mλ + log(sλ) + @inbounds for p in 1:n_particles + lam[p] = exp(log(W[p]) + logg̃[p] - logκ) + end + + # Resample ancestors ∝ λ, gather them, and propagate with fresh shocks. + particle_resample_indices!(idx, bins, rng, lam, resampling) + @inbounds for j in 1:n_particles + copy_col!(AncX, j, X, idx[j]) + end + Random.randn!(rng, E) + propagate_batch!(X2, tr, AncX, E) + @inbounds for j in 1:n_particles + logw[j] = (logZ - 0.5 * linear_quadform_col(X2, j, data_col, observables_index, inv_me_var, rows)) - logg̃[idx[j]] + end + X, X2 = X2, X + + mw = maximum(logw) + if !isfinite(mw) + return Float64(on_failure_loglikelihood) + end + sw = 0.0 + @inbounds for j in 1:n_particles + sw += exp(logw[j] - mw) + end + if sw <= 0 || !isfinite(sw) + return Float64(on_failure_loglikelihood) + end + + ll_t = logκ + (mw + log(sw) - log(n_particles)) + if t > presample_periods + loglik += ll_t + end + + logsw = mw + log(sw) + @inbounds for j in 1:n_particles + W[j] = exp(logw[j] - logsw) + end + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + + +function run_particle_filter(::Val{:first_order}, + ::Val{:tempered}, + observables_index::Vector{Int}, + 𝐒, + data_in_deviations::AbstractMatrix, + constants::constants, + state, + 𝓂::ℳ, + measurement_error_variances::AbstractVector{<:Real}, + obs_idx_per_t::Vector{Vector{Int}}, + has_missing::Bool; + n_particles::Int = DEFAULT_N_PARTICLES, + resampling::Symbol = DEFAULT_RESAMPLING, + resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, + initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, + rng::Random.AbstractRNG = Random.default_rng(), + presample_periods::Int = 0, + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, + on_failure_loglikelihood::Real = -Inf, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + nT = size(data_in_deviations, 2) + presample_periods = normalize_presample_periods(presample_periods, nT) + log2pi = log(2π) + + me_var = Float64.(measurement_error_variances) + @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + inv_me_var = 1.0 ./ me_var + + r_star = Float64(tempering_target_ratio) + c = Float64(tempering_mh_scale) + n_mh = tempering_mh_steps + max_stages = tempering_max_stages + + tr = build_linear_particle_transition(𝐒, T) + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + + # Double-buffered particle pools (columns are particles). + Anc = Matrix{Float64}(undef, nVars, n_particles) + Anc2 = Matrix{Float64}(undef, nVars, n_particles) + Sh = Matrix{Float64}(undef, nExo, n_particles) + Sh2 = Matrix{Float64}(undef, nExo, n_particles) + St = Matrix{Float64}(undef, nVars, n_particles) + St2 = Matrix{Float64}(undef, nVars, n_particles) + dv = Vector{Float64}(undef, n_particles) + dv2 = Vector{Float64}(undef, n_particles) + + Base = Matrix{Float64}(undef, nVars, n_particles) + Sprop = Matrix{Float64}(undef, nVars, n_particles) + Eprop = Matrix{Float64}(undef, nExo, n_particles) + logw = Vector{Float64}(undef, n_particles) + Wn = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) + + mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) + init_linear_particles!(St, rng, mean0, L, Anc2) + + loglik = 0.0 + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view data_in_deviations[:, t] + d_obs = length(rows) + + # Ancestors = previous filtered states; propagate the whole swarm (2 gemm). + copyto!(Anc, St) + Random.randn!(rng, Sh) + propagate_batch!(St, tr, Anc, Sh) + @inbounds for p in 1:n_particles + dv[p] = linear_quadform_col(St, p, data_col, observables_index, inv_me_var, rows) + end + if all(!isfinite, dv) + return Float64(on_failure_loglikelihood) + end + + logZ = particle_measurement_logZ(me_var, rows, log2pi) + period_ll = 0.0 + φ_old = 0.0 + stage = 0 + while φ_old < 1.0 - 1e-12 && stage < max_stages + stage += 1 + φ_new = tempered_next_phi(φ_old, dv, r_star, n_particles) + + if φ_old == 0.0 + @inbounds for p in 1:n_particles + logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dv[p] + end + else + lr = 0.5 * d_obs * (log(φ_new) - log(φ_old)) + @inbounds for p in 1:n_particles + logw[p] = lr - 0.5 * (φ_new - φ_old) * dv[p] + end + end + + m = maximum(logw) + if !isfinite(m) + return Float64(on_failure_loglikelihood) + end + s = 0.0 + @inbounds for p in 1:n_particles + s += exp(logw[p] - m) + end + if s <= 0 || !isfinite(s) + return Float64(on_failure_loglikelihood) + end + period_ll += m + log(s) - log(n_particles) + + logsw = m + log(s) + @inbounds for p in 1:n_particles + Wn[p] = exp(logw[p] - logsw) + end + + particle_resample_indices!(idx, bins, rng, Wn, resampling) + @inbounds for j in 1:n_particles + a = idx[j] + copy_col!(Anc2, j, Anc, a); copy_col!(Sh2, j, Sh, a); copy_col!(St2, j, St, a); dv2[j] = dv[a] + end + Anc, Anc2 = Anc2, Anc + Sh, Sh2 = Sh2, Sh + St, St2 = St2, St + dv, dv2 = dv2, dv + + # Metropolis mutation on the shocks. Base = A·Anc is shock-independent + # (one gemm); each proposal only recomputes the batched shock term + # B·Eprop before per-particle accept/reject. + base_batch!(Base, tr, Anc) + for _ in 1:n_mh + Random.randn!(rng, Eprop) + @inbounds for k in eachindex(Eprop) + Eprop[k] = Sh[k] + c * Eprop[k] + end + ℒ.mul!(Sprop, tr.B, Eprop) + Sprop .+= Base + @inbounds for p in 1:n_particles + dprop = linear_quadform_col(Sprop, p, data_col, observables_index, inv_me_var, rows) + esq_old = 0.0 + esq_new = 0.0 + for e in 1:nExo + so = Sh[e, p]; sn = Eprop[e, p] + esq_old += so * so + esq_new += sn * sn + end + logα = -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) + if log(rand(rng)) < logα + copy_col!(Sh, p, Eprop, p) + copy_col!(St, p, Sprop, p) + dv[p] = dprop + end + end + end + + φ_old = φ_new + end + + if t > presample_periods + loglik += period_ll + end + end + + return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) +end + end # @stable From 461c10bddbfa1fff61062a4af68a3c8e7761ff96 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Wed, 22 Jul 2026 22:57:22 +0200 Subject: [PATCH 03/24] Optimise higher-order particle filters: allocation-free transitions Extend the performance work to the nonlinear orders (:second_order through :pruned_third_order) for all three variants. Replace the type-unstable, per-call-allocating `state_update` closure with typed in-place transitions (`nonpruned_state_update_{2,3}!`, `pf_pruned_{2nd,3rd}!`) that gather the augmented state with explicit loops and apply the solution matrices via `kron!`/`mul!` with preallocated scratch. Particle pools are double-buffered and built with a concretely-typed, `Val`-dispatched initialiser; the per-period loop runs behind a function barrier so the large kwarg method body does not lose the pool element type. A `Core.Box` (from a captured-and-reassigned pool variable in the tempered filter) is avoided by keeping the captured cloud read-only and swapping separate locals. On Smets-Wouters (2007), pruned second order, N=4000 this cuts allocations from ~6.25M (bootstrap) / ~55M (tempered) to ~66k / ~153k, with GC time near zero. All higher-order likelihood values are bit-identical to before. Values, missing data, resampling schemes and error guards all verified; test_particle_filter (47) and the SW07 test (7) pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HhhiqdZNkzbGZKJ8oSeoTp --- src/filter/particle.jl | 365 +++++++++++++++++++++++++++++++++++------ 1 file changed, 315 insertions(+), 50 deletions(-) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 70a4fc4c1..6528fe6b6 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -206,6 +206,177 @@ end end +# ── Allocation-free higher-order transitions ───────────────────────────────── +# In-place transitions for the nonlinear orders, mirroring the closures built by +# `parse_algorithm_to_state_update` but writing into a preallocated `out` with +# preallocated `aug`/kron scratch and BLAS `mul!`/`kron!` (no per-call heap +# allocation). The pruned orders reuse `pruned_state_update_{2nd,3rd}_order!`. +# `𝐒[1]` here is the augmented first-order matrix (constant column inserted). + +# Gather aug = [state[past]; const; shock] into the preallocated `aug` (explicit +# loops avoid the SubArray allocation of `state[past_idx]`). +@inline function fill_aug!(aug, state, past_idx, shock, const_val) + n_past = length(past_idx) + @inbounds for i in 1:n_past + aug[i] = state[past_idx[i]] + end + @inbounds aug[n_past + 1] = const_val + @inbounds for e in eachindex(shock) + aug[n_past + 1 + e] = shock[e] + end + return aug +end + +# out = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug), aug = [state[past]; 1; shock]. +function nonpruned_state_update_2nd_order!(out, state, past_idx, shock, aug, kk, 𝐒) + fill_aug!(aug, state, past_idx, shock, 1.0) + ℒ.kron!(kk, aug, aug) + ℒ.mul!(out, 𝐒[1], aug) + ℒ.mul!(out, 𝐒[2], kk, 0.5, 1.0) + return out +end + +# out = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug) + ⅙ 𝐒₃·(aug⊗aug⊗aug). +function nonpruned_state_update_3rd_order!(out, state, past_idx, shock, aug, kk, kkk, 𝐒) + fill_aug!(aug, state, past_idx, shock, 1.0) + ℒ.kron!(kk, aug, aug) + ℒ.kron!(kkk, kk, aug) + ℒ.mul!(out, 𝐒[1], aug) + ℒ.mul!(out, 𝐒[2], kk, 0.5, 1.0) + ℒ.mul!(out, 𝐒[3], kkk, 1 / 6, 1.0) + return out +end + +# Allocation-free pruned updates (explicit past-gather; components zero the +# constant slot and, for the higher-order parts, the shock slots). +function pf_pruned_2nd!(new_s1, new_s2, s1, s2, past_idx, shock, aug1, aug2, kk, 𝐒) + n_past = length(past_idx) + fill_aug!(aug1, s1, past_idx, shock, 1.0) + @inbounds for i in 1:n_past + aug2[i] = s2[past_idx[i]] + end + @inbounds aug2[n_past + 1] = 0.0 + @inbounds for e in eachindex(shock) + aug2[n_past + 1 + e] = 0.0 + end + ℒ.kron!(kk, aug1, aug1) + ℒ.mul!(new_s1, 𝐒[1], aug1) + ℒ.mul!(new_s2, 𝐒[1], aug2) + ℒ.mul!(new_s2, 𝐒[2], kk, 0.5, 1.0) + return nothing +end + +function pf_pruned_3rd!(new_s1, new_s2, new_s3, s1, s2, s3, past_idx, shock, + aug1, aug1̂, aug2, aug3, k11, k12̂, k111, 𝐒) + n_past = length(past_idx) + fill_aug!(aug1, s1, past_idx, shock, 1.0) + fill_aug!(aug1̂, s1, past_idx, shock, 0.0) + @inbounds for i in 1:n_past + aug2[i] = s2[past_idx[i]] + aug3[i] = s3[past_idx[i]] + end + @inbounds aug2[n_past + 1] = 0.0 + @inbounds aug3[n_past + 1] = 0.0 + @inbounds for e in eachindex(shock) + aug2[n_past + 1 + e] = 0.0 + aug3[n_past + 1 + e] = 0.0 + end + ℒ.kron!(k11, aug1, aug1) + ℒ.kron!(k12̂, aug1̂, aug2) + ℒ.kron!(k111, k11, aug1) + ℒ.mul!(new_s1, 𝐒[1], aug1) + ℒ.mul!(new_s2, 𝐒[1], aug2) + ℒ.mul!(new_s2, 𝐒[2], k11, 0.5, 1.0) + ℒ.mul!(new_s3, 𝐒[1], aug3) + ℒ.mul!(new_s3, 𝐒[2], k12̂, 1.0, 1.0) + ℒ.mul!(new_s3, 𝐒[3], k111, 1 / 6, 1.0) + return nothing +end + +# Preallocated kron/aug scratch for one particle, sized per algorithm. +function build_higher_scratch(::Val{:second_order}, nPast::Int, nExo::Int) + naug = nPast + 1 + nExo + (aug = Vector{Float64}(undef, naug), kk = Vector{Float64}(undef, naug^2)) +end +function build_higher_scratch(::Val{:third_order}, nPast::Int, nExo::Int) + naug = nPast + 1 + nExo + (aug = Vector{Float64}(undef, naug), kk = Vector{Float64}(undef, naug^2), kkk = Vector{Float64}(undef, naug^3)) +end +function build_higher_scratch(::Val{:pruned_second_order}, nPast::Int, nExo::Int) + naug = nPast + 1 + nExo + (aug1 = Vector{Float64}(undef, naug), aug2 = Vector{Float64}(undef, naug), + kk = Vector{Float64}(undef, naug^2), zero_shock = zeros(Float64, nExo)) +end +function build_higher_scratch(::Val{:pruned_third_order}, nPast::Int, nExo::Int) + naug = nPast + 1 + nExo + (aug1 = Vector{Float64}(undef, naug), aug1̂ = Vector{Float64}(undef, naug), + aug2 = Vector{Float64}(undef, naug), aug3 = Vector{Float64}(undef, naug), + k11 = Vector{Float64}(undef, naug^2), k12̂ = Vector{Float64}(undef, naug^2), + k111 = Vector{Float64}(undef, naug^3), zero_shock = zeros(Float64, nExo)) +end + +# In-place propagation dispatch: writes the next state into `out`. +@inline higher_propagate!(::Val{:second_order}, out, state, shock, past_idx, 𝐒, scr) = + nonpruned_state_update_2nd_order!(out, state, past_idx, shock, scr.aug, scr.kk, 𝐒) +@inline higher_propagate!(::Val{:third_order}, out, state, shock, past_idx, 𝐒, scr) = + nonpruned_state_update_3rd_order!(out, state, past_idx, shock, scr.aug, scr.kk, scr.kkk, 𝐒) +@inline higher_propagate!(::Val{:pruned_second_order}, out, state, shock, past_idx, 𝐒, scr) = + pf_pruned_2nd!(out[1], out[2], state[1], state[2], past_idx, shock, scr.aug1, scr.aug2, scr.kk, 𝐒) +@inline higher_propagate!(::Val{:pruned_third_order}, out, state, shock, past_idx, 𝐒, scr) = + pf_pruned_3rd!(out[1], out[2], out[3], state[1], state[2], state[3], past_idx, shock, scr.aug1, scr.aug1̂, scr.aug2, scr.aug3, scr.k11, scr.k12̂, scr.k111, 𝐒) + +# Deep-copy a particle (flat vector or vector-of-components) into a preallocated slot. +@inline copy_particle!(dst::AbstractVector{Float64}, src::AbstractVector{Float64}) = copyto!(dst, src) +@inline function copy_particle!(dst::AbstractVector{<:AbstractVector}, src::AbstractVector{<:AbstractVector}) + @inbounds for c in eachindex(dst) + copyto!(dst[c], src[c]) + end + return dst +end + +# A zeroed particle with the same shape as `template` (for the second pool). +zeros_like_particle(template::AbstractVector{Float64}) = zeros(Float64, length(template)) +zeros_like_particle(template::AbstractVector{<:AbstractVector}) = [zeros(Float64, length(c)) for c in template] + +# Concretely-typed initial particle cloud (avoids the type-unstable `Union` that +# `initialise_particles` returns because its `pruning` branch is a runtime Bool). +# Dispatching on `Val{algo}` fixes the element type per specialization so the hot +# loop is allocation-free. The RNG draw order matches `initialise_particles`. +function init_higher_particles(::Union{Val{:second_order},Val{:third_order}}, rng, state, L, n_particles, nVars) + mean0 = Vector{Float64}(state) + return Vector{Float64}[mean0 .+ L * randn(rng, nVars) for _ in 1:n_particles] +end +function init_higher_particles(::Val{:pruned_second_order}, rng, state, L, n_particles, nVars) + m1 = Vector{Float64}(state[1]); m2 = Vector{Float64}(state[2]) + return Vector{Vector{Float64}}[[m1 .+ L * randn(rng, nVars), copy(m2)] for _ in 1:n_particles] +end +function init_higher_particles(::Val{:pruned_third_order}, rng, state, L, n_particles, nVars) + m1 = Vector{Float64}(state[1]); m2 = Vector{Float64}(state[2]); m3 = Vector{Float64}(state[3]) + return Vector{Vector{Float64}}[[m1 .+ L * randn(rng, nVars), copy(m2), copy(m3)] for _ in 1:n_particles] +end + +# Concrete pool type per algorithm, used to type-assert the initial cloud so the +# large kwarg method body doesn't lose the element type (which would send the +# function-barrier call through dynamic dispatch). +particle_pool_type(::Union{Val{:second_order},Val{:third_order}}) = Vector{Vector{Float64}} +particle_pool_type(::Union{Val{:pruned_second_order},Val{:pruned_third_order}}) = Vector{Vector{Vector{Float64}}} + +# Full model state a particle reports to the measurement, without allocation: +# the state itself for non-pruned orders, the sum of components (into `full_buf`) +# for pruned orders. Dispatches on the particle representation (type-stable). +@inline measurement_full(p::AbstractVector{Float64}, full_buf) = p +@inline function measurement_full(p::AbstractVector{<:AbstractVector}, full_buf) + fill!(full_buf, 0.0) + @inbounds for c in eachindex(p) + pc = p[c] + for i in eachindex(full_buf) + full_buf[i] += pc[i] + end + end + return full_buf +end + + # ── Bootstrap (sequential importance resampling) particle filter ───────────── function run_particle_filter(::Val{algo}, @@ -242,14 +413,35 @@ function run_particle_filter(::Val{algo}, me_var = Float64.(measurement_error_variances) @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." - state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + past_idx = T.past_not_future_and_mixed_idx + 𝐒f = [Matrix{Float64}(S) for S in 𝐒] + scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - particles = initialise_particles(rng, state, pruning, L, n_particles, nVars) + particles = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) + particles2 = [zeros_like_particle(particles[1]) for _ in 1:n_particles] + return bootstrap_higher_loop(Val(algo), particles, particles2, 𝐒f, scr, past_idx, + nVars, nExo, nT, presample_periods, observables_index, + data_in_deviations, obs_idx_per_t, has_missing, me_var, + resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) +end + +# Function barrier: `particles`/`particles2` arrive with a concrete element type, +# so the hot loop specialises and runs allocation-free (the enclosing kwarg method +# body is too large for inference to keep the pool types). +function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, past_idx, + nVars, nExo, nT, presample_periods, observables_index, + data_in_deviations, obs_idx_per_t, has_missing, me_var, + resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) where {algo} + n_particles = length(particles) + shock = Vector{Float64}(undef, nExo) + full_buf = Vector{Float64}(undef, nVars) W = fill(1.0 / n_particles, n_particles) logdens = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) loglik = 0.0 for t in 1:nT @@ -259,16 +451,20 @@ function run_particle_filter(::Val{algo}, if isempty(rows) # No observation this period: propagate only, weights unchanged. @inbounds for p in 1:n_particles - particles[p] = state_update(particles[p], randn(rng, nExo)) + Random.randn!(rng, shock) + higher_propagate!(Val(algo), particles2[p], particles[p], shock, past_idx, 𝐒f, scr) end + particles, particles2 = particles2, particles continue end @inbounds for p in 1:n_particles - particles[p] = state_update(particles[p], randn(rng, nExo)) - full = particle_full_state(particles[p], pruning) + Random.randn!(rng, shock) + higher_propagate!(Val(algo), particles2[p], particles[p], shock, past_idx, 𝐒f, scr) + full = measurement_full(particles2[p], full_buf) logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) end + particles, particles2 = particles2, particles m = maximum(logdens) if !isfinite(m) @@ -293,11 +489,11 @@ function run_particle_filter(::Val{algo}, end if effective_sample_size(W) < resampling_threshold * n_particles - idxs = particle_resample_indices(rng, W, resampling) - # `state_update` never mutates its argument in place, so sharing the - # underlying arrays across duplicated ancestors is safe (each slot is - # reassigned, never mutated, on the next propagation). - particles = particles[idxs] + particle_resample_indices!(idx, bins, rng, W, resampling) + @inbounds for j in 1:n_particles + copy_particle!(particles2[j], particles[idx[j]]) + end + particles, particles2 = particles2, particles fill!(W, 1.0 / n_particles) end end @@ -371,7 +567,9 @@ function run_particle_filter(::Val{algo}, me_var = Float64.(measurement_error_variances) @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." - state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + past_idx = T.past_not_future_and_mixed_idx + 𝐒f = [Matrix{Float64}(S) for S in 𝐒] + scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) # One-step-ahead predictive variance of each observable due to the structural # shocks (diagonal of Cₒ BBᵀ Cₒᵀ from the first-order shock loading), used to @@ -384,12 +582,31 @@ function run_particle_filter(::Val{algo}, Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - particles = initialise_particles(rng, state, pruning, L, n_particles, nVars) + particles = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) + particles2 = [zeros_like_particle(particles[1]) for _ in 1:n_particles] + mu_particle = zeros_like_particle(particles[1]) + + return auxiliary_higher_loop(Val(algo), particles, particles2, mu_particle, 𝐒f, scr, past_idx, + nVars, nExo, nT, presample_periods, observables_index, + data_in_deviations, obs_idx_per_t, has_missing, me_var, pred_var, + resampling, rng, on_failure_loglikelihood, log2pi) +end +# Function barrier for the auxiliary loop (see `bootstrap_higher_loop`). +function auxiliary_higher_loop(::Val{algo}, particles, particles2, mu_particle, 𝐒f, scr, past_idx, + nVars, nExo, nT, presample_periods, observables_index, + data_in_deviations, obs_idx_per_t, has_missing, me_var, pred_var, + resampling, rng, on_failure_loglikelihood, log2pi) where {algo} + n_particles = length(particles) zero_shock = zeros(Float64, nExo) + shock = Vector{Float64}(undef, nExo) + full_buf = Vector{Float64}(undef, nVars) W = fill(1.0 / n_particles, n_particles) logg̃ = Vector{Float64}(undef, n_particles) # first-stage predictive log-density logw = Vector{Float64}(undef, n_particles) # second-stage log-weight + λ = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) loglik = 0.0 for t in 1:nT @@ -399,7 +616,8 @@ function run_particle_filter(::Val{algo}, # First stage: predictive density at the transition mean (zero shock), # spread by the shock-induced predictive variance `pred_var`. @inbounds for p in 1:n_particles - μ = particle_full_state(state_update(particles[p], zero_shock), pruning) + higher_propagate!(Val(algo), mu_particle, particles[p], zero_shock, past_idx, 𝐒f, scr) + μ = measurement_full(mu_particle, full_buf) logg̃[p] = particle_log_measurement_density(μ, data_col, observables_index, pred_var, rows, log2pi) end @@ -417,21 +635,21 @@ function run_particle_filter(::Val{algo}, sλ += exp(log(W[p]) + logg̃[p] - mλ) end logκ = mλ + log(sλ) - λ = Vector{Float64}(undef, n_particles) @inbounds for p in 1:n_particles λ[p] = exp(log(W[p]) + logg̃[p] - logκ) end # Resample ancestors ∝ λ, propagate with fresh shocks, second-stage weight # w = g(yₜ|xₜ) / g̃(ancestor). - idx = particle_resample_indices(rng, λ, resampling) - newparts = Vector{eltype(particles)}(undef, n_particles) + particle_resample_indices!(idx, bins, rng, λ, resampling) @inbounds for j in 1:n_particles a = idx[j] - newparts[j] = state_update(particles[a], randn(rng, nExo)) - full = particle_full_state(newparts[j], pruning) + Random.randn!(rng, shock) + higher_propagate!(Val(algo), particles2[j], particles[a], shock, past_idx, 𝐒f, scr) + full = measurement_full(particles2[j], full_buf) logw[j] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) - logg̃[a] end + particles, particles2 = particles2, particles mw = maximum(logw) if !isfinite(mw) @@ -454,7 +672,6 @@ function run_particle_filter(::Val{algo}, @inbounds for j in 1:n_particles W[j] = exp(logw[j] - logsw) end - particles = newparts end return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) @@ -549,13 +766,50 @@ function run_particle_filter(::Val{algo}, n_mh = tempering_mh_steps max_stages = tempering_max_stages - state_update, pruning = parse_algorithm_to_state_update(algo, 𝓂, false) + past_idx = T.past_not_future_and_mixed_idx + 𝐒f = [Matrix{Float64}(S) for S in 𝐒] + scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - prev_states = initialise_particles(rng, state, pruning, L, n_particles, nVars) + # Double-buffered particle pools (ancestors and states + resample scratch). + anc = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) + return tempered_higher_loop(Val(algo), anc, 𝐒f, scr, past_idx, nVars, nExo, nT, + presample_periods, observables_index, data_in_deviations, + obs_idx_per_t, has_missing, me_var, resampling, r_star, c, + n_mh, max_stages, rng, on_failure_loglikelihood, log2pi) +end + +# Function barrier for the tempered loop (see `bootstrap_higher_loop`). +function tempered_higher_loop(::Val{algo}, anc0, 𝐒f, scr, past_idx, nVars, nExo, nT, + presample_periods, observables_index, data_in_deviations, + obs_idx_per_t, has_missing, me_var, resampling, r_star, c, + n_mh, max_stages, rng, on_failure_loglikelihood, log2pi) where {algo} + # `anc0` is captured (read-only) by the pool comprehensions below; the pools + # that get swapped are separate locals, so nothing captured is ever reassigned + # (which would force Julia to `Core.Box` it and make the loop type-unstable). + anc0 = anc0::particle_pool_type(Val(algo)) + n_particles = length(anc0) + anc = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] + st = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] + anc2 = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] + st2 = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] + @inbounds for p in 1:n_particles + copy_particle!(anc[p], anc0[p]) + end + sh = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] + sh2 = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] + dv = Vector{Float64}(undef, n_particles) + dv2 = Vector{Float64}(undef, n_particles) + + sprop = zeros_like_particle(anc0[1]) + eprop = Vector{Float64}(undef, nExo) + full_buf = Vector{Float64}(undef, nVars) logw = Vector{Float64}(undef, n_particles) + Wn = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) loglik = 0.0 for t in 1:nT @@ -563,17 +817,15 @@ function run_particle_filter(::Val{algo}, data_col = @view data_in_deviations[:, t] d_obs = length(rows) - # Bootstrap proposal: propagate every ancestor with a fresh shock. - ancestors = prev_states - shocks = [randn(rng, nExo) for _ in 1:n_particles] - states = Vector{eltype(prev_states)}(undef, n_particles) - dvec = Vector{Float64}(undef, n_particles) + # Bootstrap proposal: propagate every ancestor (`anc`) with a fresh shock. @inbounds for p in 1:n_particles - states[p] = state_update(ancestors[p], shocks[p]) - dvec[p] = particle_quadratic_form(particle_full_state(states[p], pruning), data_col, observables_index, me_var, rows) + Random.randn!(rng, sh[p]) end - - if all(!isfinite, dvec) + @inbounds for p in 1:n_particles + higher_propagate!(Val(algo), st[p], anc[p], sh[p], past_idx, 𝐒f, scr) + dv[p] = particle_quadratic_form(measurement_full(st[p], full_buf), data_col, observables_index, me_var, rows) + end + if all(!isfinite, dv) return Float64(on_failure_loglikelihood) end @@ -582,18 +834,17 @@ function run_particle_filter(::Val{algo}, stage = 0 while φ_old < 1.0 - 1e-12 && stage < max_stages stage += 1 - φ_new = tempered_next_phi(φ_old, dvec, r_star, n_particles) + φ_new = tempered_next_phi(φ_old, dv, r_star, n_particles) - # Incremental (tempered) log-weights. if φ_old == 0.0 logZ = particle_measurement_logZ(me_var, rows, log2pi) @inbounds for p in 1:n_particles - logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dvec[p] + logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dv[p] end else lr = 0.5 * d_obs * (log(φ_new) - log(φ_old)) @inbounds for p in 1:n_particles - logw[p] = lr - 0.5 * (φ_new - φ_old) * dvec[p] + logw[p] = lr - 0.5 * (φ_new - φ_old) * dv[p] end end @@ -610,31 +861,41 @@ function run_particle_filter(::Val{algo}, end period_ll += m + log(s) - log(n_particles) - # Normalise and resample. logsw = m + log(s) - Wn = Vector{Float64}(undef, n_particles) @inbounds for p in 1:n_particles Wn[p] = exp(logw[p] - logsw) end - idx = particle_resample_indices(rng, Wn, resampling) - ancestors = ancestors[idx] - shocks = shocks[idx] - states = states[idx] - dvec = dvec[idx] + particle_resample_indices!(idx, bins, rng, Wn, resampling) + @inbounds for j in 1:n_particles + a = idx[j] + copy_particle!(anc2[j], anc[a]); copyto!(sh2[j], sh[a]); copy_particle!(st2[j], st[a]); dv2[j] = dv[a] + end + anc, anc2 = anc2, anc + sh, sh2 = sh2, sh + st, st2 = st2, st + dv, dv2 = dv2, dv # Mutation: random-walk Metropolis on the shocks, targeting the # stage-φ posterior π(ε) ∝ N(ε;0,I) · exp(-φ/2 · e(ε)ᵀH⁻¹e(ε)). @inbounds for p in 1:n_particles + shp = sh[p] for _ in 1:n_mh - εp = shocks[p] - εprop = εp .+ c .* randn(rng, nExo) - sprop = state_update(ancestors[p], εprop) - dprop = particle_quadratic_form(particle_full_state(sprop, pruning), data_col, observables_index, me_var, rows) - logα = -0.5 * ((sum(abs2, εprop) - sum(abs2, εp)) + φ_new * (dprop - dvec[p])) + Random.randn!(rng, eprop) + esq_old = 0.0 + esq_new = 0.0 + for e in 1:nExo + ep = shp[e] + c * eprop[e] + eprop[e] = ep + esq_new += ep * ep + esq_old += shp[e] * shp[e] + end + higher_propagate!(Val(algo), sprop, anc[p], eprop, past_idx, 𝐒f, scr) + dprop = particle_quadratic_form(measurement_full(sprop, full_buf), data_col, observables_index, me_var, rows) + logα = -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) if log(rand(rng)) < logα - shocks[p] = εprop - states[p] = sprop - dvec[p] = dprop + copyto!(shp, eprop) + copy_particle!(st[p], sprop) + dv[p] = dprop end end end @@ -645,7 +906,11 @@ function run_particle_filter(::Val{algo}, if t > presample_periods loglik += period_ll end - prev_states = states + # Carry the filtered states forward as next period's ancestors (copy so the + # `anc`/`st` pool identities stay stable for inference). + @inbounds for p in 1:n_particles + copy_particle!(anc[p], st[p]) + end end return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) From 3768eb07d4649d90c8f90c83d5b4c2cec8b00641 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Wed, 22 Jul 2026 23:51:20 +0200 Subject: [PATCH 04/24] Add LowLevelParticleFilters.jl comparison benchmark Standalone benchmark (benchmark/particle_filter_llpf_comparison.jl) comparing MacroModelling's particle filters against a bootstrap ParticleFilter from LowLevelParticleFilters.jl on the same first-order DSGE state space, cross-checked against the exact Kalman likelihood. LLPF/Distributions are not package deps; the script header explains running it in a throwaway environment. Findings (RBC 2-obs / SW07 7-obs, N=20000): on the well-conditioned RBC all three (MacroModelling bootstrap, MacroModelling tempered, LLPF bootstrap) agree with the Kalman value; MacroModelling's specialised filter is ~2x faster than LLPF on RBC and ~15x faster on SW07 (0.25s vs 3.7s per evaluation), and its bootstrap tracks the Kalman likelihood closely on SW07 where the generic LLPF setup (rank-deficient DSGE process noise mapped to a jittered MvNormal) drifts. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HhhiqdZNkzbGZKJ8oSeoTp --- benchmark/particle_filter_llpf_comparison.jl | 109 +++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 benchmark/particle_filter_llpf_comparison.jl diff --git a/benchmark/particle_filter_llpf_comparison.jl b/benchmark/particle_filter_llpf_comparison.jl new file mode 100644 index 000000000..e9b9baed5 --- /dev/null +++ b/benchmark/particle_filter_llpf_comparison.jl @@ -0,0 +1,109 @@ +# Benchmark: MacroModelling's particle filter vs LowLevelParticleFilters.jl +# +# Compares the log-likelihood and per-evaluation wall time of MacroModelling's +# built-in particle filters (`filter = :particle`) against a bootstrap +# `ParticleFilter` from LowLevelParticleFilters.jl, on the same first-order +# (linear) DSGE state space, cross-checked against the exact Kalman likelihood. +# +# LowLevelParticleFilters and Distributions are NOT dependencies of the package; +# run this in a throwaway environment, e.g. +# +# julia --project=/tmp/pfbench -e ' +# using Pkg; Pkg.develop(path="."); Pkg.add(["LowLevelParticleFilters","Distributions","AxisKeys","DelimitedFiles"])' +# julia --project=/tmp/pfbench benchmark/particle_filter_llpf_comparison.jl +# +# The DSGE structure is mapped onto LLPF's generic interface as +# xₜ = A·xₜ₋₁ + wₜ, wₜ ~ N(0, B Bᵀ) (rank-deficient process noise) +# yₜ = xₜ[observables] + vₜ, vₜ ~ N(0, H) +# with the initial cloud drawn from the ergodic covariance Σ (discrete Lyapunov). + +using MacroModelling +using LowLevelParticleFilters +using Distributions +using Random, DelimitedFiles, AxisKeys +import Statistics +import LinearAlgebra as ℒ +import MacroModelling: get_relevant_steady_state_and_state_update + +discrete_lyap(A, Q) = begin # X = A X Aᵀ + Q via squaring/doubling + X = copy(Q); Ak = copy(A) + for _ in 1:80 + X = X + Ak * X * Ak' + Ak = Ak * Ak + maximum(abs, Ak) < 1e-15 && break + end + X +end + +# Pull the exact first-order state space (deviation form) the package filter uses. +function extract_linear(m, data_levels, observables, params) + constants, SS_and_pars, 𝐒, _, _ = get_relevant_steady_state_and_state_update(Val(:first_order), params, m) + T = constants.post_model_macro + nVars = T.nVars; nPast = T.nPast_not_future_and_mixed + ssnames = constants.post_complete_parameters.SS_and_pars_names + obs_idx = convert(Vector{Int}, indexin(observables, ssnames)) + A = zeros(nVars, nVars); A[:, T.past_not_future_and_mixed_idx] .= 𝐒[:, 1:nPast] + B = Matrix(𝐒[:, nPast+1:end]) + dev = collect(data_levels) .- SS_and_pars[obs_idx] + return A, B, obs_idx, discrete_lyap(A, B * B'), dev, nVars +end + +function bench_model(name, m, data, observables, me; N = 20000, nseed = 8) + params = m.parameter_values + kal = get_loglikelihood(m, data(observables), params; filter = :kalman, + presample_periods = 0, initial_covariance = :theoretical, + measurement_error_std = me) + println("\n==== $name (N=$N) ====") + println("Kalman+ME = ", round(kal, digits = 3)) + + for pfa in (:bootstrap, :tempered) + Nn = pfa == :tempered ? N ÷ 3 : N + t0 = time() + lls = [get_loglikelihood(m, data(observables), params; filter = :particle, + algorithm = :first_order, presample_periods = 0, initial_covariance = :theoretical, + measurement_error_std = me, particle_filter_algorithm = pfa, + n_particles = Nn, rng = Random.Xoshiro(s)) for s in 1:nseed] + println("MacroModelling ", rpad(String(pfa), 10), " N=$Nn mean=", round(Statistics.mean(lls), digits = 2), + " std=", round(Statistics.std(lls), digits = 2), " time/run=", round((time() - t0) / nseed, digits = 3), "s") + end + + A, B, obs_idx, Σ, dev, nVars = extract_linear(m, data(observables), observables, params) + me_var = (me isa AbstractVector ? collect(me) : fill(me, length(observables))) .^ 2 + nObs = length(observables); nT = size(dev, 2) + df = MvNormal(zeros(nVars), ℒ.Symmetric(B * B') + 1e-10ℒ.I) + dg = MvNormal(zeros(nObs), ℒ.Diagonal(me_var)) + d0 = MvNormal(zeros(nVars), ℒ.Symmetric(Σ) + 1e-10ℒ.I) + u = [Float64[] for _ in 1:nT] + y = [collect(dev[:, t]) for t in 1:nT] + t0 = time() + lls = map(1:nseed) do s + Random.seed!(s) + loglik(ParticleFilter(N, (x, u, p, t) -> A * x, (x, u, p, t) -> x[obs_idx], df, dg, d0), u, y) + end + println("LLPF ", rpad("bootstrap", 10), " N=$N mean=", round(Statistics.mean(lls), digits = 2), + " std=", round(Statistics.std(lls), digits = 2), " time/run=", round((time() - t0) / nseed, digits = 3), "s") +end + +@model RBC2 begin + 1 / c[0] = (β / c[1]) * (α * exp(z[1]) * k[0]^(α - 1) + (1 - δ)) + c[0] + k[0] = (1 - δ) * k[-1] + q[0] + q[0] = exp(z[0]) * k[-1]^α * exp(g[0]) + z[0] = ρz * z[-1] + std_z * eps_z[x] + g[0] = ρg * g[-1] + std_g * eps_g[x] +end +@parameters RBC2 begin + std_z = 0.01; std_g = 0.01; ρz = 0.4; ρg = 0.6; δ = 0.02; α = 0.5; β = 0.95 +end +Random.seed!(12345) +data_rbc = MacroModelling.simulate(RBC2, periods = 40)([:c, :q], :, :simulate) +bench_model("RBC (2 obs)", RBC2, data_rbc, [:c, :q], 0.002; N = 20000, nseed = 8) + +dat, header = readdlm(joinpath(@__DIR__, "..", "test", "data", "usmodel.csv"), ',', header = true) +dat = Float64.(dat); csv = vec(Symbol.(strip.(header))) +dsw = KeyedArray(dat', Variable = csv, Time = axes(dat, 1))([:dy, :dc, :dinve, :labobs, :pinfobs, :dw, :robs], 47:230) +obs_sw = [:dy, :dc, :dinve, :labobs, :pinfobs, :dwobs, :robs] +dsw = rekey(dsw, :Variable => obs_sw) +include(joinpath(@__DIR__, "..", "models", "Smets_Wouters_2007_linear.jl")) +SS(Smets_Wouters_2007_linear, parameters = [:crhoms => 0.01, :crhopinf => 0.01, :crhow => 0.01, :cmap => 0.01, :cmaw => 0.01]) +bench_model("SW07 (7 obs)", Smets_Wouters_2007_linear, dsw, obs_sw, + 2.0 .* [Statistics.std(collect(dsw(o))) for o in obs_sw]; N = 20000, nseed = 6) From 2ce7d1445119ba385faa02184353899f00857707 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 15:40:39 +0200 Subject: [PATCH 05/24] Make each particle filter its own `filter` value; smarter defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse `particle_filter_algorithm` into the `filter` argument: the filter is now fully identified by one symbol — `:bootstrap_particle`, `:auxiliary_particle` or `:tempered_particle` (with `:particle` kept as an alias for the bootstrap filter). A filter registry in `default_options.jl` drives validation and the internal variant dispatch. Prefix the particle-specific options so they read unambiguously alongside the other filter options: `particle_resampling`, `particle_resampling_threshold`, `particle_initial_state_scaling` and `particle_rng`. The filter bodies bind the historical short names once so the optimised hot loops are untouched. `measurement_error_std` now defaults to `:auto`, which resolves per filter: no measurement error for the Kalman and inversion filters (their previous behaviour), and 10% of each observable's sample standard deviation for the particle filters, which are degenerate without it. Raise the default particle count to 10_000, which keeps an SW07-sized problem accurate to a couple of log-likelihood points in well under a second per evaluation. Annotate the filter internals: why resampling is needed at all and how the four schemes trade variance against cost, what each stage and buffer of the bootstrap recursion does, and an intuitive account of what the auxiliary filter's look-ahead buys and why dividing the preview back out keeps it unbiased. The LLPF benchmark now takes its initial-state covariance from the package's own Lyapunov solver (via `particle_initial_state_covariance`) instead of a hand-rolled doubling loop, so both filters start from an identical prior. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- benchmark/particle_filter_llpf_comparison.jl | 31 ++- src/MacroModelling.jl | 14 +- src/common_docstrings.jl | 2 +- src/default_options.jl | 38 +++- src/filter/particle.jl | 206 +++++++++++++++---- src/get_functions.jl | 121 +++++++---- src/rrules.jl | 17 +- test/test_particle_filter.jl | 59 ++++-- test/test_particle_filter_sw07.jl | 7 +- 9 files changed, 356 insertions(+), 139 deletions(-) diff --git a/benchmark/particle_filter_llpf_comparison.jl b/benchmark/particle_filter_llpf_comparison.jl index e9b9baed5..ab95eb1e5 100644 --- a/benchmark/particle_filter_llpf_comparison.jl +++ b/benchmark/particle_filter_llpf_comparison.jl @@ -23,19 +23,13 @@ using Distributions using Random, DelimitedFiles, AxisKeys import Statistics import LinearAlgebra as ℒ -import MacroModelling: get_relevant_steady_state_and_state_update - -discrete_lyap(A, Q) = begin # X = A X Aᵀ + Q via squaring/doubling - X = copy(Q); Ak = copy(A) - for _ in 1:80 - X = X + Ak * X * Ak' - Ak = Ak * Ak - maximum(abs, Ak) < 1e-15 && break - end - X -end +import MacroModelling: get_relevant_steady_state_and_state_update, + particle_initial_state_covariance, merge_calculation_options # Pull the exact first-order state space (deviation form) the package filter uses. +# The initial-state covariance comes from the package's own Lyapunov solver via +# `particle_initial_state_covariance`, i.e. the very routine the particle filter +# uses to spread its initial cloud, so both filters start from the same prior. function extract_linear(m, data_levels, observables, params) constants, SS_and_pars, 𝐒, _, _ = get_relevant_steady_state_and_state_update(Val(:first_order), params, m) T = constants.post_model_macro @@ -45,7 +39,8 @@ function extract_linear(m, data_levels, observables, params) A = zeros(nVars, nVars); A[:, T.past_not_future_and_mixed_idx] .= 𝐒[:, 1:nPast] B = Matrix(𝐒[:, nPast+1:end]) dev = collect(data_levels) .- SS_and_pars[obs_idx] - return A, B, obs_idx, discrete_lyap(A, B * B'), dev, nVars + Σ, _ = particle_initial_state_covariance(m, T, merge_calculation_options(), :theoretical) + return A, B, obs_idx, Σ, dev, nVars end function bench_model(name, m, data, observables, me; N = 20000, nseed = 8) @@ -56,14 +51,14 @@ function bench_model(name, m, data, observables, me; N = 20000, nseed = 8) println("\n==== $name (N=$N) ====") println("Kalman+ME = ", round(kal, digits = 3)) - for pfa in (:bootstrap, :tempered) - Nn = pfa == :tempered ? N ÷ 3 : N + for pf_filter in (:bootstrap_particle, :tempered_particle) + Nn = pf_filter == :tempered_particle ? N ÷ 3 : N t0 = time() - lls = [get_loglikelihood(m, data(observables), params; filter = :particle, + lls = [get_loglikelihood(m, data(observables), params; filter = pf_filter, algorithm = :first_order, presample_periods = 0, initial_covariance = :theoretical, - measurement_error_std = me, particle_filter_algorithm = pfa, - n_particles = Nn, rng = Random.Xoshiro(s)) for s in 1:nseed] - println("MacroModelling ", rpad(String(pfa), 10), " N=$Nn mean=", round(Statistics.mean(lls), digits = 2), + measurement_error_std = me, + n_particles = Nn, particle_rng = Random.Xoshiro(s)) for s in 1:nseed] + println("MacroModelling ", rpad(String(pf_filter), 19), " N=$Nn mean=", round(Statistics.mean(lls), digits = 2), " std=", round(Statistics.std(lls), digits = 2), " time/run=", round((time() - t0) / nseed, digits = 3), "s") end diff --git a/src/MacroModelling.jl b/src/MacroModelling.jl index 065f6fa9d..10bb78c22 100644 --- a/src/MacroModelling.jl +++ b/src/MacroModelling.jl @@ -397,7 +397,12 @@ function normalize_filtering_options(filter::Symbol, shock_decomposition::Bool, warmup_iterations::Int; maxlog::Int = DEFAULT_MAXLOG) - @assert filter ∈ [:kalman, :inversion, :particle] "Currently only the Kalman filter (:kalman) for linear models, the inversion filter (:inversion) for linear and nonlinear models, and the particle filter (:particle) for linear and nonlinear models are supported." + # `:particle` is a convenience alias for the bootstrap particle filter. + filter = get(PARTICLE_FILTER_ALIASES, filter, filter) + + @assert filter ∈ SUPPORTED_FILTERS "Unsupported `filter = :$(filter)`. Choose the Kalman filter (`:kalman`, linear models), the inversion filter (`:inversion`, linear and nonlinear models), or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`; linear and nonlinear models). `:particle` is accepted as an alias for `:bootstrap_particle`." + + is_particle = filter ∈ PARTICLE_FILTERS pruning = algorithm ∈ (:pruned_second_order, :pruned_third_order) @@ -407,10 +412,11 @@ function normalize_filtering_options(filter::Symbol, end # Higher-order solutions are handled by the inversion filter by default, but - # the particle filter (`:particle`) is explicitly valid at every order too. - if algorithm != :first_order && filter ∉ (:inversion, :particle) + # the particle filters are explicitly valid at every order too. + if algorithm != :first_order && filter != :inversion && !is_particle @info "Higher order solution algorithms only support the inversion and particle filters. Setting `filter = :inversion`." maxlog = maxlog filter = :inversion + is_particle = false end if filter != :kalman && smooth @@ -419,7 +425,7 @@ function normalize_filtering_options(filter::Symbol, end if warmup_iterations > 0 - if filter ∈ (:kalman, :particle) + if filter == :kalman || is_particle @info "`warmup_iterations` is not a valid argument for the $(filter == :kalman ? "Kalman" : "particle") filter. Ignoring input for `warmup_iterations`." maxlog = maxlog warmup_iterations = 0 end diff --git a/src/common_docstrings.jl b/src/common_docstrings.jl index 2a966e253..a4e05d5da 100644 --- a/src/common_docstrings.jl +++ b/src/common_docstrings.jl @@ -13,7 +13,7 @@ const GENERALISED_IRF® = "`generalised_irf` [Default: `$(DEFAULT_GENERALISED_IR const GENERALISED_IRF_WARMUP_ITERATIONS® = "`generalised_irf_warmup_iterations` [Default: `$(DEFAULT_GENERALISED_IRF_WARMUP)`, Type: `Int`]: number of warm-up iterations used to draw the baseline paths in the generalised IRF simulation. Only applied when `generalised_irf = true`." const GENERALISED_IRF_DRAWS® = "`generalised_irf_draws` [Default: `$(DEFAULT_GENERALISED_IRF_DRAWS)`, Type: `Int`]: number of Monte Carlo draws used to compute the generalised IRF. Only applied when `generalised_irf = true`." const ALGORITHM® = "`algorithm` [Default: `$(DEFAULT_ALGORITHM)`, Type: `Symbol`]: algorithm to solve for the dynamics of the model. Available algorithms: `:first_order`, `:second_order`, `:pruned_second_order`, `:third_order`, `:pruned_third_order`" -const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) only works for linear problems; the inversion filter (`:inversion`) works for linear and nonlinear models; the particle filter (`:particle`) works for linear and nonlinear models and integrates out the structural shocks by Monte Carlo (it requires measurement error via `measurement_error_std`, is selected via `particle_filter_algorithm`, and is a stochastic, non-differentiable estimator suited to gradient-free samplers). If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically." +const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error_std`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." const LEVELS® = "return levels or absolute deviations from the relevant steady state corresponding to the solution algorithm (e.g. stochastic steady state for higher order solution algorithms)." const CONDITIONS® = "`conditions` [Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}}`]: conditions for which to find the corresponding shocks. The input can have multiple formats, but for all types of entries, the first dimension corresponds to variables and the second dimension to the number of periods. The conditions can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the conditions are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as conditions. Note that conditioning variables to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input conditions is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as conditions and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of variables (of type `Symbol` or `String`) for which conditions are specified can be included and all other variables are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the conditions for the specified variables bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const SHOCK_CONDITIONS® = "`shocks` [Default: `nothing`, Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}, Nothing}`]: known values of shocks. This argument allows including certain shock values. By entering restrictions on the shocks in this way the problem to match the conditions on endogenous variables is restricted to the remaining free shocks in the respective period. The input can have multiple formats, but for all types of entries, the first dimension corresponds to shocks and the second dimension to the number of periods. `shocks` can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the shocks are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as certain shock values. Note that conditioning shocks to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input known shocks is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as known shocks and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of shocks (of type `Symbol` or `String`) for which values are specified can be included and all other shocks are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the values for the specified shocks bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." diff --git a/src/default_options.jl b/src/default_options.jl index 9f2af1b9b..1a8607f2e 100644 --- a/src/default_options.jl +++ b/src/default_options.jl @@ -8,14 +8,36 @@ const DEFAULT_SHOCK_DECOMPOSITION_SELECTOR = algorithm -> algorithm ∉ (:second const DEFAULT_SMOOTH_SELECTOR = filter -> filter == :kalman const DEFAULT_WARMUP_ITERATIONS = 0 const DEFAULT_PRESAMPLE_PERIODS = 0 -const DEFAULT_MEASUREMENT_ERROR_STD = 0.0 - -# Particle filter defaults (see `src/filter/particle.jl`) -const DEFAULT_N_PARTICLES = 1000 -const DEFAULT_PARTICLE_FILTER_ALGORITHM = :bootstrap -const DEFAULT_RESAMPLING = :systematic -const DEFAULT_RESAMPLING_THRESHOLD = 0.5 -const DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR = 1.0 + +# ── Filter registry ────────────────────────────────────────────────────────── +# Each particle-filter variant is its own `filter` value, so the filter is fully +# identified by a single symbol (no separate "which particle filter" argument). +const PARTICLE_FILTERS = (:bootstrap_particle, :auxiliary_particle, :tempered_particle) +const SUPPORTED_FILTERS = (:kalman, :inversion, PARTICLE_FILTERS...) +# `:particle` is accepted as a convenience alias for the bootstrap filter. +const PARTICLE_FILTER_ALIASES = Dict(:particle => :bootstrap_particle) +# Maps a filter symbol onto the internal variant tag used for dispatch. +const PARTICLE_FILTER_VARIANT = Dict(:bootstrap_particle => :bootstrap, + :auxiliary_particle => :auxiliary, + :tempered_particle => :tempered) + +# ── Measurement error ──────────────────────────────────────────────────────── +# `:auto` resolves per filter: no measurement error for the Kalman and inversion +# filters (their historical behaviour), and a small data-driven value for the +# particle filters, which are degenerate without it. +const DEFAULT_MEASUREMENT_ERROR_STD = :auto +# Auto measurement-error standard deviation as a fraction of each observable's +# sample standard deviation. +const DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION = 0.1 + +# ── Particle filter defaults (see `src/filter/particle.jl`) ────────────────── +# 10_000 particles keeps a Smets-Wouters-sized problem (7 observables, ~180 +# periods) accurate to a couple of log-likelihood points in well under a second +# per evaluation; raise it when the likelihood is used inside a sampler. +const DEFAULT_N_PARTICLES = 10_000 +const DEFAULT_PARTICLE_RESAMPLING = :systematic +const DEFAULT_PARTICLE_RESAMPLING_THRESHOLD = 0.5 +const DEFAULT_PARTICLE_INITIAL_STATE_SCALING = 1.0 # Tempered particle filter (Herbst & Schorfheide, 2019) controls const DEFAULT_TEMPERING_TARGET_RATIO = 2.0 const DEFAULT_TEMPERING_MH_STEPS = 1 diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 6528fe6b6..b2805259e 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -20,13 +20,45 @@ # ── Resampling schemes ─────────────────────────────────────────────────────── +# +# Why resample at all? Reweighting alone degenerates: after a few periods almost +# all of the weight sits on one particle and the cloud carries no information +# about the state distribution. Resampling replaces the weighted cloud with an +# equally weighted one — duplicating heavy particles, dropping light ones — so +# the computational effort follows the probability mass. +# +# Every scheme below is unbiased (E[times particle i is picked] = N·Wᵢ), so the +# likelihood estimate stays unbiased whichever one is used. They differ only in +# the *variance* of the counts, i.e. how much extra Monte-Carlo noise resampling +# itself injects. Ranked from lowest to highest added variance: +# +# :systematic — one uniform draw, then N equally spaced points through the +# cumulative weights. Lowest variance and the cheapest (a single +# random number, one pass); the sensible default. Its one caveat +# is that the N draws are perfectly correlated, which can matter +# for some theoretical guarantees but not for the likelihood. +# :stratified — one independent uniform per stratum of width 1/N. Almost as +# low variance as systematic but with independent draws, which +# restores those guarantees; a safe, slightly noisier default. +# :residual — deterministically assign ⌊N·Wᵢ⌋ copies, then draw only the +# remainder multinomially. Removes the integer part of the noise +# entirely; useful when a few particles dominate the weights. +# :multinomial — N independent draws from the weights. The textbook scheme and +# the easiest to reason about, but the noisiest; kept mainly as +# a reference implementation. +# # Each returns a length-N vector of ancestor indices drawn from the normalised -# weights `W` (which must sum to one). Systematic/stratified have lower variance -# than multinomial and are the recommended defaults. +# weights `W` (which must sum to one). -# Effective sample size 1 / Σ Wᵢ². +# Effective sample size, 1 / Σ Wᵢ². Equals N when the weights are uniform and 1 +# when a single particle holds all the mass, so it measures how many particles +# are "really" contributing. The filters resample once it drops below a fraction +# of N, which avoids paying the resampling noise in periods that do not need it. effective_sample_size(W::AbstractVector{<:Real}) = 1.0 / sum(abs2, W) +# Walk N equally spaced points u₀, u₀+1/N, … through the cumulative weights, with +# a single random offset u₀ ∈ [0, 1/N). A particle of weight Wᵢ spans Wᵢ·N spacings +# so it is picked either ⌊N·Wᵢ⌋ or ⌈N·Wᵢ⌉ times — never far from its expectation. function systematic_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) idxs = Vector{Int}(undef, N) @@ -44,6 +76,9 @@ function systematic_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{ return idxs end +# Split [0,1) into N strata of width 1/N and draw one independent uniform inside +# each. Guarantees at most one draw per stratum (so counts stay close to N·Wᵢ) +# while keeping the draws independent, unlike the systematic scheme. function stratified_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) idxs = Vector{Int}(undef, N) @@ -60,6 +95,9 @@ function stratified_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{ return idxs end +# N independent draws from the categorical distribution defined by W, via binary +# search on the cumulative weights. Simplest and noisiest: nothing prevents a +# particle with weight 1/N from being drawn three times or not at all. function multinomial_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) c = cumsum(W) @@ -71,6 +109,10 @@ function multinomial_resample_indices(rng::Random.AbstractRNG, W::AbstractVector return idxs end +# Deterministic part first: particle i gets ⌊N·Wᵢ⌋ guaranteed copies, which carry +# no randomness at all. Only the leftover R = N - Σ⌊N·Wᵢ⌋ slots are drawn, from +# the renormalised fractional weights. Cuts the variance of the integer part to +# zero, which helps most when a handful of particles dominate. function residual_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) idxs = Vector{Int}(undef, N) @@ -391,10 +433,10 @@ function run_particle_filter(::Val{algo}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -404,6 +446,10 @@ function run_particle_filter(::Val{algo}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) @@ -428,6 +474,32 @@ function run_particle_filter(::Val{algo}, resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) end +# The bootstrap recursion, period by period. One iteration of the loop below is +# the textbook predict / weight / resample cycle: +# +# 1. PREDICT draw a fresh shock for every particle and push it through the +# model's state transition. The cloud now represents p(xₜ | y₁..ₜ₋₁). +# 2. WEIGHT score each particle by how well it explains today's observation, +# p(yₜ | xₜ). Averaging those scores over the (weighted) cloud is an +# unbiased estimate of the period's likelihood contribution, which +# is what gets accumulated into `loglik`. +# 3. RESAMPLE if the weights have become too uneven, replace the weighted cloud +# by an equally weighted one so the next predict step spends its +# particles where the probability mass actually is. +# +# Arguments (the ones that are not self-evident): +# particles / particles2 two pools of the same shape, used as a double buffer: +# we always write the propagated cloud into the spare +# pool and then swap, which avoids allocating per period. +# 𝐒f perturbation solution matrices, already densified. +# scr preallocated kron/augmented-state scratch for the +# nonlinear transition (see `build_higher_scratch`). +# past_idx positions of the predetermined states inside a state +# vector — what the transition actually reads. +# me_var per-observable measurement-error variances. +# rows which observables are actually observed this period +# (all of them unless the data has holes). +# # Function barrier: `particles`/`particles2` arrive with a concrete element type, # so the hot loop specialises and runs allocation-free (the enclosing kwarg method # body is too large for inference to keep the pool types). @@ -436,13 +508,13 @@ function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, p data_in_deviations, obs_idx_per_t, has_missing, me_var, resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) where {algo} n_particles = length(particles) - shock = Vector{Float64}(undef, nExo) - full_buf = Vector{Float64}(undef, nVars) - W = fill(1.0 / n_particles, n_particles) - logdens = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) - loglik = 0.0 + shock = Vector{Float64}(undef, nExo) # one draw of structural shocks, reused + full_buf = Vector{Float64}(undef, nVars) # summed state of a pruned particle + W = fill(1.0 / n_particles, n_particles) # normalised importance weights + logdens = Vector{Float64}(undef, n_particles) # log p(yₜ | xₜ) per particle + idx = Vector{Int}(undef, n_particles) # ancestor indices from resampling + bins = Vector{Float64}(undef, n_particles) # cumulative-weight scratch + loglik = 0.0 for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) @@ -458,16 +530,23 @@ function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, p continue end + # 1. PREDICT + 2. SCORE, fused into one pass over the cloud: draw this + # particle's shocks, push it one period forward, and evaluate how well + # the resulting state explains today's observation. @inbounds for p in 1:n_particles Random.randn!(rng, shock) higher_propagate!(Val(algo), particles2[p], particles[p], shock, past_idx, 𝐒f, scr) full = measurement_full(particles2[p], full_buf) logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) end - particles, particles2 = particles2, particles + particles, particles2 = particles2, particles # propagated cloud becomes current + # The period's likelihood contribution is log Σₚ Wₚ·p(yₜ|xₜᵖ). Factor out + # the largest log-density first (log-sum-exp) so the exponentials cannot + # underflow to zero when every particle fits the data poorly. m = maximum(logdens) if !isfinite(m) + # every particle is impossible (or the model blew up): give up cleanly return Float64(on_failure_loglikelihood) end @@ -480,21 +559,25 @@ function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, p end ll_t = m + log(s) - if t > presample_periods + if t > presample_periods # presample periods only warm the cloud up loglik += ll_t end + # Bayes update of the weights: Wₚ ∝ Wₚ · p(yₜ|xₜᵖ), normalised by `s`. @inbounds for p in 1:n_particles W[p] = W[p] * exp(logdens[p] - m) / s end + # 3. RESAMPLE, but only once the cloud has actually degenerated. Doing it + # every period would add resampling noise for nothing; doing it never + # would leave all the weight on a single particle within a few periods. if effective_sample_size(W) < resampling_threshold * n_particles particle_resample_indices!(idx, bins, rng, W, resampling) @inbounds for j in 1:n_particles copy_particle!(particles2[j], particles[idx[j]]) end particles, particles2 = particles2, particles - fill!(W, 1.0 / n_particles) + fill!(W, 1.0 / n_particles) # survivors are equally likely again end end @@ -529,9 +612,32 @@ end # ── Auxiliary particle filter (Pitt & Shephard, 1999) ──────────────────────── -# A look-ahead stage reweights ancestors by the predictive likelihood evaluated -# at the transition mean (zero shock) before propagating, reducing variance when -# the signal is informative. The likelihood estimate remains unbiased. +# +# The problem it fixes. The bootstrap filter propagates every particle blindly — +# it draws shocks from the prior, *then* looks at the observation. Particles that +# were already heading somewhere the data rules out are propagated anyway and +# then killed by a near-zero weight, so a large part of the cloud is wasted. The +# more informative the observation (small measurement error, many observables), +# the more wasteful this is. +# +# The idea. Peek at the observation *before* choosing which ancestors to +# propagate. For each ancestor compute a cheap preview of how plausible it is +# going to look next period — here the measurement density at its transition +# mean (the zero-shock prediction), inflated by the shock-induced predictive +# variance so the preview is not artificially sharp. Resample ancestors in +# proportion to weight × preview, so parents likely to produce good children get +# more offspring, and only then draw shocks and propagate. +# +# Keeping it honest. Selecting ancestors with a preview biases the cloud, so the +# second stage divides it back out: each child's weight is the true measurement +# density divided by the preview used to pick its parent. The preview cancels +# exactly, leaving an unbiased likelihood estimate whatever preview is used — a +# bad preview costs efficiency, never correctness. +# +# When it helps. Most when the observation is informative but the one-step-ahead +# state is well predicted by its mean. It costs roughly one extra transition +# evaluation per particle per period, so with a weak signal (large measurement +# error) the plain bootstrap filter is the better trade. function run_particle_filter(::Val{algo}, ::Val{:auxiliary}, @@ -545,10 +651,10 @@ function run_particle_filter(::Val{algo}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -558,6 +664,10 @@ function run_particle_filter(::Val{algo}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) @@ -739,10 +849,10 @@ function run_particle_filter(::Val{algo}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -752,6 +862,10 @@ function run_particle_filter(::Val{algo}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) @@ -1080,10 +1194,10 @@ function run_particle_filter(::Val{:first_order}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -1093,6 +1207,10 @@ function run_particle_filter(::Val{:first_order}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) @@ -1182,10 +1300,10 @@ function run_particle_filter(::Val{:first_order}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -1195,6 +1313,10 @@ function run_particle_filter(::Val{:first_order}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) @@ -1312,10 +1434,10 @@ function run_particle_filter(::Val{:first_order}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, @@ -1325,6 +1447,10 @@ function run_particle_filter(::Val{:first_order}, tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 T = constants.post_model_macro + rng = particle_rng + resampling = particle_resampling + resampling_threshold = particle_resampling_threshold + initial_state_prior_scaling_factor = particle_initial_state_scaling nVars = T.nVars nExo = T.nExo nT = size(data_in_deviations, 2) diff --git a/src/get_functions.jl b/src/get_functions.jl index 24202064d..14d9fd986 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -4297,6 +4297,42 @@ function get_statistics(𝓂::ℳ, return ret end +# Resolve `measurement_error_std = :auto` for the filter-based `get_loglikelihood` +# path. The Kalman and inversion filters default to no measurement error (their +# historical behaviour). The particle filters are degenerate without measurement +# error — every particle would need to reproduce the observation exactly — so they +# default to a small fraction of each observable's sample standard deviation. +# This is a convenience default: for serious work set `measurement_error_std` +# (or estimate it) explicitly, since the likelihood level depends on it. +function resolve_auto_measurement_error_std(filter_choice::Symbol, data_in_deviations::AbstractMatrix) + filter_choice ∈ PARTICLE_FILTERS || return 0.0 + + n_obs = size(data_in_deviations, 1) + stds = Vector{Float64}(undef, n_obs) + + @inbounds for i in 1:n_obs + # sample standard deviation over the finite (observed) entries of row i + n = 0 + μ = 0.0 + for v in @view data_in_deviations[i, :] + if isfinite(v); n += 1; μ += v; end + end + s = 0.0 + if n > 1 + μ /= n + acc = 0.0 + for v in @view data_in_deviations[i, :] + if isfinite(v); acc += (v - μ)^2; end + end + s = sqrt(acc / (n - 1)) + end + # fall back to a unit scale for a (near) constant or unobserved series + stds[i] = DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION * (isfinite(s) && s > 0 ? s : 1.0) + end + + return stds +end + # Validate `measurement_error_std` supplied to the filter-based `get_loglikelihood` # path and return the per-observable measurement-error variances (in the observable # / data-row order), or `nothing` when no measurement error is active (all zero). @@ -4304,7 +4340,7 @@ end # Time-varying (matrix) measurement error is not yet supported on this path. function build_filter_measurement_error_variances(measurement_error_std, n_obs::Int) if measurement_error_std isa AbstractMatrix - error("Time-varying (matrix) `measurement_error_std` is not yet supported on the filter-based `get_loglikelihood` path (`filter = :kalman` / `:particle`); provide a scalar or a per-observable vector.") + error("Time-varying (matrix) `measurement_error_std` is not supported on the filter-based `get_loglikelihood` path; provide a scalar, a per-observable vector, or use `measurement_error_covariance` for a full covariance matrix.") end stds = measurement_error_std isa AbstractVector ? collect(float.(measurement_error_std)) : fill(float(measurement_error_std), n_obs) @@ -4322,9 +4358,9 @@ end """ $(SIGNATURES) -Return the loglikelihood of the model given the data and parameters provided. The loglikelihood is either calculated based on the inversion or the Kalman filter (depending on the `filter` keyword argument). By default the package selects the Kalman filter for first order solutions and the inversion filter for nonlinear (higher order) solution algorithms. The data must be provided as a `KeyedArray{Float64}` with the names of the variables to be matched in rows and the periods in columns. The `KeyedArray` type is provided by the `AxisKeys` package. +Return the loglikelihood of the model given the data and parameters provided. The loglikelihood is calculated with the filter selected by the `filter` keyword argument: the Kalman filter, the inversion filter, or one of the particle filters. By default the package selects the Kalman filter for first order solutions and the inversion filter for nonlinear (higher order) solution algorithms. The data must be provided as a `KeyedArray{Float64}` with the names of the variables to be matched in rows and the periods in columns. The `KeyedArray` type is provided by the `AxisKeys` package. -This function is differentiable and supports both the Kalman and inversion likelihoods. +The Kalman and inversion likelihoods are differentiable. The particle filters are stochastic Monte-Carlo estimators and are not differentiable; use them with gradient-free samplers. See the Filters section of the documentation for a comparison. If occasionally binding constraints are present in the model, they are not taken into account here. @@ -4341,13 +4377,16 @@ If occasionally binding constraints are present in the model, they are not taken - `initial_covariance` [Default: `:theoretical`, Type: `Union{Symbol,AbstractMatrix{<:Real}}`]: defines the method to initialise the Kalman filters covariance matrix. It can be initialised with the theoretical long run values (option `:theoretical`), large values (10.0) along the diagonal (option `:diagonal`), or a user-supplied matrix of appropriate size (number of observables and states). - $INITIAL_STATE® - `on_failure_loglikelihood` [Default: `-Inf`, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails. Setting this to a finite value can avoid errors in codes that rely on finite loglikelihood values, such as e.g. slice samplers (in Pigeons.jl). -- `measurement_error_std` [Default: `0.0`, Type: `Union{Real,AbstractVector{<:Real}}`]: standard deviation of Gaussian measurement error on the observables. A scalar is broadcast to all observables; a vector supplies one entry per observable. The default `0.0` disables measurement error (the previous behaviour). Measurement error is supported by the Kalman filter (`filter = :kalman`) and is required by the particle filter (`filter = :particle`); it is not available for the inversion filter. -- `n_particles` [Default: `1000`, Type: `Int`]: number of particles used when `filter = :particle`. -- `particle_filter_algorithm` [Default: `:bootstrap`, Type: `Symbol`]: particle filter variant when `filter = :particle`. One of `:bootstrap` (sequential-importance-resampling, as in Dynare), `:auxiliary` (Pitt–Shephard auxiliary particle filter), or `:tempered` (Herbst–Schorfheide tempered particle filter). -- `resampling` [Default: `:systematic`, Type: `Symbol`]: resampling scheme for the particle filter. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`. -- `resampling_threshold` [Default: `0.5`, Type: `Real`]: the particle filter resamples whenever the effective sample size falls below `resampling_threshold * n_particles`. -- `initial_state_prior_scaling_factor` [Default: `1.0`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state. -- `rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filter (pass a seeded RNG for reproducibility). +- `measurement_error_std` [Default: `:auto`, Type: `Union{Symbol,Real,AbstractVector{<:Real}}`]: standard deviation of Gaussian measurement error on the observables. A scalar is broadcast to all observables; a vector supplies one entry per observable. `:auto` resolves per filter: no measurement error for the Kalman and inversion filters, and $(DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION) times each observable's sample standard deviation for the particle filters, which are degenerate without it. Measurement error is supported by the Kalman filter and required by the particle filters; it is not available for the inversion filter, which reproduces the observables exactly. +- `n_particles` [Default: `$(DEFAULT_N_PARTICLES)`, Type: `Int`]: number of particles used by the particle filters. More particles reduce the Monte-Carlo variance of the likelihood at roughly linear cost. +- `particle_resampling` [Default: `:$(DEFAULT_PARTICLE_RESAMPLING)`, Type: `Symbol`]: resampling scheme. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`. +- `particle_resampling_threshold` [Default: `$(DEFAULT_PARTICLE_RESAMPLING_THRESHOLD)`, Type: `Real`]: resample whenever the effective sample size falls below `particle_resampling_threshold * n_particles`. +- `particle_initial_state_scaling` [Default: `$(DEFAULT_PARTICLE_INITIAL_STATE_SCALING)`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state. +- `particle_rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filters (pass a seeded RNG for reproducible likelihoods). +- `tempering_target_ratio` [Default: `$(DEFAULT_TEMPERING_TARGET_RATIO)`, Type: `Real`]: target inefficiency ratio that sets the tempering schedule of `filter = :tempered_particle`. +- `tempering_mh_steps` [Default: `$(DEFAULT_TEMPERING_MH_STEPS)`, Type: `Int`]: number of Metropolis-Hastings mutation steps per tempering stage. +- `tempering_max_stages` [Default: `$(DEFAULT_TEMPERING_MAX_STAGES)`, Type: `Int`]: cap on the number of tempering stages per period. +- `tempering_mh_scale` [Default: `$(DEFAULT_TEMPERING_MH_SCALE)`, Type: `Real`]: scale of the random-walk Metropolis-Hastings proposal used in the mutation step. - $QME® - $SYLVESTER® - $LYAPUNOV® @@ -4396,13 +4435,12 @@ function get_loglikelihood(𝓂::ℳ, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, n_particles::Int = DEFAULT_N_PARTICLES, - particle_filter_algorithm::Symbol = DEFAULT_PARTICLE_FILTER_ALGORITHM, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, @@ -4428,11 +4466,10 @@ function get_loglikelihood(𝓂::ℳ, filter_algorithm = filter_algorithm, measurement_error_std = measurement_error_std, n_particles = n_particles, - particle_filter_algorithm = particle_filter_algorithm, - resampling = resampling, - resampling_threshold = resampling_threshold, - initial_state_prior_scaling_factor = initial_state_prior_scaling_factor, - rng = rng, + particle_resampling = particle_resampling, + particle_resampling_threshold = particle_resampling_threshold, + particle_initial_state_scaling = particle_initial_state_scaling, + particle_rng = particle_rng, tempering_target_ratio = tempering_target_ratio, tempering_mh_steps = tempering_mh_steps, tempering_max_stages = tempering_max_stages, @@ -4458,13 +4495,12 @@ function get_loglikelihood(𝓂::ℳ, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, n_particles::Int = DEFAULT_N_PARTICLES, - particle_filter_algorithm::Symbol = DEFAULT_PARTICLE_FILTER_ALGORITHM, - resampling::Symbol = DEFAULT_RESAMPLING, - resampling_threshold::Real = DEFAULT_RESAMPLING_THRESHOLD, - initial_state_prior_scaling_factor::Real = DEFAULT_INITIAL_STATE_PRIOR_SCALING_FACTOR, - rng::Random.AbstractRNG = Random.default_rng(), + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, @@ -4603,31 +4639,38 @@ function get_loglikelihood(𝓂::ℳ, return zero(S) end + is_particle_filter = filter ∈ PARTICLE_FILTERS + # Diagonal Gaussian measurement-error variances (per observable, in data-row # order), or `nothing` when no measurement error is active. Supported by the # Kalman and particle filters; the inversion filter recovers shocks exactly - # and does not admit measurement error. - measurement_error_variances = build_filter_measurement_error_variances(measurement_error_std, size(data_in_deviations, 1)) + # and does not admit measurement error. `:auto` resolves per filter. + resolved_measurement_error_std = measurement_error_std === :auto ? + resolve_auto_measurement_error_std(filter, data_in_deviations) : measurement_error_std + + @assert !(resolved_measurement_error_std isa Symbol) "`measurement_error_std` must be `:auto`, a scalar, or a per-observable vector; got `:$(resolved_measurement_error_std)`." + + measurement_error_variances = build_filter_measurement_error_variances(resolved_measurement_error_std, size(data_in_deviations, 1)) if filter == :inversion && measurement_error_variances !== nothing - error("`measurement_error_std` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or `filter = :particle`.") + error("`measurement_error_std` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`).") end - if filter == :particle + if is_particle_filter if measurement_error_variances === nothing - error("The particle filter (`filter = :particle`) requires measurement error; set `measurement_error_std` to a positive value (scalar or per-observable vector).") + error("The particle filters require measurement error (they are degenerate without it); set `measurement_error_std` to a positive value (scalar or per-observable vector), or leave it at `:auto`.") end # The particle filter evaluates in Float64 and is not differentiable; a # forward-mode `Dual` parameter type would silently yield a zero gradient. if !(S <: AbstractFloat) - error("The particle filter (`filter = :particle`) is not differentiable and cannot be used with automatic differentiation. Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") + error("The particle filters are not differentiable and cannot be used with automatic differentiation. Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") end end # @timeit_debug timer "Filter" begin - llh = if filter == :particle + llh = if is_particle_filter run_particle_filter(Val(algorithm), - Val(particle_filter_algorithm), + Val(PARTICLE_FILTER_VARIANT[filter]), obs_indices, 𝐒, data_in_deviations, @@ -4638,10 +4681,10 @@ function get_loglikelihood(𝓂::ℳ, obs_idx_per_t, has_missing; n_particles = n_particles, - resampling = resampling, - resampling_threshold = resampling_threshold, - initial_state_prior_scaling_factor = initial_state_prior_scaling_factor, - rng = rng, + particle_resampling = particle_resampling, + particle_resampling_threshold = particle_resampling_threshold, + particle_initial_state_scaling = particle_initial_state_scaling, + particle_rng = particle_rng, presample_periods = presample_periods, initial_covariance = initial_covariance, on_failure_loglikelihood = on_failure_loglikelihood, diff --git a/src/rrules.jl b/src/rrules.jl index ba3ee74f7..6ae0a7afb 100644 --- a/src/rrules.jl +++ b/src/rrules.jl @@ -1850,7 +1850,7 @@ function rrule(::typeof(get_loglikelihood), presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, @@ -1865,10 +1865,19 @@ function rrule(::typeof(get_loglikelihood), # The particle filter is a stochastic, non-differentiable estimator and the # Kalman likelihood with measurement error does not yet ship an analytical # reverse-mode rule. Fail loudly rather than return an incorrect gradient. - if filter == :particle - error("The particle filter (`filter = :particle`) is not differentiable and cannot be used with reverse-mode automatic differentiation (Zygote/Mooncake). Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") + if filter ∈ PARTICLE_FILTERS + error("The particle filters (`filter = :$(filter)`) are not differentiable and cannot be used with reverse-mode automatic differentiation (Zygote/Mooncake). Use a gradient-free sampler (e.g. Pigeons slice sampling or nested sampling).") + end + # `:auto` means "no measurement error" for the non-particle filters, so it is + # the one symbolic value that can reach here and is always differentiable. + me_active = if measurement_error_std isa Symbol + false + elseif measurement_error_std isa AbstractArray + any(x -> x != 0, measurement_error_std) + else + measurement_error_std != 0 end - if measurement_error_std isa AbstractArray ? any(x -> x != 0, measurement_error_std) : measurement_error_std != 0 + if me_active error("Reverse-mode automatic differentiation of the Kalman likelihood with measurement error (`measurement_error_std`) is not yet supported. Use forward-mode AD (e.g. `AutoForwardDiff`) or a gradient-free sampler.") end diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 79cae6092..34dd2d061 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -58,8 +58,8 @@ threw(f) = try; f(); false; catch; true; end # The bootstrap particle-filter likelihood estimator is unbiased for the # true likelihood, so log L̂ is downward biased by ≈ Var(log L̂)/2 and both # the bias and the variance shrink with the number of particles. - pf(N, s) = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, - measurement_error_std = me, n_particles = N, rng = Random.Xoshiro(s)) + pf(N, s) = get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_std = me, n_particles = N, particle_rng = Random.Xoshiro(s)) nseeds = 24 ll_small = [pf(2_000, 100 + s) for s in 1:nseeds] ll_large = [pf(16_000, 200 + s) for s in 1:nseeds] @@ -78,13 +78,13 @@ threw(f) = try; f(); false; catch; true; end end @testset "Variants: correct and ordered by efficiency" begin - variant(pfa, N, s) = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, - particle_filter_algorithm = pfa, measurement_error_std = me, - n_particles = N, rng = Random.Xoshiro(s)) + variant(pf_filter, N, s) = get_loglikelihood(RBC_pf, data, p; filter = pf_filter, algorithm = :first_order, + measurement_error_std = me, + n_particles = N, particle_rng = Random.Xoshiro(s)) nseeds = 16 - boot = [variant(:bootstrap, 3_000, 300 + s) for s in 1:nseeds] - aux = [variant(:auxiliary, 3_000, 300 + s) for s in 1:nseeds] - temp = [variant(:tempered, 3_000, 300 + s) for s in 1:nseeds] + boot = [variant(:bootstrap_particle, 3_000, 300 + s) for s in 1:nseeds] + aux = [variant(:auxiliary_particle, 3_000, 300 + s) for s in 1:nseeds] + temp = [variant(:tempered_particle, 3_000, 300 + s) for s in 1:nseeds] for v in (boot, aux, temp) @test all(isfinite, v) @@ -113,15 +113,15 @@ threw(f) = try; f(); false; catch; true; end @testset "Higher-order algorithms run" begin for algo in (:first_order, :second_order, :pruned_second_order, :third_order, :pruned_third_order) - llh = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = algo, - measurement_error_std = me, n_particles = 3_000, rng = Random.Xoshiro(7)) + llh = get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = algo, + measurement_error_std = me, n_particles = 3_000, particle_rng = Random.Xoshiro(7)) @test isfinite(llh) end # every variant runs at a pruned nonlinear order - for pfa in (:bootstrap, :auxiliary, :tempered) - llh = get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :pruned_second_order, - particle_filter_algorithm = pfa, measurement_error_std = me, - n_particles = 3_000, rng = Random.Xoshiro(7)) + for pf_filter in (:bootstrap_particle, :auxiliary_particle, :tempered_particle) + llh = get_loglikelihood(RBC_pf, data, p; filter = pf_filter, algorithm = :pruned_second_order, + measurement_error_std = me, + n_particles = 3_000, particle_rng = Random.Xoshiro(7)) @test isfinite(llh) end end @@ -132,25 +132,42 @@ threw(f) = try; f(); false; catch; true; end raw[2, 20] = missing datam = KeyedArray(raw, Variable = [:c, :q], Time = 1:size(raw, 2)) kal_m = get_loglikelihood(RBC_pf, datam, p; filter = :kalman, measurement_error_std = me) - pf_m = get_loglikelihood(RBC_pf, datam, p; filter = :particle, algorithm = :first_order, - measurement_error_std = me, n_particles = 16_000, rng = Random.Xoshiro(9)) + pf_m = get_loglikelihood(RBC_pf, datam, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_std = me, n_particles = 16_000, particle_rng = Random.Xoshiro(9)) @test isfinite(kal_m) @test isfinite(pf_m) @test abs(kal_m - pf_m) < 3.0 end + @testset "Filter selection and automatic measurement error" begin + # `:particle` is an alias for the bootstrap filter: same RNG ⇒ same value + @test get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, + measurement_error_std = me, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) == + get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_std = me, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) + # `:auto` leaves the Kalman filter without measurement error + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman) == + get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = :auto) + # `:auto` gives the particle filters a workable measurement error + @test isfinite(get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + n_particles = 2_000, particle_rng = Random.Xoshiro(5))) + # an unknown filter name is rejected + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :not_a_filter)) + end + @testset "Error guards" begin # measurement error is not available for the inversion filter @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :inversion, measurement_error_std = me)) # the particle filter requires measurement error - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order)) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_std = 0.0)) # the particle filter is not differentiable (forward or reverse mode) - @test threw(() -> ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :particle, + @test threw(() -> ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :bootstrap_particle, algorithm = :first_order, measurement_error_std = me, n_particles = 500, - rng = Random.Xoshiro(1)), p)) - @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :particle, + particle_rng = Random.Xoshiro(1)), p)) + @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :bootstrap_particle, algorithm = :first_order, measurement_error_std = me, n_particles = 500, - rng = Random.Xoshiro(1)), p)) + particle_rng = Random.Xoshiro(1)), p)) # reverse-mode AD of the Kalman likelihood with measurement error is guarded @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, measurement_error_std = me), p)) diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index 0c804914a..c70cd9207 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -39,11 +39,10 @@ using DelimitedFiles, AxisKeys measurement_error_std = me) @test isfinite(kal) - for (pfa, N) in ((:bootstrap, 20_000), (:auxiliary, 20_000), (:tempered, 8_000)) - lls = [get_loglikelihood(m, data(observables), p; filter = :particle, algorithm = :first_order, + for (pf_filter, N) in ((:bootstrap_particle, 20_000), (:auxiliary_particle, 20_000), (:tempered_particle, 8_000)) + lls = [get_loglikelihood(m, data(observables), p; filter = pf_filter, algorithm = :first_order, presample_periods = 4, initial_covariance = :theoretical, - measurement_error_std = me, particle_filter_algorithm = pfa, - n_particles = N, rng = Random.Xoshiro(1000 + s)) for s in 1:6] + measurement_error_std = me, n_particles = N, particle_rng = Random.Xoshiro(1000 + s)) for s in 1:6] @test all(isfinite, lls) # the Monte-Carlo mean matches the Kalman value up to the (downward) Var/2 bias @test abs(kal - Statistics.mean(lls)) < 15 From 173a88ea20e4dd3ab9469b962ba5a1ab7e051975 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 15:49:37 +0200 Subject: [PATCH 06/24] Support a full measurement-error covariance matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `measurement_error_covariance` keyword to `get_loglikelihood` for correlated measurement error, superseding `measurement_error_std` when supplied and validated as square, symmetric and positive definite. Nothing in a filter requires H to be diagonal — only H⁻¹ and log det H are ever needed. The Kalman filter forms its innovation covariance as a matrix anyway, so it now accepts an arbitrary H, in both the dense and the missing-data recursion (where the observed sub-block H[idx, idx] is used) and in the ForwardDiff extension. A diagonal covariance reproduces the per-observable standard deviations exactly. The particle filters keep the diagonal fast path: their inner loop is an elementwise quadratic form, and correlated measurement error in a DSGE is more naturally written into the model itself — measurement-error processes in the observation equations move the correlation into the state transition and leave H diagonal. An off-diagonal covariance is therefore rejected with a message pointing at both alternatives. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- ext/ForwardDiffExt.jl | 15 ++++++++++---- src/filter/kalman.jl | 38 ++++++++++++++++++++++++------------ src/filter/particle.jl | 14 +++++++++++-- src/get_functions.jl | 26 ++++++++++++++++++++++-- test/test_particle_filter.jl | 23 ++++++++++++++++++++++ 5 files changed, 96 insertions(+), 20 deletions(-) diff --git a/ext/ForwardDiffExt.jl b/ext/ForwardDiffExt.jl index 9b6892cb3..6b2d655ae 100644 --- a/ext/ForwardDiffExt.jl +++ b/ext/ForwardDiffExt.jl @@ -1009,7 +1009,7 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::ℱ.Dual{Z,S,N} where {Z,S,N,R <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -1073,10 +1073,17 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, ℒ.mul!(CP, C, P) ℒ.mul!(F_buf, CP, C') - # Add the diagonal measurement-error covariance H: F = C P C' + H. + # Add the measurement-error covariance H: F = C P C' + H (a vector of + # per-observable variances, or a full covariance matrix). if measurement_error_variances !== nothing - @inbounds for i in 1:no - F_buf[i, i] += measurement_error_variances[i] + if measurement_error_variances isa AbstractMatrix + @inbounds for j in 1:no, i in 1:no + F_buf[i, j] += measurement_error_variances[i, j] + end + else + @inbounds for i in 1:no + F_buf[i, i] += measurement_error_variances[i] + end end end diff --git a/src/filter/kalman.jl b/src/filter/kalman.jl index 1c2a1795b..ed2f7488e 100644 --- a/src/filter/kalman.jl +++ b/src/filter/kalman.jl @@ -16,7 +16,7 @@ function calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -81,7 +81,7 @@ function calculate_loglikelihood_with_missing(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -153,7 +153,7 @@ function run_kalman_iterations(A::Matrix{S}, u₀::AbstractVector{V}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, # timer::TimerOutput = TimerOutput(), verbose::Bool = false) where {S <: Real, R <: Real, V <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -211,12 +211,19 @@ function run_kalman_iterations(A::Matrix{S}, ℒ.mul!(Ctmp, C, Pwork) # Ctmp = C * P ℒ.mul!(F, Ctmp, C') # F = C * P * C' - # Add the diagonal measurement-error covariance H: F = C P C' + H. - # `measurement_error_variances` holds the per-observable variances in the - # innovation (data-row) order, which matches F's rows. + # Add the measurement-error covariance H: F = C P C' + H. `H` may be a + # vector of per-observable variances (diagonal H, the common case) or a + # full covariance matrix; both are in the innovation (data-row) order, + # which matches F's rows and columns. if measurement_error_variances !== nothing - @inbounds for i in 1:n_obs - F[i, i] += measurement_error_variances[i] + if measurement_error_variances isa AbstractMatrix + @inbounds for j in 1:n_obs, i in 1:n_obs + F[i, j] += measurement_error_variances[i, j] + end + else + @inbounds for i in 1:n_obs + F[i, i] += measurement_error_variances[i] + end end end @@ -312,7 +319,7 @@ function run_kalman_iterations_missing(A::Matrix{S}, u₀::AbstractVector{<:Real}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real}} = nothing, + measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, verbose::Bool = false)::S where {S <: Float64, R <: Real, U <: AbstractFloat} n_obs = size(C, 1) @@ -374,10 +381,17 @@ function run_kalman_iterations_missing(A::Matrix{S}, ℒ.mul!(Ctv, Cv, P) # Ctv = C[idx,:] * P ℒ.mul!(Fv, Ctv, Cv') # Fv = C[idx,:] * P * C[idx,:]' - # Add the diagonal measurement-error covariance for the observed rows. + # Add the measurement-error covariance restricted to the observed rows + # (the conditional block H[idx, idx] of a full covariance matrix). if measurement_error_variances !== nothing - @inbounds for i in 1:m - Fv[i, i] += measurement_error_variances[idx[i]] + if measurement_error_variances isa AbstractMatrix + @inbounds for j in 1:m, i in 1:m + Fv[i, j] += measurement_error_variances[idx[i], idx[j]] + end + else + @inbounds for i in 1:m + Fv[i, i] += measurement_error_variances[idx[i]] + end end end diff --git a/src/filter/particle.jl b/src/filter/particle.jl index b2805259e..a83a70a53 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -2,8 +2,18 @@ # Particle filters for the (possibly nonlinear) DSGE state-space representation. # -# The measurement equation is yₜ = full_stateₜ[observables] + ηₜ, ηₜ ~ N(0, H), -# with H a diagonal matrix of measurement-error variances. The structural shocks +# The measurement equation is yₜ = full_stateₜ[observables] + ηₜ, ηₜ ~ N(0, H). +# +# H is taken to be diagonal here. Nothing in the algorithm requires that — the +# filters only ever need H⁻¹ and log det H, so a full covariance would just mean +# replacing the elementwise quadratic form below by a triangular solve against a +# Cholesky factor of H (cached per missing-data pattern). It is kept diagonal +# because that is what the elementwise inner loop is optimised for, and because +# correlated measurement error in a DSGE is more naturally written into the model +# itself: adding measurement-error processes to the observation equations makes +# the correlation part of the state transition and leaves H diagonal again. The +# Kalman filter, whose innovation covariance is formed as a matrix anyway, does +# accept an arbitrary `measurement_error_covariance`. The structural shocks # are i.i.d. standard normal (their standard deviations are baked into the # solution matrices 𝐒), and the state transition is the perturbation solution's # `state_update` (first order through pruned third order). diff --git a/src/get_functions.jl b/src/get_functions.jl index 14d9fd986..a0386fd7e 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -4378,6 +4378,7 @@ If occasionally binding constraints are present in the model, they are not taken - $INITIAL_STATE® - `on_failure_loglikelihood` [Default: `-Inf`, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails. Setting this to a finite value can avoid errors in codes that rely on finite loglikelihood values, such as e.g. slice samplers (in Pigeons.jl). - `measurement_error_std` [Default: `:auto`, Type: `Union{Symbol,Real,AbstractVector{<:Real}}`]: standard deviation of Gaussian measurement error on the observables. A scalar is broadcast to all observables; a vector supplies one entry per observable. `:auto` resolves per filter: no measurement error for the Kalman and inversion filters, and $(DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION) times each observable's sample standard deviation for the particle filters, which are degenerate without it. Measurement error is supported by the Kalman filter and required by the particle filters; it is not available for the inversion filter, which reproduces the observables exactly. +- `measurement_error_covariance` [Default: `nothing`, Type: `Union{Nothing,AbstractMatrix{<:Real}}`]: full measurement-error covariance matrix (one row/column per observable), for correlated measurement error. Supersedes `measurement_error_std` when supplied. Must be symmetric positive definite. The Kalman filter supports an arbitrary covariance; the particle filters currently require it to be diagonal — correlated measurement error can instead be modelled structurally by adding measurement-error processes to the observation equations. - `n_particles` [Default: `$(DEFAULT_N_PARTICLES)`, Type: `Int`]: number of particles used by the particle filters. More particles reduce the Monte-Carlo variance of the likelihood at roughly linear cost. - `particle_resampling` [Default: `:$(DEFAULT_PARTICLE_RESAMPLING)`, Type: `Symbol`]: resampling scheme. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`. - `particle_resampling_threshold` [Default: `$(DEFAULT_PARTICLE_RESAMPLING_THRESHOLD)`, Type: `Real`]: resample whenever the effective sample size falls below `particle_resampling_threshold * n_particles`. @@ -4436,6 +4437,7 @@ function get_loglikelihood(𝓂::ℳ, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error_covariance::Union{Nothing,AbstractMatrix{<:Real}} = nothing, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -4465,6 +4467,7 @@ function get_loglikelihood(𝓂::ℳ, initial_covariance = initial_covariance, filter_algorithm = filter_algorithm, measurement_error_std = measurement_error_std, + measurement_error_covariance = measurement_error_covariance, n_particles = n_particles, particle_resampling = particle_resampling, particle_resampling_threshold = particle_resampling_threshold, @@ -4496,6 +4499,7 @@ function get_loglikelihood(𝓂::ℳ, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error_covariance::Union{Nothing,AbstractMatrix{<:Real}} = nothing, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -4650,7 +4654,22 @@ function get_loglikelihood(𝓂::ℳ, @assert !(resolved_measurement_error_std isa Symbol) "`measurement_error_std` must be `:auto`, a scalar, or a per-observable vector; got `:$(resolved_measurement_error_std)`." - measurement_error_variances = build_filter_measurement_error_variances(resolved_measurement_error_std, size(data_in_deviations, 1)) + measurement_error_variances = if measurement_error_covariance !== nothing + # A full covariance matrix supersedes the per-observable standard deviations. + n_obs_me = size(data_in_deviations, 1) + @assert size(measurement_error_covariance) == (n_obs_me, n_obs_me) "`measurement_error_covariance` must be a square matrix with one row/column per observable ($(n_obs_me)); got $(size(measurement_error_covariance))." + H = Matrix{Float64}(measurement_error_covariance) + @assert all(isfinite, H) "`measurement_error_covariance` must contain only finite entries." + @assert isapprox(H, H', rtol = 1e-10) "`measurement_error_covariance` must be symmetric." + H = (H + H') / 2 # symmetrise away round-off + @assert ℒ.isposdef(H) "`measurement_error_covariance` must be positive definite." + if is_particle_filter && !ℒ.isdiag(H) + error("The particle filters currently require a diagonal measurement-error covariance; `measurement_error_covariance` is off-diagonal. Either use `filter = :kalman` (which supports a full covariance), or model the correlated measurement error structurally by adding measurement-error processes to the observation equations, which turns it back into an independent (diagonal) one.") + end + H + else + build_filter_measurement_error_variances(resolved_measurement_error_std, size(data_in_deviations, 1)) + end if filter == :inversion && measurement_error_variances !== nothing error("`measurement_error_std` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`).") @@ -4677,7 +4696,10 @@ function get_loglikelihood(𝓂::ℳ, constants_obj, state, 𝓂, - measurement_error_variances, + # the particle filters take the per-observable variances; + # a (necessarily diagonal) covariance matrix is reduced here + measurement_error_variances isa AbstractMatrix ? + collect(ℒ.diag(measurement_error_variances)) : measurement_error_variances, obs_idx_per_t, has_missing; n_particles = n_particles, diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 34dd2d061..27680a2fc 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -139,6 +139,29 @@ threw(f) = try; f(); false; catch; true; end @test abs(kal_m - pf_m) < 3.0 end + @testset "Full measurement-error covariance" begin + Hdiag = [me^2 0.0; 0.0 me^2] + # a diagonal covariance reproduces the equivalent per-observable stds + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = Hdiag) ≈ + get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + # the Kalman filter accepts genuinely correlated measurement error + Hfull = [me^2 0.6me^2; 0.6me^2 me^2] + llf = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = Hfull) + @test isfinite(llf) + @test llf != get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + # the particle filters take a diagonal covariance but reject an off-diagonal one + @test isfinite(get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_covariance = Hdiag, n_particles = 2_000, + particle_rng = Random.Xoshiro(1))) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_covariance = Hfull, n_particles = 500, + particle_rng = Random.Xoshiro(1))) + # a covariance must be symmetric, positive definite and correctly sized + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = [1.0 2.0; 0.0 1.0])) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = -Hdiag)) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = fill(me^2, 1, 1))) + end + @testset "Filter selection and automatic measurement error" begin # `:particle` is an alias for the bootstrap filter: same RNG ⇒ same value @test get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, From 066b5785670c2962aa86b3138d2f5121bdb15196 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 15:52:28 +0200 Subject: [PATCH 07/24] Add a Filters documentation page; cite primary sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `docs/src/filters.md`, registered in the page tree, covering all filter options in one place: the state-space setup they share, a comparison table and a short decision rule, then the maths and assumptions of each filter — the Kalman recursion and why it is exact and differentiable, what the inversion filter inverts and why that rules out measurement error and requires as many shocks as observables, and the predict/weight/resample skeleton of the particle filters. Explains the properties that matter in practice: why the particle likelihood is unbiased but its logarithm is biased downward by about Var/2 (so it sits below the Kalman value and converges from below), why measurement error is required at all, what each particle variant buys, and how the resampling schemes trade variance against cost. Adds a section on how the filters map into one another — the particle filters converge to the Kalman likelihood on a linear model, the inversion and particle filters make opposite measurement-error assumptions, and correlated measurement error can be moved into the model's observation equations so that every filter can handle it. Replace the Dynare reference in the estimation tutorial with the primary sources (Gordon, Salmond & Smith 1993; Fernández-Villaverde & Rubio-Ramírez 2007; Pitt & Shephard 1999; Herbst & Schorfheide 2019) and update it to the new filter names. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/make.jl | 1 + docs/src/filters.md | 180 +++++++++++++++++++++++++++++++ docs/src/tutorials/estimation.md | 19 ++-- 3 files changed, 191 insertions(+), 9 deletions(-) create mode 100644 docs/src/filters.md diff --git a/docs/make.jl b/docs/make.jl index 8486ccc31..62d2fceac 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -48,6 +48,7 @@ makedocs( "Variance Decomposition" => "plot_conditional_variance_decomposition.md", "Model Estimates" => "plot_model_estimates.md", ], + "Filters" => "filters.md", "Steady State" => "steady_state.md", "Shapley decompositions (higher order)" => "shapley_decompositions.md", "Speed Benchmarks" => "speed.md", diff --git a/docs/src/filters.md b/docs/src/filters.md new file mode 100644 index 000000000..ca01e5175 --- /dev/null +++ b/docs/src/filters.md @@ -0,0 +1,180 @@ +# Filters + +Every likelihood in `MacroModelling.jl` answers the same question: given the model, the parameters, and the data, how plausible is what we observed? They differ in *how they deal with the states we cannot see*. + +The model is a state-space system. The solution gives a transition, + +```math +x_t = g(x_{t-1}, \varepsilon_t), \qquad \varepsilon_t \sim N(0, I), +``` + +where ``g`` is linear at first order and nonlinear at higher orders, and an observation equation that picks the observed variables out of the state and (optionally) adds measurement error, + +```math +y_t = x_t[\text{observables}] + \eta_t, \qquad \eta_t \sim N(0, H). +``` + +The likelihood we want is ``p(y_{1:T}) = \prod_t p(y_t \mid y_{1:t-1})``. Each filter is a different way of tracking the distribution of ``x_t`` given the data so far, which is what turns that product into something computable. + +Select a filter with the `filter` keyword: + +```julia +get_loglikelihood(model, data, parameters; filter = :kalman) +``` + +## Choosing a filter + +| filter | models | likelihood | differentiable | measurement error | smoothing | relative cost | +|---|---|---|---|---|---|---| +| `:kalman` | linear (`:first_order`) | exact | yes | optional (incl. correlated) | yes | 1× | +| `:inversion` | linear and nonlinear | exact given the shocks | yes | not available | no | ~1–10× | +| `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~10³× | +| `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~2× bootstrap | +| `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~5–10× bootstrap | + +A short decision rule: + +- **Linear model?** Use `:kalman`. It is exact, fast and differentiable, so gradient-based samplers (NUTS/HMC) work. There is no reason to use anything else. +- **Nonlinear model, as many shocks as observables, no measurement error?** Use `:inversion` (the default at higher order). It is exact and differentiable. +- **Nonlinear model with measurement error, or fewer shocks than observables?** Use a particle filter. Start with `:tempered_particle` if the observation is informative (small measurement error, many observables), otherwise `:bootstrap_particle`. +- Particle-filter likelihoods are noisy and non-differentiable: pair them with gradient-free samplers such as slice sampling (Pigeons.jl) or nested sampling. + +By default the package picks `:kalman` for `:first_order` and `:inversion` for the nonlinear algorithms. + +## The Kalman filter + +For a linear model with Gaussian shocks the filtering distribution stays Gaussian forever, so tracking it only requires tracking a mean and a covariance. The recursion alternates prediction and update: + +```math +\begin{aligned} +v_t &= y_t - C u_t, & F_t &= C P_t C' + H,\\ +u_{t+1} &= A (u_t + K_t v_t), & P_{t+1} &= A (P_t - K_t C P_t) A' + BB', +\end{aligned} +``` + +with the Kalman gain ``K_t = P_t C' F_t^{-1}``. Each period contributes + +```math +\log p(y_t \mid y_{1:t-1}) = -\tfrac{1}{2}\left( d\log 2\pi + \log\det F_t + v_t' F_t^{-1} v_t \right). +``` + +This is exact — no approximation beyond the linearity of the model itself — and every step is a smooth function of the parameters, which is why the Kalman likelihood is differentiable and works with NUTS. + +`initial_covariance` sets ``P_1``: `:theoretical` solves the Lyapunov equation ``P = APA' + BB'`` for the ergodic covariance, `:diagonal` starts diffuse (10 on the diagonal), or supply your own matrix. Missing observations are handled by shrinking the update to the observed rows in that period; a fully unobserved period becomes a pure prediction step. + +The Kalman filter is also the only filter that supports **smoothing** (`smooth = true`), i.e. estimates of ``x_t`` given the *whole* sample rather than only the past. `get_model_estimates`, `get_estimated_shocks` and the estimate plots use the Durbin–Koopman smoother. + +**References:** Kalman (1960); Durbin & Koopman (2012), *Time Series Analysis by State Space Methods*. + +## The inversion filter + +The inversion filter takes a different route: instead of integrating the shocks out, it asks *which shocks would have produced exactly this data?* Given the state ``x_{t-1}`` it solves + +```math +y_t = g(x_{t-1}, \varepsilon_t)[\text{observables}] +``` + +for ``\varepsilon_t``. At first order this is a linear solve; at higher order it is a small Newton problem per period (`filter_algorithm = :LagrangeNewton`). The recovered shocks are then scored under their own standard normal prior, with a Jacobian term for the change of variables: + +```math +\log p(y_{1:T}) = -\tfrac{1}{2}\sum_t \left( \varepsilon_t'\varepsilon_t + d\log 2\pi \right) - \sum_t \log\left|\det J_t\right|. +``` + +Because it inverts the observation equation exactly, the inversion filter: + +- needs **at least as many shocks as observables** (otherwise the system is not invertible), and +- admits **no measurement error** — measurement error would make the mapping stochastic, and there would be nothing left to invert uniquely. + +In exchange it is exact for nonlinear models, deterministic, and differentiable, which makes it the default at higher order. It provides no smoothed estimates. + +**References:** Fair & Taylor (1983); Cuba-Borda, Guerrieri, Iacoviello & Zhong (2019). + +## Particle filters + +When the model is nonlinear *and* there is measurement error, the filtering distribution is no longer Gaussian and no longer invertible. Particle filters represent it by a cloud of ``N`` weighted draws ("particles") and update the cloud each period. They are the general-purpose fallback: they work for any transition, any number of shocks, and any measurement error. + +All variants share the same skeleton: + +1. **Predict** — push every particle through the model's transition with a freshly drawn shock. +2. **Weight** — score each particle by ``p(y_t \mid x_t)``. The weighted average of those scores estimates the period's likelihood contribution: + ```math + \widehat{p}(y_t \mid y_{1:t-1}) = \sum_p W_{t-1}^p \, p(y_t \mid x_t^p). + ``` +3. **Resample** — when the weights get too uneven, replace the weighted cloud by an equally weighted one, so particles are spent where the probability mass is. + +The crucial property is that ``\widehat{p}(y_{1:T})`` is an **unbiased** estimator of the true likelihood, for any ``N``. That is what makes particle filters usable inside a sampler (pseudo-marginal MCMC targets the exact posterior despite the noise). Note the consequence for the *log* likelihood: by Jensen's inequality ``E[\log \widehat{p}] < \log p``, with a downward bias of roughly ``\mathrm{Var}(\log\widehat p)/2``. So a particle log-likelihood is systematically a little *below* the Kalman value on a linear model, and the gap shrinks as ``N`` grows — this is the expected behaviour, not a bug. + +### Why measurement error is required + +Without measurement error the observation equation is a deterministic function of the state. A particle would have to reproduce ``y_t`` *exactly* to get non-zero weight, which happens with probability zero — every weight collapses to zero and the filter dies. Measurement error smears the observation density and gives particles something to score against. + +`measurement_error_std = :auto` (the default) therefore resolves to 10% of each observable's sample standard deviation for the particle filters, and to *no* measurement error for the Kalman and inversion filters. For serious work set it explicitly or estimate it: the level of the likelihood depends on it, so likelihoods computed under different measurement errors are not comparable. + +### Bootstrap (`:bootstrap_particle`) + +The plain sequential-importance-resampling filter: propose from the model's own transition (the "prior"), weight by the observation density. Simple, robust, and the cheapest per particle. + +Its weakness is that it proposes blindly — it draws shocks without looking at ``y_t``, so when the observation is very informative most particles land in implausible places and are discarded. This gets worse as the number of observables grows (the weights concentrate exponentially in the observation dimension), which is why a 7-observable model needs far more particles than a 2-observable one at the same measurement error. + +**References:** Gordon, Salmond & Smith (1993); Fernández-Villaverde & Rubio-Ramírez (2007) for the DSGE application. + +### Auxiliary (`:auxiliary_particle`) + +Peeks at the observation *before* deciding which particles to propagate. Each ancestor gets a cheap preview of how plausible its children will look (the measurement density at its zero-shock transition mean, inflated by the shock-induced predictive variance), and ancestors are resampled in proportion to weight × preview. Only then are shocks drawn. + +Selecting on a preview biases the cloud, so the second stage divides the preview back out — the child's weight is the true density divided by the preview used to pick its parent. The preview cancels exactly, so the estimator stays unbiased regardless of how good the preview is; a poor preview costs efficiency, never correctness. + +Helps most when the signal is informative *and* the one-step-ahead state is well predicted by its mean. It costs roughly one extra transition evaluation per particle per period. + +**Reference:** Pitt & Shephard (1999). + +### Tempered (`:tempered_particle`) + +Instead of confronting the particles with the full observation in one step, the tempered filter introduces the information gradually. Within each period it walks a bridging sequence ``0 = \phi_0 < \phi_1 < \dots < \phi_N = 1``, at each stage using an inflated measurement covariance ``H/\phi``: early stages are nearly uninformative and easy to match, later stages sharpen towards the true density. At every stage the particles are reweighted by the incremental density, resampled, and then **mutated** by a few random-walk Metropolis steps on their shocks that target the stage's tempered posterior. The stage contributions telescope back to the period's likelihood. + +The mutation is what makes this powerful: it *moves* particles towards the data rather than merely reweighting the ones that happen to be well placed, so the cloud does not degenerate even when the observation is sharp. The bridging schedule is chosen adaptively to hit a target inefficiency ratio (`tempering_target_ratio`), so hard periods automatically get more stages than easy ones. + +In practice this buys a large variance reduction per particle — several times lower standard deviation than the bootstrap filter at the same ``N`` — at several times the cost per particle. It is the right default when the bootstrap filter degenerates. + +**Reference:** Herbst & Schorfheide (2019), *Tempered Particle Filtering*; see also Herbst & Schorfheide (2015), *Bayesian Estimation of DSGE Models*. + +### Resampling schemes + +`particle_resampling` selects how survivors are drawn. All schemes are unbiased, so the choice affects only the extra Monte-Carlo noise that resampling itself injects, ordered here from least to most: + +- `:systematic` (default) — one uniform draw, then ``N`` equally spaced points through the cumulative weights. Lowest variance and cheapest. +- `:stratified` — one independent uniform per stratum of width ``1/N``. Nearly as good, with independent draws. +- `:residual` — assign ``\lfloor N W_i \rfloor`` copies deterministically, draw only the remainder. +- `:multinomial` — ``N`` independent draws. The textbook scheme, and the noisiest. + +Resampling only happens when the effective sample size ``1/\sum_i W_i^2`` falls below `particle_resampling_threshold * n_particles` (default 0.5), which avoids paying the noise in periods that do not need it. + +**References:** Kitagawa (1996); Douc & Cappé (2005). + +## How the filters relate + +- **Particle → Kalman.** On a *linear* model with Gaussian shocks, the particle filters estimate exactly the quantity the Kalman filter computes in closed form. As ``N \to \infty`` the particle log-likelihood converges to the Kalman log-likelihood (from below, by the Jensen bias above). This is the sharpest correctness check available and is exactly what the package's tests do, on both a small RBC model and Smets-Wouters (2007). +- **Kalman → particle.** The Kalman filter is the special case where the transition and observation are linear and the noise Gaussian, so the "cloud" is fully described by its first two moments. +- **Inversion → particle.** Both handle nonlinear models, but they make opposite trades. The inversion filter assumes measurement error is *zero* and recovers the shocks exactly; the particle filter assumes measurement error is *positive* and integrates the shocks out. As the measurement error goes to zero the particle filter degenerates towards the inversion filter's problem — and this is precisely where it needs the most particles. +- **Correlated measurement error.** The Kalman filter accepts an arbitrary `measurement_error_covariance`. The particle filters require it to be diagonal, but this is not a real restriction: correlated measurement error can be written into the model itself as measurement-error processes in the observation equations, which moves the correlation into the state transition and makes ``H`` diagonal again. That reformulation works for every filter. + +## Reproducibility and cost + +Particle-filter likelihoods are random. Pass a seeded generator via `particle_rng` to make an evaluation reproducible: + +```julia +import Random +get_loglikelihood(model, data, parameters; + filter = :tempered_particle, + algorithm = :pruned_second_order, + n_particles = 20_000, + measurement_error_std = 1e-3, + particle_rng = Random.Xoshiro(1)) +``` + +Two practical notes when using them inside a sampler: + +- Use the *same* seed across parameter draws only if you want a "common random numbers" scheme; otherwise let the sampler see fresh noise, which is what pseudo-marginal correctness assumes. +- Set `on_failure_loglikelihood` to a large finite negative number so an occasional failed evaluation does not abort the chain. + +Cost scales roughly linearly in `n_particles` and in the sample length, and the number of particles needed grows quickly with the number of observables. The default of 10,000 particles keeps a Smets-Wouters-sized problem accurate to a couple of log-likelihood points; raise it when the estimates look noisy. diff --git a/docs/src/tutorials/estimation.md b/docs/src/tutorials/estimation.md index 56bca2e3a..0cf9a4766 100644 --- a/docs/src/tutorials/estimation.md +++ b/docs/src/tutorials/estimation.md @@ -257,13 +257,15 @@ shows the variables of the model (blue), data (red), the shock decomposition for ## Nonlinear estimation with the particle filter -For genuinely nonlinear models the structural shocks can be integrated out by Monte Carlo using the particle filter (`filter = :particle`). It works for every perturbation order (`:first_order` through `:pruned_third_order`), requires measurement error on the observables (`measurement_error_std`), and is selected via `particle_filter_algorithm`: +For genuinely nonlinear models the structural shocks can be integrated out by Monte Carlo using a particle filter. These work for every perturbation order (`:first_order` through `:pruned_third_order`) and require measurement error on the observables (`measurement_error_std`, which defaults to a small data-driven value). Each variant is its own `filter` value: -- `:bootstrap` — the sequential-importance-resampling filter (as in Dynare), -- `:auxiliary` — the Pitt–Shephard auxiliary particle filter, -- `:tempered` — the Herbst–Schorfheide tempered particle filter, which yields a much lower-variance likelihood estimate for the same number of particles. +- `:bootstrap_particle` — the sequential-importance-resampling filter of Gordon, Salmond & Smith (1993), applied to DSGE models by Fernández-Villaverde & Rubio-Ramírez (2007), +- `:auxiliary_particle` — the auxiliary particle filter of Pitt & Shephard (1999), which uses a look-ahead proposal, +- `:tempered_particle` — the tempered particle filter of Herbst & Schorfheide (2019), which yields a much lower-variance likelihood estimate for the same number of particles. -The particle-filter likelihood is a stochastic estimator and is **not** differentiable (resampling is discontinuous), so it must be used with gradient-free samplers such as the slice sampler in `Pigeons.jl` or nested sampling. Pass a seeded `rng` for reproducibility. +The particle-filter likelihood is a stochastic estimator and is **not** differentiable (resampling is discontinuous), so it must be used with gradient-free samplers such as the slice sampler in `Pigeons.jl` or nested sampling. Pass a seeded `particle_rng` for reproducibility. + +See the [Filters](../filters.md) page for the full comparison, the maths behind each filter, and guidance on choosing between them. ```julia using MacroModelling @@ -275,11 +277,10 @@ Turing.@model function FS2000_particle(data, m) parameters ~ Turing.product_distribution(prior_distributions) Turing.@addlogprob! get_loglikelihood(m, data, parameters; algorithm = :pruned_second_order, - filter = :particle, - particle_filter_algorithm = :tempered, + filter = :tempered_particle, n_particles = 5000, measurement_error_std = 1e-3, - rng = Random.Xoshiro(1), + particle_rng = Random.Xoshiro(1), on_failure_loglikelihood = -1e12) end @@ -287,4 +288,4 @@ pt = Pigeons.pigeons(target = Pigeons.TuringLogPotential(FS2000_particle(data, F n_rounds = 8) ``` -Because the likelihood is noisy, set `on_failure_loglikelihood` to a finite value (as above) so the sampler tolerates occasional failed evaluations, and prefer a larger `n_particles` (and the `:tempered` variant) to reduce the estimate's variance. +Because the likelihood is noisy, set `on_failure_loglikelihood` to a finite value (as above) so the sampler tolerates occasional failed evaluations, and prefer a larger `n_particles` (and the `:tempered_particle` filter) to reduce the estimate's variance. From cf56f77e04fcd319c87d86496cef6fb67d7e6390 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 16:10:01 +0200 Subject: [PATCH 08/24] Extend particle filtering to the model-estimate and plotting entry points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `filter_data_with_model` method for the particle filters, so `get_model_estimates`, `get_estimated_variables`, `get_estimated_shocks` and the estimate plots work with `filter = :bootstrap_particle` and friends. It returns the filtered moments of the particle cloud: the weighted mean of the states, the weighted spread as standard deviations, and the weighted mean of the drawn shocks. All three particle variants target the same filtering distribution — they differ only in how efficiently they estimate the likelihood — so the moments come from the standard predict/weight/resample recursion whichever variant is selected. Two honest limitations, both signalled: these are filtered rather than smoothed estimates (a particle smoother is a different algorithm), and a linear shock decomposition does not exist for a nonlinear filter, so `decomposition` is returned as zeros with an informational message. Thread the particle options (`measurement_error_std`, `n_particles`, `particle_resampling`, `particle_resampling_threshold`, `particle_initial_state_scaling`, `particle_rng`) through those entry points and the two plotting functions that take a `filter`. Tested against the Kalman estimates on a linear model, where the filtered particle paths track the smoothed Kalman paths closely, plus nonlinear orders and the combined estimates entry point. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- ext/StatsPlotsExt.jl | 33 ++++++- src/filter/particle.jl | 179 +++++++++++++++++++++++++++++++++++ src/get_functions.jl | 47 ++++++++- test/test_particle_filter.jl | 24 +++++ 4 files changed, 277 insertions(+), 6 deletions(-) diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index 702cc0567..b13dcc0d8 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -5,6 +5,7 @@ using MacroModelling import MacroModelling: ParameterType, ℳ, Symbol_input, String_input, Tolerances, NsssTolerances, SolverTolerances, merge_calculation_options, MODEL®, DATA®, PARAMETERS®, ALGORITHM®, FILTER®, VARIABLES®, SMOOTH®, SHOW_PLOTS®, SAVE_PLOTS®, SAVE_PLOTS_NAME®, SAVE_PLOTS_FORMAT®, SAVE_PLOTS_PATH®, PLOTS_PER_PAGE®, MAX_ELEMENTS_PER_LEGENDS_ROW®, EXTRA_LEGEND_SPACE®, PLOT_ATTRIBUTES®, QME®, SYLVESTER®, LYAPUNOV®, TOLERANCES®, VERBOSE®, DATA_IN_LEVELS®, PERIODS®, SHOCKS®, SHOCK_SIZE®, NEGATIVE_SHOCK®, GENERALISED_IRF®, GENERALISED_IRF_WARMUP_ITERATIONS®, CONDITIONS_IN_LEVELS®, GENERALISED_IRF_DRAWS®, INITIAL_STATE®, IGNORE_OBC®, CONDITIONS®, SHOCK_CONDITIONS®, LEVELS®, LABEL®, RENAME_DICTIONARY®, STEADY_STATE_FUNCTION®, parse_shocks_input_to_index, parse_variables_input_to_index, replace_indices, replace_indices_special, filter_data_with_model, get_relevant_steady_states, replace_indices_in_symbol, parse_algorithm_to_state_update, girf, decompose_name, obc_objective_optim_fun, obc_constraint_optim_fun, compute_irf_responses, process_ignore_obc_flag, adjust_generalised_irf_flag, process_shocks_input, normalize_filtering_options, normalize_presample_periods, trim_informative_sample, adjust_initial_state, SteadyStateFunctionType import MacroModelling: DEFAULT_CACHING, DEFAULT_USE_WORKSPACES, DEFAULT_ALGORITHM, DEFAULT_FILTER_SELECTOR, DEFAULT_WARMUP_ITERATIONS, DEFAULT_VARIABLES_EXCLUDING_OBC, DEFAULT_SHOCK_SELECTION, DEFAULT_PRESAMPLE_PERIODS, DEFAULT_DATA_IN_LEVELS, DEFAULT_SHOCK_DECOMPOSITION_SELECTOR, DEFAULT_SMOOTH_SELECTOR, DEFAULT_LABEL, DEFAULT_SHOW_PLOTS, DEFAULT_SAVE_PLOTS, DEFAULT_SAVE_PLOTS_FORMAT, DEFAULT_SAVE_PLOTS_PATH, DEFAULT_PLOTS_PER_PAGE_SMALL, DEFAULT_TRANSPARENCY, DEFAULT_MAX_ELEMENTS_PER_LEGEND_ROW, DEFAULT_EXTRA_LEGEND_SPACE, DEFAULT_VERBOSE, DEFAULT_QME_ALGORITHM, DEFAULT_SYLVESTER_SELECTOR, DEFAULT_SYLVESTER_THRESHOLD, DEFAULT_LARGE_SYLVESTER_ALGORITHM, DEFAULT_SYLVESTER_ALGORITHM, DEFAULT_LYAPUNOV_ALGORITHM, DEFAULT_PLOT_ATTRIBUTES, DEFAULT_ARGS_AND_KWARGS_NAMES, DEFAULT_PLOTS_PER_PAGE_LARGE, DEFAULT_SHOCKS_EXCLUDING_OBC, DEFAULT_VARIABLES_EXCLUDING_AUX_AND_OBC, DEFAULT_PERIODS, DEFAULT_SHOCK_SIZE, DEFAULT_NEGATIVE_SHOCK, DEFAULT_GENERALISED_IRF, DEFAULT_GENERALISED_IRF_WARMUP, DEFAULT_GENERALISED_IRF_DRAWS, DEFAULT_INITIAL_STATE, DEFAULT_IGNORE_OBC, DEFAULT_PLOT_TYPE, DEFAULT_CONDITIONS_IN_LEVELS, DEFAULT_SIGMA_RANGE, DEFAULT_FONT_SIZE, DEFAULT_VARIABLE_SELECTION, DEFAULT_FORECAST_PERIODS import DocStringExtensions: FIELDS, SIGNATURES, TYPEDEF, TYPEDSIGNATURES, TYPEDFIELDS +import Random import LaTeXStrings const irf_active_plot_container = Dict[] @@ -705,7 +706,19 @@ function plot_model_estimates(𝓂::ℳ, parameters::ParameterType = nothing, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, - filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, + particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, + particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -840,6 +853,12 @@ function plot_model_estimates(𝓂::ℳ, x_axis = x_axis[periods] extra_kw = mc ? (; marginal_contribution = true) : NamedTuple() + if filter ∈ MacroModelling.PARTICLE_FILTERS + extra_kw = merge(extra_kw, (; measurement_error_std, n_particles, particle_resampling, + particle_resampling_threshold, particle_initial_state_scaling, + particle_rng)) + end + variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; extra_kw...) if is_pruned @@ -1360,6 +1379,12 @@ function plot_model_estimates!(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, + particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -1482,7 +1507,11 @@ function plot_model_estimates!(𝓂::ℳ, x_axis = x_axis[periods] - variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts) + particle_kw = filter ∈ MacroModelling.PARTICLE_FILTERS ? + (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + particle_initial_state_scaling, particle_rng) : NamedTuple() + + variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; particle_kw...) if pruning decomposition[:,1:(end - 2 - pruning),:] .+= SSS_delta diff --git a/src/filter/particle.jl b/src/filter/particle.jl index a83a70a53..bbd2a80f9 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -1605,4 +1605,183 @@ function run_particle_filter(::Val{:first_order}, return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) end + +# ── Filtered estimates from a particle filter ──────────────────────────────── +# +# `filter_data_with_model` is the entry point behind `get_model_estimates`, +# `get_estimated_variables`, `get_estimated_shocks` and the estimate plots. For +# the particle filters it returns the *filtered* moments of the particle cloud: +# +# variables E[xₜ | y₁..ₜ] — weighted mean of the cloud +# standard_deviations sd(xₜ | y₁..ₜ) — weighted spread of the cloud +# shocks E[εₜ | y₁..ₜ] — weighted mean of the drawn shocks +# +# All three particle variants target the same filtering distribution p(xₜ|y₁..ₜ) +# — they differ only in how efficiently they estimate the *likelihood* — so the +# moments below are produced by the standard predict/weight/resample recursion +# whichever particle filter was selected. +# +# Two caveats relative to the Kalman path: these are filtered, not smoothed, +# estimates (a particle smoother is a different algorithm), and a linear shock +# decomposition does not exist for a nonlinear filter, so `decomposition` is +# returned as zeros. +@unstable function filter_data_with_model(𝓂::ℳ, + data_in_deviations::KeyedArray{Float64}, + ::Val{algo}, + ::Union{Val{:bootstrap_particle},Val{:auxiliary_particle},Val{:tempered_particle}}; + warmup_iterations::Int = 0, + opts::CalculationOptions = merge_calculation_options(), + smooth::Bool = true, + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical) where {algo} + + obs_axis = collect(axiskeys(data_in_deviations, 1)) + observables = obs_axis isa String_input ? obs_axis .|> Meta.parse .|> replace_indices : obs_axis + + constants = 𝓂.constants + T = constants.post_model_macro + nVars = T.nVars + nExo = T.nExo + past_idx = T.past_not_future_and_mixed_idx + + ss_names = constants.post_complete_parameters.SS_and_pars_names + observables_index = convert(Vector{Int}, indexin(observables, ss_names)) + + dat = missing_data_to_nan(collect(data_in_deviations)) + obs_idx_per_t, has_missing = build_obs_index(dat) + nT = size(dat, 2) + + # measurement error: same `:auto` convention as the likelihood path + me_std = measurement_error_std === :auto ? + DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION .* vec(sqrt.(sum(abs2, dat .- sum(dat, dims = 2) ./ nT, dims = 2) ./ max(nT - 1, 1))) : + measurement_error_std + me_var = me_std isa AbstractVector ? collect(float.(me_std)) .^ 2 : fill(float(me_std)^2, length(observables)) + @inbounds for i in eachindex(me_var) + if !(isfinite(me_var[i])) || me_var[i] <= 0 + me_var[i] = (DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION)^2 + end + end + + # solution matrices and the initial state, exactly as the likelihood path builds them + _, _, 𝐒, state, solved = get_relevant_steady_state_and_state_update(Val(algo), 𝓂.parameter_values, 𝓂, opts = opts) + @assert solved "Could not solve the model for `algorithm = $(algo)`; cannot run the particle filter." + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(particle_initial_state_scaling)) + + log2pi = log(2π) + rng = particle_rng + + # storage for the filtered moments + variables = zeros(nVars, nT) + stds = zeros(nVars, nT) + shocks_out = zeros(nExo, nT) + + W = fill(1.0 / n_particles, n_particles) + logdens = Vector{Float64}(undef, n_particles) + idx = Vector{Int}(undef, n_particles) + bins = Vector{Float64}(undef, n_particles) + shocks = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] + full_buf = Vector{Float64}(undef, nVars) + + if algo == :first_order + tr = build_linear_particle_transition(𝐒, T) + mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) + parts = [mean0 .+ L * randn(rng, nVars) for _ in 1:n_particles] + parts2 = [zeros(Float64, nVars) for _ in 1:n_particles] + propagate! = (out, prev, sh) -> linear_propagate_estimates!(out, tr, prev, sh) + else + 𝐒f = [Matrix{Float64}(S) for S in 𝐒] + scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) + parts = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars) + parts2 = [zeros_like_particle(parts[1]) for _ in 1:n_particles] + propagate! = (out, prev, sh) -> higher_propagate!(Val(algo), out, prev, sh, past_idx, 𝐒f, scr) + end + + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view dat[:, t] + + @inbounds for p in 1:n_particles + Random.randn!(rng, shocks[p]) + propagate!(parts2[p], parts[p], shocks[p]) + end + parts, parts2 = parts2, parts + + if isempty(rows) + # nothing observed: the weights are unchanged, the cloud just predicts + fill!(logdens, 0.0) + else + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) + end + m = maximum(logdens) + if isfinite(m) + s = 0.0 + @inbounds for p in 1:n_particles + s += W[p] * exp(logdens[p] - m) + end + if s > 0 && isfinite(s) + @inbounds for p in 1:n_particles + W[p] = W[p] * exp(logdens[p] - m) / s + end + end + end + end + + # filtered moments of the (weighted) cloud + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + w = W[p] + for i in 1:nVars + variables[i, t] += w * full[i] + end + for e in 1:nExo + shocks_out[e, t] += w * shocks[p][e] + end + end + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + w = W[p] + for i in 1:nVars + stds[i, t] += w * (full[i] - variables[i, t])^2 + end + end + @inbounds for i in 1:nVars + stds[i, t] = sqrt(max(stds[i, t], 0.0)) + end + + if effective_sample_size(W) < particle_resampling_threshold * n_particles + particle_resample_indices!(idx, bins, rng, W, particle_resampling) + @inbounds for j in 1:n_particles + copy_particle!(parts2[j], parts[idx[j]]) + end + parts, parts2 = parts2, parts + fill!(W, 1.0 / n_particles) + end + end + + @info "Shock decomposition is not defined for the particle filters (the state transition is nonlinear and the contributions do not add up); returning zeros. Use `filter = :kalman` for a shock decomposition." maxlog = 1 + + decomposition = zeros(nVars, nExo + 2, nT) + decomposition[:, end, :] .= variables + + return variables, shocks_out, stds, decomposition +end + +# out = A·prev[past] + B·shock for a single particle (estimates path; the +# likelihood path propagates the whole swarm with one gemm instead). +@inline function linear_propagate_estimates!(out::Vector{Float64}, tr::LinearParticleTransition, + prev::Vector{Float64}, shock::Vector{Float64}) + ℒ.mul!(out, tr.A, prev) + ℒ.mul!(out, tr.B, shock, 1.0, 1.0) + return out +end + end # @stable diff --git a/src/get_functions.jl b/src/get_functions.jl index a0386fd7e..b150fa967 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -297,6 +297,12 @@ And data, 4×2×40 Array{Float64, 3}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -339,6 +345,11 @@ And data, 4×2×40 Array{Float64, 3}: data_in_deviations = prepare_trimmed_data_in_deviations(data, 𝓂, NSSS; data_in_levels = data_in_levels) extra_kw = marginal_contribution ? (; marginal_contribution = true) : NamedTuple() + if filter ∈ PARTICLE_FILTERS + extra_kw = merge(extra_kw, (; measurement_error_std, n_particles, particle_resampling, + particle_resampling_threshold, particle_initial_state_scaling, + particle_rng)) + end ensure_name_display_constants!(𝓂) axis1 = 𝓂.constants.post_complete_parameters.var_axis exo_axis = 𝓂.constants.post_complete_parameters.exo_axis_with_subscript @@ -441,7 +452,13 @@ And data, 1×40 Matrix{Float64}: parameters::ParameterType = nothing, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, - filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -485,10 +502,15 @@ And data, 1×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Shocks = axis1, Periods = 1:0) end + particle_kw = filter ∈ PARTICLE_FILTERS ? + (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + particle_initial_state_scaling, particle_rng) : NamedTuple() + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, opts = opts, - smooth = smooth) + smooth = smooth; + particle_kw...) if !use_workspaces; 𝓂.workspaces = orig_ws; end @@ -567,7 +589,13 @@ And data, 4×40 Matrix{Float64}: parameters::ParameterType = nothing, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, - filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, @@ -612,10 +640,15 @@ And data, 4×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Variables = axis1, Periods = 1:0) end + particle_kw = filter ∈ PARTICLE_FILTERS ? + (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + particle_initial_state_scaling, particle_rng) : NamedTuple() + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, opts = opts, - smooth = smooth) + smooth = smooth; + particle_kw...) result = KeyedArray(levels ? variables .+ NSSS[1:length(𝓂.constants.post_model_macro.var)] : variables; Variables = axis1, Periods = 1:size(data_in_deviations,2)) @@ -698,6 +731,12 @@ And data, 5×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 27680a2fc..5770dff76 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -139,6 +139,30 @@ threw(f) = try; f(); false; catch; true; end @test abs(kal_m - pf_m) < 3.0 end + @testset "Filtered estimates from the particle filters" begin + # the filtered particle estimates should track the Kalman estimates closely + kal_v = get_estimated_variables(RBC_pf, data; filter = :kalman) + for pf_filter in (:bootstrap_particle, :tempered_particle) + v = get_estimated_variables(RBC_pf, data; filter = pf_filter, algorithm = :first_order, + measurement_error_std = me, n_particles = 20_000, + particle_rng = Random.Xoshiro(1)) + @test size(v) == size(kal_v) + @test all(isfinite, collect(v)) + @test maximum(abs, collect(v) .- collect(kal_v)) < 0.1 + end + s = get_estimated_shocks(RBC_pf, data; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error_std = me, n_particles = 10_000, + particle_rng = Random.Xoshiro(1)) + @test all(isfinite, collect(s)) + # nonlinear orders and the combined estimates entry point + @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :pruned_second_order, measurement_error_std = me, + n_particles = 5_000, particle_rng = Random.Xoshiro(1)))) + @test all(isfinite, collect(get_model_estimates(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :first_order, measurement_error_std = me, + n_particles = 5_000, particle_rng = Random.Xoshiro(1)))) + end + @testset "Full measurement-error covariance" begin Hdiag = [me^2 0.0; 0.0 me^2] # a diagonal covariance reproduces the equivalent per-observable stds From db085d2a3bb30c754be26e7d3a683475a8aea050 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 16:55:09 +0200 Subject: [PATCH 09/24] Add particle smoothing; finish the filter-argument cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement smoothing for the particle filters. `smooth = true` now returns E[xₜ | y₁..T] instead of E[xₜ | y₁..ₜ] for `get_model_estimates`, `get_estimated_variables`, `get_estimated_shocks` and the estimate plots. The method is fixed-interval smoothing along the filter's genealogy: every particle surviving at T carries the ancestral line that produced it, and those lines are draws from p(x₁..T | y₁..T), so averaging them with the terminal weights gives the smoothed moments. The textbook backward-kernel smoother is not usable here: it reweights by p(xₜ₊₁ | xₜ), which for a DSGE with fewer shocks than states is a Dirac on a lower-dimensional manifold and hence undefined. The known limitation (path degeneracy of the ancestral lines) and the memory cost are documented. `normalize_filtering_options` now permits smoothing for the Kalman and particle filters and only disables it for the inversion filter. Verified on a linear model: smoothing more than halves the distance of the particle estimates to the Durbin-Koopman smoother (0.020 vs 0.047 filtered). Finish the argument cleanup: the last `particle_filter_algorithm` mention (a stale header comment in particle.jl) is gone, so the filter is identified by a single symbol everywhere. Replace the remaining Dynare references in the filter sources with the primary works — Gordon, Salmond & Smith (1993) and Fernández-Villaverde & Rubio-Ramírez (2007) for the bootstrap filter. All six filter-taking entry points now expose the same particle options, and a `measurement_error_std` passed to an entry point that cannot use it (the Kalman smoother path) reports that it is ignored instead of dropping it silently. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 16 +++- src/MacroModelling.jl | 7 +- src/common_docstrings.jl | 2 +- src/filter/particle.jl | 166 ++++++++++++++++++++++++++++------- src/get_functions.jl | 15 ++++ test/test_particle_filter.jl | 29 ++++++ 6 files changed, 198 insertions(+), 37 deletions(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index ca01e5175..44e9d14b0 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -28,9 +28,9 @@ get_loglikelihood(model, data, parameters; filter = :kalman) |---|---|---|---|---|---|---| | `:kalman` | linear (`:first_order`) | exact | yes | optional (incl. correlated) | yes | 1× | | `:inversion` | linear and nonlinear | exact given the shocks | yes | not available | no | ~1–10× | -| `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~10³× | -| `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~2× bootstrap | -| `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required | no | ~5–10× bootstrap | +| `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~10³× | +| `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~2× bootstrap | +| `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~5–10× bootstrap | A short decision rule: @@ -62,7 +62,7 @@ This is exact — no approximation beyond the linearity of the model itself — `initial_covariance` sets ``P_1``: `:theoretical` solves the Lyapunov equation ``P = APA' + BB'`` for the ergodic covariance, `:diagonal` starts diffuse (10 on the diagonal), or supply your own matrix. Missing observations are handled by shrinking the update to the observed rows in that period; a fully unobserved period becomes a pure prediction step. -The Kalman filter is also the only filter that supports **smoothing** (`smooth = true`), i.e. estimates of ``x_t`` given the *whole* sample rather than only the past. `get_model_estimates`, `get_estimated_shocks` and the estimate plots use the Durbin–Koopman smoother. +The Kalman filter also supports **smoothing** (`smooth = true`), i.e. estimates of ``x_t`` given the *whole* sample rather than only the past. `get_model_estimates`, `get_estimated_shocks` and the estimate plots use the Durbin–Koopman smoother. The particle filters support smoothing too (see below); the inversion filter does not. **References:** Kalman (1960); Durbin & Koopman (2012), *Time Series Analysis by State Space Methods*. @@ -138,6 +138,14 @@ In practice this buys a large variance reduction per particle — several times **Reference:** Herbst & Schorfheide (2019), *Tempered Particle Filtering*; see also Herbst & Schorfheide (2015), *Bayesian Estimation of DSGE Models*. +### Smoothing + +`smooth = true` returns ``E[x_t \mid y_{1:T}]`` rather than ``E[x_t \mid y_{1:t}]``, i.e. estimates that use the *whole* sample. For the particle filters this is done by **fixed-interval smoothing along the filter's genealogy**: every particle surviving at ``T`` carries the ancestral line that produced it, and those lines are draws from the joint smoothing distribution ``p(x_{1:T} \mid y_{1:T})``, so averaging them with the terminal weights gives the smoothed moments directly. + +Why not the textbook backward-kernel smoother? That one reweights particles at ``t`` by the backward transition density ``p(x_{t+1} \mid x_t)``. In a DSGE that density is **singular**: with fewer shocks than states the transition maps ``x_t`` onto a lower-dimensional manifold, so ``p(x_{t+1} \mid x_t)`` is a Dirac on that manifold and the reweighting is undefined. The genealogy is what remains well defined. + +The known limitation is **path degeneracy**: ancestral lines coalesce as one goes back in time, so the earliest periods rest on fewer distinct trajectories than the particle count suggests. More particles push the coalescence point further back. Smoothing also stores the whole cloud, so its memory cost is about ``n_{vars} \times N \times T \times 8`` bytes — worth keeping in mind before raising `n_particles` for a long sample. + ### Resampling schemes `particle_resampling` selects how survivors are drawn. All schemes are unbiased, so the choice affects only the extra Monte-Carlo noise that resampling itself injects, ordered here from least to most: diff --git a/src/MacroModelling.jl b/src/MacroModelling.jl index 10bb78c22..aca360ccd 100644 --- a/src/MacroModelling.jl +++ b/src/MacroModelling.jl @@ -419,8 +419,11 @@ function normalize_filtering_options(filter::Symbol, is_particle = false end - if filter != :kalman && smooth - @info "Only the Kalman filter supports smoothing. Setting `smooth = false`." maxlog = maxlog + # Smoothing is available for the Kalman filter (Durbin-Koopman smoother) and + # for the particle filters (fixed-interval smoothing along the filter's + # genealogy). The inversion filter has no smoothing counterpart. + if filter == :inversion && smooth + @info "The inversion filter does not support smoothing. Setting `smooth = false`." maxlog = maxlog smooth = false end diff --git a/src/common_docstrings.jl b/src/common_docstrings.jl index a4e05d5da..58ad99994 100644 --- a/src/common_docstrings.jl +++ b/src/common_docstrings.jl @@ -13,7 +13,7 @@ const GENERALISED_IRF® = "`generalised_irf` [Default: `$(DEFAULT_GENERALISED_IR const GENERALISED_IRF_WARMUP_ITERATIONS® = "`generalised_irf_warmup_iterations` [Default: `$(DEFAULT_GENERALISED_IRF_WARMUP)`, Type: `Int`]: number of warm-up iterations used to draw the baseline paths in the generalised IRF simulation. Only applied when `generalised_irf = true`." const GENERALISED_IRF_DRAWS® = "`generalised_irf_draws` [Default: `$(DEFAULT_GENERALISED_IRF_DRAWS)`, Type: `Int`]: number of Monte Carlo draws used to compute the generalised IRF. Only applied when `generalised_irf = true`." const ALGORITHM® = "`algorithm` [Default: `$(DEFAULT_ALGORITHM)`, Type: `Symbol`]: algorithm to solve for the dynamics of the model. Available algorithms: `:first_order`, `:second_order`, `:pruned_second_order`, `:third_order`, `:pruned_third_order`" -const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error_std`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." +const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error_std`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. Smoothing (`smooth = true`) is available for the Kalman filter (Durbin-Koopman) and the particle filters (fixed-interval smoothing along the filter genealogy), but not for the inversion filter. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." const LEVELS® = "return levels or absolute deviations from the relevant steady state corresponding to the solution algorithm (e.g. stochastic steady state for higher order solution algorithms)." const CONDITIONS® = "`conditions` [Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}}`]: conditions for which to find the corresponding shocks. The input can have multiple formats, but for all types of entries, the first dimension corresponds to variables and the second dimension to the number of periods. The conditions can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the conditions are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as conditions. Note that conditioning variables to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input conditions is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as conditions and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of variables (of type `Symbol` or `String`) for which conditions are specified can be included and all other variables are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the conditions for the specified variables bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const SHOCK_CONDITIONS® = "`shocks` [Default: `nothing`, Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}, Nothing}`]: known values of shocks. This argument allows including certain shock values. By entering restrictions on the shocks in this way the problem to match the conditions on endogenous variables is restricted to the remaining free shocks in the respective period. The input can have multiple formats, but for all types of entries, the first dimension corresponds to shocks and the second dimension to the number of periods. `shocks` can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the shocks are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as certain shock values. Note that conditioning shocks to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input known shocks is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as known shocks and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of shocks (of type `Symbol` or `String`) for which values are specified can be included and all other shocks are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the values for the specified shocks bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." diff --git a/src/filter/particle.jl b/src/filter/particle.jl index bbd2a80f9..558d3e6ba 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -18,11 +18,13 @@ # solution matrices 𝐒), and the state transition is the perturbation solution's # `state_update` (first order through pruned third order). # -# Three variants are provided, selected by `particle_filter_algorithm`: -# :bootstrap — sequential-importance-resampling (as in Dynare 7's -# `sequential_importance_particle_filter.m`) -# :auxiliary — Pitt & Shephard (1999) auxiliary particle filter -# :tempered — Herbst & Schorfheide (2019) tempered particle filter +# Three variants are provided, each selected by its own `filter` value: +# :bootstrap_particle — sequential importance resampling, i.e. the bootstrap +# filter of Gordon, Salmond & Smith (1993), applied to +# DSGE models by Fernández-Villaverde & Rubio-Ramírez +# (2007) +# :auxiliary_particle — auxiliary particle filter of Pitt & Shephard (1999) +# :tempered_particle — tempered particle filter of Herbst & Schorfheide (2019) # # The particle filter is a stochastic likelihood estimator and is **not** # differentiable (resampling is discontinuous); it is intended for use with @@ -176,7 +178,8 @@ end # Covariance used to spread the initial particle cloud over the full state. # `:theoretical` (default) uses the first-order ergodic (unconditional) state # covariance Σ solving the discrete Lyapunov equation Σ = A Σ A' + B B' (built -# from the cached first-order solution, as in Dynare); `:diagonal` uses 10·I; an +# from the cached first-order solution, the usual choice for a stationary model); +# `:diagonal` uses 10·I; an # nVars×nVars matrix is used directly. function particle_initial_state_covariance(𝓂::ℳ, T, opts::CalculationOptions, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}}) @@ -1612,19 +1615,21 @@ end # `get_estimated_variables`, `get_estimated_shocks` and the estimate plots. For # the particle filters it returns the *filtered* moments of the particle cloud: # -# variables E[xₜ | y₁..ₜ] — weighted mean of the cloud -# standard_deviations sd(xₜ | y₁..ₜ) — weighted spread of the cloud -# shocks E[εₜ | y₁..ₜ] — weighted mean of the drawn shocks +# variables mean of the cloud (states) +# standard_deviations spread of the cloud (states) +# shocks mean of the drawn shocks +# +# With `smooth = false` these condition on the past only, E[xₜ | y₁..ₜ]. With +# `smooth = true` they condition on the whole sample, E[xₜ | y₁..T], obtained by +# the genealogy smoother in `smooth_particle_trajectories!` below. # # All three particle variants target the same filtering distribution p(xₜ|y₁..ₜ) # — they differ only in how efficiently they estimate the *likelihood* — so the # moments below are produced by the standard predict/weight/resample recursion # whichever particle filter was selected. # -# Two caveats relative to the Kalman path: these are filtered, not smoothed, -# estimates (a particle smoother is a different algorithm), and a linear shock -# decomposition does not exist for a nonlinear filter, so `decomposition` is -# returned as zeros. +# One caveat relative to the Kalman path: a linear shock decomposition does not +# exist for a nonlinear filter, so `decomposition` is returned as zeros. @unstable function filter_data_with_model(𝓂::ℳ, data_in_deviations::KeyedArray{Float64}, ::Val{algo}, @@ -1703,6 +1708,15 @@ end propagate! = (out, prev, sh) -> higher_propagate!(Val(algo), out, prev, sh, past_idx, 𝐒f, scr) end + # Smoothing storage (see the backward pass below). `hist_*` keep the cloud and + # the shocks of every period; `parent[t]` is the resampling map applied at the + # end of period t (empty ⇒ no resampling ⇒ identity), which is the genealogy + # the backward pass walks. + hist_states = smooth ? [Matrix{Float64}(undef, nVars, n_particles) for _ in 1:nT] : Matrix{Float64}[] + hist_shocks = smooth ? [Matrix{Float64}(undef, nExo, n_particles) for _ in 1:nT] : Matrix{Float64}[] + parent = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] + terminal_weights = smooth ? fill(1.0 / n_particles, n_particles) : Float64[] + for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view dat[:, t] @@ -1735,26 +1749,41 @@ end end end - # filtered moments of the (weighted) cloud - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - w = W[p] - for i in 1:nVars - variables[i, t] += w * full[i] + if smooth + # The backward pass starts from the cloud as stored here, i.e. *before* + # any resampling at the end of this period, so it needs the weights in + # that same indexing. Resampling would overwrite `W` with uniform ones. + copyto!(terminal_weights, W) + # keep the whole cloud so the backward pass can walk the genealogy + Hs = hist_states[t]; Hh = hist_shocks[t] + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + for i in 1:nVars; Hs[i, p] = full[i]; end + sp = shocks[p] + for e in 1:nExo; Hh[e, p] = sp[e]; end end - for e in 1:nExo - shocks_out[e, t] += w * shocks[p][e] + else + # filtered moments of the (weighted) cloud + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + w = W[p] + for i in 1:nVars + variables[i, t] += w * full[i] + end + for e in 1:nExo + shocks_out[e, t] += w * shocks[p][e] + end end - end - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - w = W[p] - for i in 1:nVars - stds[i, t] += w * (full[i] - variables[i, t])^2 + @inbounds for p in 1:n_particles + full = measurement_full(parts[p], full_buf) + w = W[p] + for i in 1:nVars + stds[i, t] += w * (full[i] - variables[i, t])^2 + end + end + @inbounds for i in 1:nVars + stds[i, t] = sqrt(max(stds[i, t], 0.0)) end - end - @inbounds for i in 1:nVars - stds[i, t] = sqrt(max(stds[i, t], 0.0)) end if effective_sample_size(W) < particle_resampling_threshold * n_particles @@ -1764,9 +1793,14 @@ end end parts, parts2 = parts2, parts fill!(W, 1.0 / n_particles) + if smooth; parent[t] = copy(idx); end end end + if smooth + smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, parent, terminal_weights) + end + @info "Shock decomposition is not defined for the particle filters (the state transition is nonlinear and the contributions do not add up); returning zeros. Use `filter = :kalman` for a shock decomposition." maxlog = 1 decomposition = zeros(nVars, nExo + 2, nT) @@ -1775,6 +1809,78 @@ end return variables, shocks_out, stds, decomposition end +# Backward pass of the particle smoother (fixed-interval smoothing by genealogy, +# a.k.a. forward-filtering backward-sampling on the filter's ancestral lines). +# +# Why this and not the textbook backward kernel? The usual particle smoother +# reweights particles at t by the backward transition density p(xₜ₊₁ | xₜ). For a +# DSGE that density is *singular*: with fewer shocks than states the transition +# maps xₜ onto a lower-dimensional manifold, so p(xₜ₊₁ | xₜ) is a Dirac on that +# manifold and the reweighting is undefined. What is well defined is the filter's +# own genealogy: every surviving particle at T carries the ancestral line that +# produced it, and those lines are draws from p(x₁..T | y₁..T). Averaging the +# lines with the final weights therefore gives the smoothed moments directly. +# +# `parent[t]` is the resampling map applied at the end of period t (empty means no +# resampling happened, i.e. the identity). Walking it backwards from T turns each +# final particle index into the index it occupied at every earlier period. +# +# Caveat worth knowing: ancestral lines coalesce as one goes back in time (path +# degeneracy), so the smoothed estimate for the earliest periods rests on fewer +# distinct trajectories than the particle count suggests. More particles push the +# coalescence point further back. +function smooth_particle_trajectories!(variables::Matrix{Float64}, + stds::Matrix{Float64}, + shocks_out::Matrix{Float64}, + hist_states::Vector{Matrix{Float64}}, + hist_shocks::Vector{Matrix{Float64}}, + parent::Vector{Vector{Int}}, + W::Vector{Float64}) + nT = length(hist_states) + nT == 0 && return nothing + nVars = size(variables, 1) + nExo = size(shocks_out, 1) + n_particles = length(W) + + # lineage: where each final particle sat at the period currently being visited + lineage = collect(1:n_particles) + + for t in nT:-1:1 + Hs = hist_states[t]; Hh = hist_shocks[t] + + @inbounds for p in 1:n_particles + a = lineage[p]; w = W[p] + for i in 1:nVars + variables[i, t] += w * Hs[i, a] + end + for e in 1:nExo + shocks_out[e, t] += w * Hh[e, a] + end + end + @inbounds for p in 1:n_particles + a = lineage[p]; w = W[p] + for i in 1:nVars + stds[i, t] += w * (Hs[i, a] - variables[i, t])^2 + end + end + @inbounds for i in 1:nVars + stds[i, t] = sqrt(max(stds[i, t], 0.0)) + end + + # step the lineage back across the resampling applied at the end of t-1 + if t > 1 + par = parent[t-1] + if !isempty(par) + @inbounds for p in 1:n_particles + lineage[p] = par[lineage[p]] + end + end + end + end + + return nothing +end + # out = A·prev[past] + B·shock for a single particle (estimates path; the # likelihood path propagates the whole swarm with one gemm instead). @inline function linear_propagate_estimates!(out::Vector{Float64}, tr::LinearParticleTransition, diff --git a/src/get_functions.jl b/src/get_functions.jl index b150fa967..b34cda93d 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -345,6 +345,7 @@ And data, 4×2×40 Array{Float64, 3}: data_in_deviations = prepare_trimmed_data_in_deviations(data, 𝓂, NSSS; data_in_levels = data_in_levels) extra_kw = marginal_contribution ? (; marginal_contribution = true) : NamedTuple() + warn_unused_measurement_error(filter, measurement_error_std) if filter ∈ PARTICLE_FILTERS extra_kw = merge(extra_kw, (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, @@ -502,6 +503,8 @@ And data, 1×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Shocks = axis1, Periods = 1:0) end + warn_unused_measurement_error(filter, measurement_error_std) + particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng) : NamedTuple() @@ -640,6 +643,8 @@ And data, 4×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Variables = axis1, Periods = 1:0) end + warn_unused_measurement_error(filter, measurement_error_std) + particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng) : NamedTuple() @@ -4336,6 +4341,16 @@ function get_statistics(𝓂::ℳ, return ret end +# The Kalman smoother path (`filter_and_smooth`) does not take measurement error, +# so a `measurement_error_std` supplied to the estimate entry points only has an +# effect for the particle filters. Say so rather than silently dropping it. +function warn_unused_measurement_error(filter::Symbol, measurement_error_std; maxlog::Int = DEFAULT_MAXLOG) + if filter ∉ PARTICLE_FILTERS && measurement_error_std !== DEFAULT_MEASUREMENT_ERROR_STD + @info "`measurement_error_std` is only used by the particle filters on this path; it is ignored for `filter = :$(filter)`. Use `get_loglikelihood` if you need measurement error in the Kalman likelihood." maxlog = maxlog + end + return nothing +end + # Resolve `measurement_error_std = :auto` for the filter-based `get_loglikelihood` # path. The Kalman and inversion filters default to no measurement error (their # historical behaviour). The particle filters are degenerate without measurement diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 5770dff76..a76d156a4 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -163,6 +163,35 @@ threw(f) = try; f(); false; catch; true; end n_particles = 5_000, particle_rng = Random.Xoshiro(1)))) end + @testset "Particle smoothing" begin + kal_sm = collect(get_estimated_variables(RBC_pf, data; filter = :kalman, smooth = true)) + pf_filt = collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :first_order, smooth = false, measurement_error_std = me, + n_particles = 20_000, particle_rng = Random.Xoshiro(1))) + pf_sm = collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :first_order, smooth = true, measurement_error_std = me, + n_particles = 20_000, particle_rng = Random.Xoshiro(1))) + @test all(isfinite, pf_sm) + @test size(pf_sm) == size(kal_sm) + # using the whole sample must move the estimates closer to the Kalman smoother + @test maximum(abs, pf_sm .- kal_sm) < maximum(abs, pf_filt .- kal_sm) + # smoothing works for the other variants and at nonlinear orders + for pf_filter in (:auxiliary_particle, :tempered_particle) + @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = pf_filter, + algorithm = :first_order, smooth = true, measurement_error_std = me, + n_particles = 5_000, particle_rng = Random.Xoshiro(2)))) + end + @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :pruned_second_order, smooth = true, measurement_error_std = me, + n_particles = 3_000, particle_rng = Random.Xoshiro(3)))) + @test all(isfinite, collect(get_estimated_shocks(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :first_order, smooth = true, measurement_error_std = me, + n_particles = 10_000, particle_rng = Random.Xoshiro(4)))) + # the inversion filter still has no smoother + @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :inversion, + algorithm = :first_order, smooth = true))) + end + @testset "Full measurement-error covariance" begin Hdiag = [me^2 0.0; 0.0 me^2] # a diagonal covariance reproduces the equivalent per-observable stds From 2e51d65aedac4571789336b68876ce530c9b2ee3 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 17:29:16 +0200 Subject: [PATCH 10/24] Add particle-filter shock decomposition; fix broken plotting signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shock decomposition works for the particle filters after all — it needs a shock path, and the smoother now supplies one, so the same attribution the inversion filter uses applies. At first order the contributions are additive and the split is exact. At pruned second and third order they are not additive, which is what the Aumann-Shapley (marginal contribution) attribution is for, so those orders reuse `aumann_shapley_shock_decomposition_pruned_{2nd,3rd}_order!` from inversion.jl under `marginal_contribution = true`. Non-pruned second/third order have no decomposition at any filter, matching the existing gating. The previous blanket "not defined for particle filters" claim was wrong and is gone. One subtlety this surfaced: the Aumann-Shapley routine checks that the contributions reproduce the supplied `variables`, but a smoothed *mean* is not a model trajectory (averaging does not commute with a nonlinear transition), so passing it in left a closure error the routine tried to remove by refining its quadrature until it ran past its hand-coded node limit. The pruned decomposition therefore attributes the trajectory implied by the smoothed shocks — the same object the inversion filter decomposes — which closes exactly. Also fixes a duplicated keyword block in `plot_model_estimates` introduced when the particle options were threaded into the plotting extension, which made the function uncallable with any arguments. Adds plotting coverage (Kalman, filtered and smoothed particle filters, and a pruned second-order run with the Aumann-Shapley decomposition) so the extension is exercised by the particle filter test set rather than only by the plots jobs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 6 ++++ ext/StatsPlotsExt.jl | 6 ---- src/filter/particle.jl | 64 ++++++++++++++++++++++++++++++++++-- test/test_particle_filter.jl | 35 ++++++++++++++++++++ 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index 44e9d14b0..9e4944537 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -144,6 +144,12 @@ In practice this buys a large variance reduction per particle — several times Why not the textbook backward-kernel smoother? That one reweights particles at ``t`` by the backward transition density ``p(x_{t+1} \mid x_t)``. In a DSGE that density is **singular**: with fewer shocks than states the transition maps ``x_t`` onto a lower-dimensional manifold, so ``p(x_{t+1} \mid x_t)`` is a Dirac on that manifold and the reweighting is undefined. The genealogy is what remains well defined. +#### Shock decomposition + +A shock decomposition needs a shock path, and the smoother supplies one, so the particle filters decompose too. At **first order** the contributions are additive and the split is exact — each shock's contribution is propagated through the linear transition and the columns sum to the total. At **pruned second and third order** the contributions are *not* additive, which is exactly what the Aumann–Shapley (marginal contribution) attribution is for; set `marginal_contribution = true` and the particle path reuses the same routines the inversion filter uses. Non-pruned `:second_order` / `:third_order` have no decomposition at any filter. + +One subtlety specific to a Monte-Carlo filter: the smoothed *mean* path is not itself a model trajectory, because averaging does not commute with a nonlinear transition (``E[g(x,\varepsilon)] \neq g(E[x],E[\varepsilon])``). The pruned decomposition therefore attributes the trajectory implied by the smoothed shocks — the same object the inversion filter decomposes — so that the contributions close exactly. + The known limitation is **path degeneracy**: ancestral lines coalesce as one goes back in time, so the earliest periods rest on fewer distinct trajectories than the particle count suggests. More particles push the coalescence point further back. Smoothing also stores the whole cloud, so its memory cost is about ``n_{vars} \times N \times T \times 8`` bytes — worth keeping in mind before raising `n_particles` for a long sample. ### Resampling schemes diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index b13dcc0d8..71255fba6 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -712,12 +712,6 @@ function plot_model_estimates(𝓂::ℳ, particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, - particle_rng::Random.AbstractRNG = Random.default_rng(), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, - n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, - particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, - particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, - particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 558d3e6ba..85e85eb7f 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -1643,6 +1643,7 @@ end particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + marginal_contribution::Bool = false, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical) where {algo} obs_axis = collect(axiskeys(data_in_deviations, 1)) @@ -1801,11 +1802,70 @@ end smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, parent, terminal_weights) end - @info "Shock decomposition is not defined for the particle filters (the state transition is nonlinear and the contributions do not add up); returning zeros. Use `filter = :kalman` for a shock decomposition." maxlog = 1 - + # ── Shock decomposition ────────────────────────────────────────────────── + # A decomposition needs a shock path; the particle filter supplies one (the + # smoothed shocks above), so the same attribution the inversion filter uses + # applies here. At first order contributions are additive and the split is + # exact. At pruned higher order they are not additive, which is precisely + # what the Aumann-Shapley (marginal contribution) attribution is for, so the + # pruned decomposition reuses the routines in `inversion.jl`. Non-pruned + # `:second_order` / `:third_order` have no decomposition at all (the caller + # already turns `shock_decomposition` off for them). decomposition = zeros(nVars, nExo + 2, nT) decomposition[:, end, :] .= variables + if algo == :first_order + 𝐒₁ = 𝐒 isa AbstractMatrix ? 𝐒 : 𝐒[1] + init_vec = state isa AbstractVector{<:AbstractVector} ? state[1] : state + sck = zeros(nExo) + @inbounds for i in 1:nExo + fill!(sck, 0.0); sck[i] = shocks_out[i, 1] + decomposition[:, i, 1] .= 𝐒₁ * vcat(init_vec[past_idx], sck) + end + decomposition[:, end - 1, 1] .= decomposition[:, end, 1] - sum(decomposition[:, 1:end-2, 1], dims = 2) + for t in 2:nT + @inbounds for i in 1:nExo + fill!(sck, 0.0); sck[i] = shocks_out[i, t] + decomposition[:, i, t] .= 𝐒₁ * vcat(decomposition[past_idx, i, t-1], sck) + end + decomposition[:, end - 1, t] .= decomposition[:, end, t] - sum(decomposition[:, 1:end-2, t], dims = 2) + end + elseif algo ∈ (:pruned_second_order, :pruned_third_order) && marginal_contribution + # The Aumann-Shapley attribution requires `variables` and `shocks` to lie + # on the *same* model trajectory — it checks that the contributions plus + # the zero-shock baseline reproduce `variables`. A smoothed mean is not a + # model path (averaging does not commute with the nonlinear transition: + # E[g(x,ε)] ≠ g(E[x],E[ε])), so feeding it in directly leaves a closure + # error that the routine tries to remove by refining its quadrature. + # Decompose the trajectory implied by the smoothed shocks instead, which + # is the same object the inversion filter decomposes and closes exactly. + traj = zeros(nVars, nT) + cur = deepcopy(state) + nxt = zeros_like_particle(cur) + buf = Vector{Float64}(undef, nVars) + shk = Vector{Float64}(undef, nExo) + for t in 1:nT + @inbounds for e in 1:nExo; shk[e] = shocks_out[e, t]; end + higher_propagate!(Val(algo), nxt, cur, shk, past_idx, 𝐒f, scr) + full = measurement_full(nxt, buf) + @inbounds for i in 1:nVars; traj[i, t] = full[i]; end + cur, nxt = nxt, cur + end + decomposition[:, end, :] .= traj + + if algo == :pruned_second_order + aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, traj, shocks_out, + state, 𝐒, T, nExo; verbose = opts.verbose) + else + aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, traj, shocks_out, + state, 𝐒, T, nExo; verbose = opts.verbose) + end + elseif algo ∈ (:pruned_second_order, :pruned_third_order) + @info "At pruned higher order the shock contributions are not additive, so the particle filter decomposes them with the Aumann-Shapley (marginal contribution) attribution. Set `marginal_contribution = true` to get it; returning zeros otherwise." maxlog = 1 + else + @info "Shock decomposition is not available for $(algo) solutions (use a pruned solution); returning zeros." maxlog = 1 + end + return variables, shocks_out, stds, decomposition end diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index a76d156a4..b1a07cb90 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -5,6 +5,7 @@ import Statistics import AxisKeys: KeyedArray import ForwardDiff import Zygote +using StatsPlots # A small RBC model with two shocks and two observables, so the (bootstrap) # particle filter is non-degenerate and can be validated against the exact @@ -192,6 +193,40 @@ threw(f) = try; f(); false; catch; true; end algorithm = :first_order, smooth = true))) end + @testset "Shock decomposition" begin + # first order: contributions are additive, so the split is exact + d1 = collect(get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, + algorithm = :first_order, smooth = true, measurement_error_std = me, + n_particles = 10_000, particle_rng = Random.Xoshiro(1))) + @test all(isfinite, d1) + @test !all(iszero, d1) + # pruned orders: not additive, hence the Aumann-Shapley (marginal contribution) split + for algo in (:pruned_second_order, :pruned_third_order) + d = collect(get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, + algorithm = algo, smooth = true, marginal_contribution = true, + measurement_error_std = me, n_particles = 2_000, + particle_rng = Random.Xoshiro(2))) + @test all(isfinite, d) + @test !all(iszero, d) + end + end + + @testset "Plotting with the particle filters" begin + tmp = mktempdir() + for (pf_filter, algo, kw) in ((:bootstrap_particle, :first_order, (;)), + (:tempered_particle, :first_order, (; smooth = true)), + (:bootstrap_particle, :pruned_second_order, + (; smooth = true, shock_decomposition = true, marginal_contribution = true))) + p = plot_model_estimates(RBC_pf, data; filter = pf_filter, algorithm = algo, + measurement_error_std = me, n_particles = 2_000, + particle_rng = Random.Xoshiro(1), show_plots = false, + save_plots = true, save_plots_path = tmp, + save_plots_format = :png, kw...) + @test p !== nothing + end + @test !isempty(readdir(tmp)) + end + @testset "Full measurement-error covariance" begin Hdiag = [me^2 0.0; 0.0 me^2] # a diagonal covariance reproduces the equivalent per-observable stds From e8ed6c2548682bf32ee33aaa1d35b5929b10577d Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 17:49:47 +0200 Subject: [PATCH 11/24] Support both pruned decomposition attributions for the particle filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the sequential pruned attribution so `marginal_contribution = false` works too, rather than reporting that Aumann-Shapley is required. It runs one trajectory per shock with only that shock switched on plus one with all of them: each single-shock path is that shock's contribution, the all-shock path minus their sum is the interaction the nonlinearity creates, and the remainder goes to the residual. This reproduces the inversion filter's column layout, so both attributions now match it exactly — nExo+3 columns for the sequential split and nExo+2 for the Aumann-Shapley split, which distributes the interaction across the shocks instead of isolating it. Decomposition works off whichever shock path the run produced, so it is available for the filtered estimates (`smooth = false`) as well as the smoothed ones (`smooth = true`); the two give different decompositions because the underlying shock estimates differ. Tests cover both attributions at both pruned orders under both settings, and check that at first order — where the split is additive — the shocks explain the bulk of the movement and the residual is left carrying the initial-state contribution. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 2 +- src/filter/particle.jl | 47 +++++++++++++++++++++++++++++++---- test/test_particle_filter.jl | 48 +++++++++++++++++++++++++----------- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index 9e4944537..337047d26 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -146,7 +146,7 @@ Why not the textbook backward-kernel smoother? That one reweights particles at ` #### Shock decomposition -A shock decomposition needs a shock path, and the smoother supplies one, so the particle filters decompose too. At **first order** the contributions are additive and the split is exact — each shock's contribution is propagated through the linear transition and the columns sum to the total. At **pruned second and third order** the contributions are *not* additive, which is exactly what the Aumann–Shapley (marginal contribution) attribution is for; set `marginal_contribution = true` and the particle path reuses the same routines the inversion filter uses. Non-pruned `:second_order` / `:third_order` have no decomposition at any filter. +A shock decomposition needs a shock path, and the particle filters supply one — filtered with `smooth = false`, smoothed with `smooth = true` — so they decompose either way. At **first order** the contributions are additive and the split is exact — each shock's contribution is propagated through the linear transition and the columns sum to the total. At **pruned second and third order** the contributions are *not* additive, which is exactly what the Aumann–Shapley (marginal contribution) attribution is for; both attributions are available, exactly as for the inversion filter: `marginal_contribution = false` gives the sequential split with an explicit interaction column, and `marginal_contribution = true` gives the Aumann–Shapley split that distributes the interaction across the shocks. Non-pruned `:second_order` / `:third_order` have no decomposition at any filter. One subtlety specific to a Monte-Carlo filter: the smoothed *mean* path is not itself a model trajectory, because averaging does not commute with a nonlinear transition (``E[g(x,\varepsilon)] \neq g(E[x],E[\varepsilon])``). The pruned decomposition therefore attributes the trajectory implied by the smoothed shocks — the same object the inversion filter decomposes — so that the contributions close exactly. diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 85e85eb7f..b6d822216 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -1628,8 +1628,8 @@ end # moments below are produced by the standard predict/weight/resample recursion # whichever particle filter was selected. # -# One caveat relative to the Kalman path: a linear shock decomposition does not -# exist for a nonlinear filter, so `decomposition` is returned as zeros. +# `decomposition` is the shock decomposition of whichever shock path was +# produced — filtered when `smooth = false`, smoothed when `smooth = true`. @unstable function filter_data_with_model(𝓂::ℳ, data_in_deviations::KeyedArray{Float64}, ::Val{algo}, @@ -1811,7 +1811,12 @@ end # pruned decomposition reuses the routines in `inversion.jl`. Non-pruned # `:second_order` / `:third_order` have no decomposition at all (the caller # already turns `shock_decomposition` off for them). - decomposition = zeros(nVars, nExo + 2, nT) + # Column layout follows the inversion filter: with the Aumann-Shapley + # attribution (and at first order) it is [contributions…, baseline, total] = + # nExo+2; the sequential pruned attribution adds an explicit interaction and + # residual column, [contributions…, interaction, residual, total] = nExo+3. + sequential_pruned = algo ∈ (:pruned_second_order, :pruned_third_order) && !marginal_contribution + decomposition = zeros(nVars, sequential_pruned ? nExo + 3 : nExo + 2, nT) decomposition[:, end, :] .= variables if algo == :first_order @@ -1860,8 +1865,40 @@ end aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, traj, shocks_out, state, 𝐒, T, nExo; verbose = opts.verbose) end - elseif algo ∈ (:pruned_second_order, :pruned_third_order) - @info "At pruned higher order the shock contributions are not additive, so the particle filter decomposes them with the Aumann-Shapley (marginal contribution) attribution. Set `marginal_contribution = true` to get it; returning zeros otherwise." maxlog = 1 + elseif sequential_pruned + # Sequential attribution: run one trajectory per shock with only that + # shock switched on, plus one with all of them. Each single-shock path is + # that shock's contribution; the all-shock path minus the sum of the + # single-shock paths is the interaction the nonlinearity creates (this is + # the term the Aumann-Shapley variant instead distributes across shocks), + # and whatever is still left over goes into the residual column. + states_dec = [deepcopy(state) for _ in 1:nExo + 1] + nxt = zeros_like_particle(state) + dbuf = Vector{Float64}(undef, nVars) + single = zeros(nExo) + allsh = Vector{Float64}(undef, nExo) + + for t in 1:nT + @inbounds for ii in 1:nExo + fill!(single, 0.0); single[ii] = shocks_out[ii, t] + higher_propagate!(Val(algo), nxt, states_dec[ii], single, past_idx, 𝐒f, scr) + copy_particle!(states_dec[ii], nxt) + full = measurement_full(states_dec[ii], dbuf) + for v in 1:nVars; decomposition[v, ii, t] = full[v]; end + end + + @inbounds for e in 1:nExo; allsh[e] = shocks_out[e, t]; end + higher_propagate!(Val(algo), nxt, states_dec[end], allsh, past_idx, 𝐒f, scr) + copy_particle!(states_dec[end], nxt) + full = measurement_full(states_dec[end], dbuf) + + # interaction = all-shock path − Σ single-shock paths + @inbounds for v in 1:nVars; decomposition[v, end - 2, t] = full[v]; end + decomposition[:, end - 2, t] .-= sum(decomposition[:, 1:end-3, t], dims = 2) + # residual = reported estimate − everything attributed so far + decomposition[:, end - 1, t] .= variables[:, t] + decomposition[:, end - 1, t] .-= sum(decomposition[:, 1:end-2, t], dims = 2) + end else @info "Shock decomposition is not available for $(algo) solutions (use a pruned solution); returning zeros." maxlog = 1 end diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index b1a07cb90..f9a40a7ef 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -194,20 +194,40 @@ threw(f) = try; f(); false; catch; true; end end @testset "Shock decomposition" begin - # first order: contributions are additive, so the split is exact - d1 = collect(get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :first_order, smooth = true, measurement_error_std = me, - n_particles = 10_000, particle_rng = Random.Xoshiro(1))) - @test all(isfinite, d1) - @test !all(iszero, d1) - # pruned orders: not additive, hence the Aumann-Shapley (marginal contribution) split - for algo in (:pruned_second_order, :pruned_third_order) - d = collect(get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, - algorithm = algo, smooth = true, marginal_contribution = true, - measurement_error_std = me, n_particles = 2_000, - particle_rng = Random.Xoshiro(2))) - @test all(isfinite, d) - @test !all(iszero, d) + # `get_shock_decomposition` returns [contributions..., (interaction,) residual]; + # the residual carries whatever the shocks do not explain, i.e. the + # contribution of the initial state. + nE = length(get_shocks(RBC_pf)) + dec(; kw...) = get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, + measurement_error_std = me, n_particles = 6_000, + particle_rng = Random.Xoshiro(1), kw...) + + # available for the filtered *and* the smoothed shock estimates + for sm in (false, true) + d = dec(algorithm = :first_order, smooth = sm) + @test size(d, 2) == nE + 1 + @test all(isfinite, collect(d)) + @test !all(iszero, collect(d)) + # first order is additive, so the shocks explain most of the movement + A = collect(d) + @test maximum(abs, A[:, end, :]) < 0.25 * maximum(abs, A[:, 1:end-1, :]) + end + # filtered and smoothed shock paths give different decompositions + @test collect(dec(algorithm = :first_order, smooth = false)) != + collect(dec(algorithm = :first_order, smooth = true)) + + # pruned orders, both attributions, filtered and smoothed + for algo in (:pruned_second_order, :pruned_third_order), sm in (false, true) + # sequential: an explicit interaction column for the non-additive part + ds = dec(algorithm = algo, smooth = sm, marginal_contribution = false) + @test size(ds, 2) == nE + 2 + @test all(isfinite, collect(ds)) + @test !all(iszero, collect(ds)) + # Aumann-Shapley: the interaction is distributed across the shocks + dm = dec(algorithm = algo, smooth = sm, marginal_contribution = true) + @test size(dm, 2) == nE + 1 + @test all(isfinite, collect(dm)) + @test !all(iszero, collect(dm)) end end From 95e7f374651bffa2421c08625ff0e3507845126a Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 20:54:23 +0200 Subject: [PATCH 12/24] Rename measurement error to `measurement_error` with covariance semantics Reviewer feedback: the argument is a covariance, not a standard deviation, and it had three different names across the code (`measurement_error_std`, `measurement_error_variances`, `measurement_error_covariance`). - one user-facing kwarg `measurement_error`, subsuming the separate covariance argument: scalar = common variance, vector = per-observable variances, matrix = full covariance. The pre-existing positional `measurement_error_std` on the filter-free `get_loglikelihood` keeps its name, since it genuinely is a standard deviation. - kernels take `measurement_error` too, and `:auto` is resolved once at the user-facing layer (`resolve_measurement_error`) so no kernel sees a Symbol. - the particle filters now accept a correlated covariance: `DenseMeasurementError` caches a Cholesky factor of H restricted to each missing-data pattern. A diagonal matrix is reduced to the variance vector so the elementwise fast path is unchanged. - `on_failure_loglikelihood` defaults to -1e6 for the particle filters instead of -Inf; a stochastic failure should reject a proposal, not kill a chain. - `get_estimated_variable_standard_deviations` gains `algorithm`/`filter` and the particle kwargs, reporting the cloud spread at any perturbation order. - particle-filter keyword docs move to `common_docstrings.jl` as shared constants; `filters.md` and the estimation tutorial follow the new semantics. - answer the review questions inline: why the Kalman filter needs H at all (particle.jl header), why the initial covariance is first order at every perturbation order, and why the inversion filter's smoother is a no-op rather than unsupported (its filtered estimate is already the smoothed one). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- benchmark/particle_filter_llpf_comparison.jl | 4 +- docs/src/filters.md | 29 +-- docs/src/tutorials/estimation.md | 8 +- ext/ForwardDiffExt.jl | 10 +- ext/StatsPlotsExt.jl | 8 +- src/MacroModelling.jl | 21 +- src/common_docstrings.jl | 32 ++- src/default_options.jl | 19 +- src/filter/kalman.jl | 36 +-- src/filter/particle.jl | 225 ++++++++++++++---- src/get_functions.jl | 232 ++++++++++--------- src/rrules.jl | 14 +- test/test_particle_filter.jl | 131 +++++++---- test/test_particle_filter_sw07.jl | 4 +- 14 files changed, 505 insertions(+), 268 deletions(-) diff --git a/benchmark/particle_filter_llpf_comparison.jl b/benchmark/particle_filter_llpf_comparison.jl index ab95eb1e5..f7d0f8d3f 100644 --- a/benchmark/particle_filter_llpf_comparison.jl +++ b/benchmark/particle_filter_llpf_comparison.jl @@ -47,7 +47,7 @@ function bench_model(name, m, data, observables, me; N = 20000, nseed = 8) params = m.parameter_values kal = get_loglikelihood(m, data(observables), params; filter = :kalman, presample_periods = 0, initial_covariance = :theoretical, - measurement_error_std = me) + measurement_error = me .^ 2) println("\n==== $name (N=$N) ====") println("Kalman+ME = ", round(kal, digits = 3)) @@ -56,7 +56,7 @@ function bench_model(name, m, data, observables, me; N = 20000, nseed = 8) t0 = time() lls = [get_loglikelihood(m, data(observables), params; filter = pf_filter, algorithm = :first_order, presample_periods = 0, initial_covariance = :theoretical, - measurement_error_std = me, + measurement_error = me .^ 2, n_particles = Nn, particle_rng = Random.Xoshiro(s)) for s in 1:nseed] println("MacroModelling ", rpad(String(pf_filter), 19), " N=$Nn mean=", round(Statistics.mean(lls), digits = 2), " std=", round(Statistics.std(lls), digits = 2), " time/run=", round((time() - t0) / nseed, digits = 3), "s") diff --git a/docs/src/filters.md b/docs/src/filters.md index 337047d26..8f6eaf673 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -35,7 +35,7 @@ get_loglikelihood(model, data, parameters; filter = :kalman) A short decision rule: - **Linear model?** Use `:kalman`. It is exact, fast and differentiable, so gradient-based samplers (NUTS/HMC) work. There is no reason to use anything else. -- **Nonlinear model, as many shocks as observables, no measurement error?** Use `:inversion` (the default at higher order). It is exact and differentiable. +- **Nonlinear model, at least as many shocks as observables, no measurement error?** Use `:inversion` (the default at higher order). It is exact and differentiable. - **Nonlinear model with measurement error, or fewer shocks than observables?** Use a particle filter. Start with `:tempered_particle` if the observation is informative (small measurement error, many observables), otherwise `:bootstrap_particle`. - Particle-filter likelihoods are noisy and non-differentiable: pair them with gradient-free samplers such as slice sampling (Pigeons.jl) or nested sampling. @@ -104,11 +104,13 @@ All variants share the same skeleton: The crucial property is that ``\widehat{p}(y_{1:T})`` is an **unbiased** estimator of the true likelihood, for any ``N``. That is what makes particle filters usable inside a sampler (pseudo-marginal MCMC targets the exact posterior despite the noise). Note the consequence for the *log* likelihood: by Jensen's inequality ``E[\log \widehat{p}] < \log p``, with a downward bias of roughly ``\mathrm{Var}(\log\widehat p)/2``. So a particle log-likelihood is systematically a little *below* the Kalman value on a linear model, and the gap shrinks as ``N`` grows — this is the expected behaviour, not a bug. +Because the estimate is random, a repeated evaluation at the same parameters gives a different number unless you fix the stream: pass a seeded generator via `particle_rng` (e.g. `particle_rng = Random.Xoshiro(1)`). Inside a sampler, reuse the same seed across parameter draws only if you deliberately want common random numbers; otherwise let the sampler see fresh noise, which is what pseudo-marginal correctness assumes. Cost scales roughly linearly in `n_particles` and in the sample length, and the number of particles needed grows quickly with the number of observables. + ### Why measurement error is required Without measurement error the observation equation is a deterministic function of the state. A particle would have to reproduce ``y_t`` *exactly* to get non-zero weight, which happens with probability zero — every weight collapses to zero and the filter dies. Measurement error smears the observation density and gives particles something to score against. -`measurement_error_std = :auto` (the default) therefore resolves to 10% of each observable's sample standard deviation for the particle filters, and to *no* measurement error for the Kalman and inversion filters. For serious work set it explicitly or estimate it: the level of the likelihood depends on it, so likelihoods computed under different measurement errors are not comparable. +`measurement_error` is the covariance ``H`` of ``\eta_t``, never a standard deviation: a scalar is the common variance of every observable, a vector the per-observable variances, and a matrix the full covariance. `measurement_error = :auto` (the default) resolves to a variance of ``(0.1 s_i)^2`` per observable, where ``s_i`` is that observable's sample standard deviation, for the particle filters — and to *no* measurement error for the Kalman and inversion filters. For serious work set it explicitly or estimate it: the level of the likelihood depends on it, so likelihoods computed under different measurement errors are not comparable. ### Bootstrap (`:bootstrap_particle`) @@ -170,25 +172,6 @@ Resampling only happens when the effective sample size ``1/\sum_i W_i^2`` falls - **Particle → Kalman.** On a *linear* model with Gaussian shocks, the particle filters estimate exactly the quantity the Kalman filter computes in closed form. As ``N \to \infty`` the particle log-likelihood converges to the Kalman log-likelihood (from below, by the Jensen bias above). This is the sharpest correctness check available and is exactly what the package's tests do, on both a small RBC model and Smets-Wouters (2007). - **Kalman → particle.** The Kalman filter is the special case where the transition and observation are linear and the noise Gaussian, so the "cloud" is fully described by its first two moments. - **Inversion → particle.** Both handle nonlinear models, but they make opposite trades. The inversion filter assumes measurement error is *zero* and recovers the shocks exactly; the particle filter assumes measurement error is *positive* and integrates the shocks out. As the measurement error goes to zero the particle filter degenerates towards the inversion filter's problem — and this is precisely where it needs the most particles. -- **Correlated measurement error.** The Kalman filter accepts an arbitrary `measurement_error_covariance`. The particle filters require it to be diagonal, but this is not a real restriction: correlated measurement error can be written into the model itself as measurement-error processes in the observation equations, which moves the correlation into the state transition and makes ``H`` diagonal again. That reformulation works for every filter. - -## Reproducibility and cost - -Particle-filter likelihoods are random. Pass a seeded generator via `particle_rng` to make an evaluation reproducible: - -```julia -import Random -get_loglikelihood(model, data, parameters; - filter = :tempered_particle, - algorithm = :pruned_second_order, - n_particles = 20_000, - measurement_error_std = 1e-3, - particle_rng = Random.Xoshiro(1)) -``` - -Two practical notes when using them inside a sampler: - -- Use the *same* seed across parameter draws only if you want a "common random numbers" scheme; otherwise let the sampler see fresh noise, which is what pseudo-marginal correctness assumes. -- Set `on_failure_loglikelihood` to a large finite negative number so an occasional failed evaluation does not abort the chain. +- **Inversion → Kalman.** On a *first-order* solution with as many shocks as observables and no measurement error, the two agree. The Kalman filter's innovation covariance is then ``F_t = C P_t C'`` with ``P_t = BB'`` — the state is exactly identified by the data, so ``P_t`` never accumulates uncertainty and the update is a deterministic inversion. Both filters end up scoring the same shock path, the inversion filter directly as ``-\tfrac12(\varepsilon_t'\varepsilon_t + \log 2\pi)`` plus a Jacobian term, the Kalman filter through ``v_t'F_t^{-1}v_t + \log\det F_t``, and these are the same number written two ways. They part company as soon as either assumption breaks: with *more observables than shocks* the system is stochastically singular and only the Kalman filter (with measurement error) is defined; with *fewer* observables than shocks the state is no longer pinned down by the data, ``P_t`` is genuinely non-degenerate, and only the Kalman filter integrates over it. Add measurement error and the inversion filter is not defined at all. At higher order the inversion filter's per-period Newton solve has no Kalman counterpart, which is why it — not the Kalman filter — is the default for nonlinear algorithms. +- **Correlated measurement error.** All filters that admit measurement error accept an arbitrary covariance: pass `measurement_error` a matrix instead of a vector of variances. The Kalman filter adds it to ``F_t`` directly; the particle filters factorise ``H`` once per missing-data pattern and score against the resulting triangular solve. The diagonal case is detected and takes a faster elementwise path, so there is no cost to the common case. A third option is to write the correlation into the model itself as measurement-error processes in the observation equations, which moves it into the state transition and makes ``H`` diagonal again — worth doing when the measurement errors are persistent rather than merely contemporaneously correlated. -Cost scales roughly linearly in `n_particles` and in the sample length, and the number of particles needed grows quickly with the number of observables. The default of 10,000 particles keeps a Smets-Wouters-sized problem accurate to a couple of log-likelihood points; raise it when the estimates look noisy. diff --git a/docs/src/tutorials/estimation.md b/docs/src/tutorials/estimation.md index 0cf9a4766..f029ab811 100644 --- a/docs/src/tutorials/estimation.md +++ b/docs/src/tutorials/estimation.md @@ -257,7 +257,7 @@ shows the variables of the model (blue), data (red), the shock decomposition for ## Nonlinear estimation with the particle filter -For genuinely nonlinear models the structural shocks can be integrated out by Monte Carlo using a particle filter. These work for every perturbation order (`:first_order` through `:pruned_third_order`) and require measurement error on the observables (`measurement_error_std`, which defaults to a small data-driven value). Each variant is its own `filter` value: +For genuinely nonlinear models the structural shocks can be integrated out by Monte Carlo using a particle filter. These work for every perturbation order (`:first_order` through `:pruned_third_order`) and require measurement error on the observables. That is what `measurement_error` supplies: the covariance of the Gaussian measurement error, as a scalar variance, a vector of per-observable variances, or a full covariance matrix — not a standard deviation. It defaults to `:auto`, which is a variance of `(0.1 s_i)^2` per observable, `s_i` being that observable's sample standard deviation. Because it is scale-free that default is a reasonable starting point on any dataset, from a two-observable RBC model to Smets-Wouters (2007); set it explicitly (or estimate it) once you care about the level of the likelihood. Each variant is its own `filter` value: - `:bootstrap_particle` — the sequential-importance-resampling filter of Gordon, Salmond & Smith (1993), applied to DSGE models by Fernández-Villaverde & Rubio-Ramírez (2007), - `:auxiliary_particle` — the auxiliary particle filter of Pitt & Shephard (1999), which uses a look-ahead proposal, @@ -279,13 +279,11 @@ Turing.@model function FS2000_particle(data, m) algorithm = :pruned_second_order, filter = :tempered_particle, n_particles = 5000, - measurement_error_std = 1e-3, - particle_rng = Random.Xoshiro(1), - on_failure_loglikelihood = -1e12) + particle_rng = Random.Xoshiro(1)) end pt = Pigeons.pigeons(target = Pigeons.TuringLogPotential(FS2000_particle(data, FS2000)), n_rounds = 8) ``` -Because the likelihood is noisy, set `on_failure_loglikelihood` to a finite value (as above) so the sampler tolerates occasional failed evaluations, and prefer a larger `n_particles` (and the `:tempered_particle` filter) to reduce the estimate's variance. +Because the likelihood is noisy, `on_failure_loglikelihood` already defaults to a large finite penalty (`-1e6`) for the particle filters rather than `-Inf`, so an occasional failed evaluation rejects one proposal instead of killing the chain; override it if your posterior legitimately reaches values that low. Prefer a larger `n_particles` (and the `:tempered_particle` filter) to reduce the estimate's variance. diff --git a/ext/ForwardDiffExt.jl b/ext/ForwardDiffExt.jl index 6b2d655ae..9f564c994 100644 --- a/ext/ForwardDiffExt.jl +++ b/ext/ForwardDiffExt.jl @@ -1009,7 +1009,7 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::ℱ.Dual{Z,S,N} where {Z,S,N,R <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -1075,14 +1075,14 @@ function MacroModelling.calculate_loglikelihood(::Val{:kalman}, # Add the measurement-error covariance H: F = C P C' + H (a vector of # per-observable variances, or a full covariance matrix). - if measurement_error_variances !== nothing - if measurement_error_variances isa AbstractMatrix + if measurement_error !== nothing + if measurement_error isa AbstractMatrix @inbounds for j in 1:no, i in 1:no - F_buf[i, j] += measurement_error_variances[i, j] + F_buf[i, j] += measurement_error[i, j] end else @inbounds for i in 1:no - F_buf[i, i] += measurement_error_variances[i] + F_buf[i, i] += measurement_error[i] end end end diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index 71255fba6..3688bc672 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -707,7 +707,7 @@ function plot_model_estimates(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR, n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -848,7 +848,7 @@ function plot_model_estimates(𝓂::ℳ, extra_kw = mc ? (; marginal_contribution = true) : NamedTuple() if filter ∈ MacroModelling.PARTICLE_FILTERS - extra_kw = merge(extra_kw, (; measurement_error_std, n_particles, particle_resampling, + extra_kw = merge(extra_kw, (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng)) end @@ -1373,7 +1373,7 @@ function plot_model_estimates!(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR, n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -1502,7 +1502,7 @@ function plot_model_estimates!(𝓂::ℳ, x_axis = x_axis[periods] particle_kw = filter ∈ MacroModelling.PARTICLE_FILTERS ? - (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng) : NamedTuple() variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; particle_kw...) diff --git a/src/MacroModelling.jl b/src/MacroModelling.jl index aca360ccd..e52cf0366 100644 --- a/src/MacroModelling.jl +++ b/src/MacroModelling.jl @@ -421,9 +421,26 @@ function normalize_filtering_options(filter::Symbol, # Smoothing is available for the Kalman filter (Durbin-Koopman smoother) and # for the particle filters (fixed-interval smoothing along the filter's - # genealogy). The inversion filter has no smoothing counterpart. + # genealogy). + # + # For the inversion filter there is nothing left to smooth. Given x₀ it solves + # yₜ = g(xₜ₋₁, εₜ)[observables] for εₜ exactly, so xₜ is a *deterministic* + # function of y₁..ₜ — the filtering distribution is a point mass. Conditioning + # on future data cannot sharpen a point mass, hence p(xₜ|y₁..T) = p(xₜ|y₁..ₜ) + # and a backward pass recovers exactly the shocks the forward pass already + # found. The filtered estimate *is* the smoothed estimate; `smooth` is a no-op + # rather than an unsupported option. + # + # Two caveats, neither of which a smoothing recursion would fix. The initial + # state x₀ is not identified by the data and is fixed at the (stochastic) + # steady state; refining it is a fixed-point problem over x₀, not a backward + # recursion. And with more shocks than observables the per-period solve picks + # the minimum-norm εₜ (at higher order, the root whose basin contains the + # origin — see `find_shocks`), which is a per-period choice a smoother could in + # principle redistribute across time; doing so would be a different estimator, + # not the inversion filter's smoother. if filter == :inversion && smooth - @info "The inversion filter does not support smoothing. Setting `smooth = false`." maxlog = maxlog + @info "The inversion filter identifies the state exactly, so its smoothed and filtered estimates coincide. Setting `smooth = false`." maxlog = maxlog smooth = false end diff --git a/src/common_docstrings.jl b/src/common_docstrings.jl index 58ad99994..840780b1c 100644 --- a/src/common_docstrings.jl +++ b/src/common_docstrings.jl @@ -13,13 +13,41 @@ const GENERALISED_IRF® = "`generalised_irf` [Default: `$(DEFAULT_GENERALISED_IR const GENERALISED_IRF_WARMUP_ITERATIONS® = "`generalised_irf_warmup_iterations` [Default: `$(DEFAULT_GENERALISED_IRF_WARMUP)`, Type: `Int`]: number of warm-up iterations used to draw the baseline paths in the generalised IRF simulation. Only applied when `generalised_irf = true`." const GENERALISED_IRF_DRAWS® = "`generalised_irf_draws` [Default: `$(DEFAULT_GENERALISED_IRF_DRAWS)`, Type: `Int`]: number of Monte Carlo draws used to compute the generalised IRF. Only applied when `generalised_irf = true`." const ALGORITHM® = "`algorithm` [Default: `$(DEFAULT_ALGORITHM)`, Type: `Symbol`]: algorithm to solve for the dynamics of the model. Available algorithms: `:first_order`, `:second_order`, `:pruned_second_order`, `:third_order`, `:pruned_third_order`" -const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error_std`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. Smoothing (`smooth = true`) is available for the Kalman filter (Durbin-Koopman) and the particle filters (fixed-interval smoothing along the filter genealogy), but not for the inversion filter. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." +const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. Smoothing (`smooth = true`) is available for the Kalman filter (Durbin-Koopman) and the particle filters (fixed-interval smoothing along the filter genealogy), but not for the inversion filter. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." const LEVELS® = "return levels or absolute deviations from the relevant steady state corresponding to the solution algorithm (e.g. stochastic steady state for higher order solution algorithms)." const CONDITIONS® = "`conditions` [Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}}`]: conditions for which to find the corresponding shocks. The input can have multiple formats, but for all types of entries, the first dimension corresponds to variables and the second dimension to the number of periods. The conditions can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the conditions are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as conditions. Note that conditioning variables to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input conditions is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as conditions and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of variables (of type `Symbol` or `String`) for which conditions are specified can be included and all other variables are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the conditions for the specified variables bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const SHOCK_CONDITIONS® = "`shocks` [Default: `nothing`, Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}, Nothing}`]: known values of shocks. This argument allows including certain shock values. By entering restrictions on the shocks in this way the problem to match the conditions on endogenous variables is restricted to the remaining free shocks in the respective period. The input can have multiple formats, but for all types of entries, the first dimension corresponds to shocks and the second dimension to the number of periods. `shocks` can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the shocks are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as certain shock values. Note that conditioning shocks to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input known shocks is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as known shocks and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of shocks (of type `Symbol` or `String`) for which values are specified can be included and all other shocks are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the values for the specified shocks bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const PARAMETER_DERIVATIVES® = "`parameter_derivatives` [Default: :all]: parameters for which to calculate partial derivatives. Inputs can be a parameter name passed on as either a `Symbol` or `String` (e.g. `:alpha`, or \"alpha\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. `:all` will include all parameters." const DATA® = "`data` [Type: `KeyedArray`]: data matrix with variables (`String` or `Symbol`) in rows and periods in columns. Periods can have any format and will be used for the output. `missing` and `nothing` entries are treated as unobserved observations, and non-finite numeric entries are handled the same way. Interior unobserved entries remain in the sample and are handled period by period using only the available observations, while fully unobserved leading and trailing periods are removed automatically with an informational message. `KeyedArray` is provided by the `AxisKeys` package." -const SMOOTH® = "`smooth` [Default: selector that enables smoothing when `filter = $(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` and disables it otherwise, Type: `Bool`]: whether to return smoothed (`true`) or filtered (`false`) shocks/variables. Smoothing is only available for the Kalman filter. The inversion filter only returns filtered shocks/variables, so the default turns smoothing off in that case." +const SMOOTH® = "`smooth` [Default: selector that enables smoothing when `filter = $(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` and disables it otherwise, Type: `Bool`]: whether to return smoothed (`true`) or filtered (`false`) shocks/variables. Smoothing is available for the Kalman filter (Durbin-Koopman) and for the particle filters (fixed-interval smoothing along the filter genealogy). The inversion filter identifies the state exactly, so its smoothed and filtered estimates coincide and the default turns smoothing off in that case." + +# ── Measurement error and particle-filter keywords ─────────────────────────── +# These appear on every function that takes a `filter`, so they live here rather +# than being restated in each docstring. `PARTICLE_FILTER_KEYWORDS®` is the whole +# block as consecutive bullet points, for functions that accept all of them. +const MEASUREMENT_ERROR® = "`measurement_error` [Default: `$(repr(DEFAULT_MEASUREMENT_ERROR))`, Type: `Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}}`]: covariance `H` of the Gaussian measurement error on the observables, `yₜ = C xₜ + ηₜ` with `ηₜ ~ N(0, H)`. This is a **variance**, not a standard deviation: a scalar is the common variance of every observable, a vector supplies one variance per observable, and a matrix is the full (possibly correlated) covariance. `:auto` resolves per filter: no measurement error for the Kalman and inversion filters, and `($(DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION) * sᵢ)^2` per observable — `sᵢ` being that observable's sample standard deviation — for the particle filters, which are degenerate without it. Measurement error is supported by the Kalman filter and required by the particle filters; it is not available for the inversion filter, which reproduces the observables exactly." +const N_PARTICLES® = "`n_particles` [Default: `$(DEFAULT_N_PARTICLES)`, Type: `Int`]: number of particles used by the particle filters. More particles reduce the Monte-Carlo variance of the likelihood at roughly linear cost." +const PARTICLE_RESAMPLING® = "`particle_resampling` [Default: `:$(DEFAULT_PARTICLE_RESAMPLING)`, Type: `Symbol`]: resampling scheme. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`." +const PARTICLE_RESAMPLING_THRESHOLD® = "`particle_resampling_threshold` [Default: `$(DEFAULT_PARTICLE_RESAMPLING_THRESHOLD)`, Type: `Real`]: resample whenever the effective sample size falls below `particle_resampling_threshold * n_particles`." +const PARTICLE_INITIAL_STATE_SCALING® = "`particle_initial_state_scaling` [Default: `$(DEFAULT_PARTICLE_INITIAL_STATE_SCALING)`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state." +const PARTICLE_RNG® = "`particle_rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filters (pass a seeded RNG for reproducible results)." +const TEMPERING_TARGET_RATIO® = "`tempering_target_ratio` [Default: `$(DEFAULT_TEMPERING_TARGET_RATIO)`, Type: `Real`]: target inefficiency ratio that sets the tempering schedule of `filter = :tempered_particle`." +const TEMPERING_MH_STEPS® = "`tempering_mh_steps` [Default: `$(DEFAULT_TEMPERING_MH_STEPS)`, Type: `Int`]: number of Metropolis-Hastings mutation steps per tempering stage." +const TEMPERING_MAX_STAGES® = "`tempering_max_stages` [Default: `$(DEFAULT_TEMPERING_MAX_STAGES)`, Type: `Int`]: cap on the number of tempering stages per period." +const TEMPERING_MH_SCALE® = "`tempering_mh_scale` [Default: `$(DEFAULT_TEMPERING_MH_SCALE)`, Type: `Real`]: scale of the random-walk Metropolis-Hastings proposal used in the mutation step." +const TEMPERING_KEYWORDS® = join(("- " * TEMPERING_TARGET_RATIO®, + "- " * TEMPERING_MH_STEPS®, + "- " * TEMPERING_MAX_STAGES®, + "- " * TEMPERING_MH_SCALE®), "\n") +const PARTICLE_FILTER_KEYWORDS® = join(("- " * MEASUREMENT_ERROR®, + "- " * N_PARTICLES®, + "- " * PARTICLE_RESAMPLING®, + "- " * PARTICLE_RESAMPLING_THRESHOLD®, + "- " * PARTICLE_INITIAL_STATE_SCALING®, + "- " * PARTICLE_RNG®, + TEMPERING_KEYWORDS®), "\n") +const ON_FAILURE_LOGLIKELIHOOD® = "`on_failure_loglikelihood` [Default: selector that returns `-1e6` for the particle filters and `-Inf` otherwise, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails (e.g. the solution did not converge). The particle filters default to a large finite penalty rather than `-Inf` because they can fail for purely stochastic reasons, and `-Inf` would kill a sampler's chain state instead of rejecting a single proposal." +const INITIAL_COVARIANCE® = "`initial_covariance` [Default: `:theoretical`, Type: `Union{Symbol,AbstractMatrix{<:Real}}`]: how to initialise the filter's state covariance (for the particle filters, the covariance the initial cloud is drawn from). `:theoretical` uses the first-order ergodic values from the Lyapunov equation, `:diagonal` starts diffuse with 10.0 along the diagonal, or supply a matrix of the appropriate size." const DATA_IN_LEVELS® = "`data_in_levels` [Default: `$(DEFAULT_DATA_IN_LEVELS)`, Type: `Bool`]: indicator whether the data is provided in levels. If `true` the input to the data argument will have the non-stochastic steady state subtracted." const LYAPUNOV® = "`lyapunov_algorithm` [Default: `$(DEFAULT_LYAPUNOV_ALGORITHM)`, Type: `Symbol`]: algorithm to solve Lyapunov equation (`A * X * A' + C = X`). Available algorithms: `:doubling`, `:bartels_stewart`, `:bicgstab`, `:gmres`, `:dqgmres`" const SYLVESTER® = "`sylvester_algorithm` [Default: selector that uses `$(DEFAULT_SYLVESTER_ALGORITHM)` for smaller problems and switches to `$(DEFAULT_LARGE_SYLVESTER_ALGORITHM)` for larger problems, Type: `Union{Symbol,Vector{Symbol},Tuple{Symbol,Vararg{Symbol}}}`]: algorithm to solve the Sylvester equation (`A * X * B + C = X`). Available algorithms: `:doubling`, `:bartels_stewart`, `:bicgstab`, `:dqgmres`, `:gmres`. Input argument can contain up to two elements in a `Vector` or `Tuple`. The first (second) element corresponds to the second (third) order perturbation solutions' Sylvester equation. If only one element is provided it corresponds to the second order perturbation solutions' Sylvester equation." diff --git a/src/default_options.jl b/src/default_options.jl index 1a8607f2e..9eb26ebf3 100644 --- a/src/default_options.jl +++ b/src/default_options.jl @@ -22,14 +22,27 @@ const PARTICLE_FILTER_VARIANT = Dict(:bootstrap_particle => :bootstrap, :tempered_particle => :tempered) # ── Measurement error ──────────────────────────────────────────────────────── +# `measurement_error` is the covariance H of ηₜ ~ N(0, H) in yₜ = C xₜ + ηₜ. It is +# *not* a standard deviation: a scalar is the common variance of every observable, +# a vector the per-observable variances, and a matrix the full covariance. # `:auto` resolves per filter: no measurement error for the Kalman and inversion # filters (their historical behaviour), and a small data-driven value for the # particle filters, which are degenerate without it. -const DEFAULT_MEASUREMENT_ERROR_STD = :auto -# Auto measurement-error standard deviation as a fraction of each observable's -# sample standard deviation. +const DEFAULT_MEASUREMENT_ERROR = :auto +# Auto measurement-error *standard deviation* as a fraction of each observable's +# sample standard deviation (squared into a variance before it reaches a filter). +# 0.1 puts ~1% of each observable's variance into measurement error: enough to +# keep the particle weights well spread on a Smets-Wouters-sized problem, small +# enough that the likelihood still reflects the model rather than the noise. const DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION = 0.1 +# `-Inf` is the right failure value for the deterministic filters: it tells a +# sampler the draw is impossible. A particle filter, though, can fail for purely +# stochastic reasons (every particle far off in one period), and an `-Inf` there +# would permanently kill the chain state rather than just reject one proposal — +# so it returns a large finite penalty instead. +const DEFAULT_ON_FAILURE_LOGLIKELIHOOD_SELECTOR = filter -> get(PARTICLE_FILTER_ALIASES, filter, filter) ∈ PARTICLE_FILTERS ? -1e6 : -Inf + # ── Particle filter defaults (see `src/filter/particle.jl`) ────────────────── # 10_000 particles keeps a Smets-Wouters-sized problem (7 observables, ~180 # periods) accurate to a couple of log-likelihood points in well under a second diff --git a/src/filter/kalman.jl b/src/filter/kalman.jl index ed2f7488e..885f7afc5 100644 --- a/src/filter/kalman.jl +++ b/src/filter/kalman.jl @@ -16,7 +16,7 @@ function calculate_loglikelihood(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -61,7 +61,7 @@ function calculate_loglikelihood(::Val{:kalman}, # initial_state at the get_loglikelihood level. u₀ = state[1][observables_and_states] - return run_kalman_iterations(A, 𝐁, C, P, data_in_deviations, kalman_ws, u₀, presample_periods = presample_periods, verbose = opts.verbose, on_failure_loglikelihood = on_failure_loglikelihood, measurement_error_variances = measurement_error_variances) + return run_kalman_iterations(A, 𝐁, C, P, data_in_deviations, kalman_ws, u₀, presample_periods = presample_periods, verbose = opts.verbose, on_failure_loglikelihood = on_failure_loglikelihood, measurement_error = measurement_error) # timer = timer, end @@ -81,7 +81,7 @@ function calculate_loglikelihood_with_missing(::Val{:kalman}, filter_algorithm::Symbol = :LagrangeNewton, lyapunov_algorithm::Symbol = :doubling, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, opts::CalculationOptions = merge_calculation_options())::S where {S <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) T = constants.post_model_macro @@ -113,7 +113,7 @@ function calculate_loglikelihood_with_missing(::Val{:kalman}, presample_periods = presample_periods, verbose = opts.verbose, on_failure_loglikelihood = on_failure_loglikelihood, - measurement_error_variances = measurement_error_variances) + measurement_error = measurement_error) end # Specialization for :theoretical @@ -144,6 +144,11 @@ function get_initial_covariance(::Val{:diagonal}, end +# `measurement_error` is the covariance H of the Gaussian measurement error in +# yₜ = C xₜ + ηₜ, ηₜ ~ N(0, H). It is *not* a standard deviation: a vector is read +# as the per-observable variances (the diagonal of H), a matrix as the full +# covariance H. `nothing` means no measurement error. It enters the filter only +# through the innovation covariance, F = C P C' + H. function run_kalman_iterations(A::Matrix{S}, 𝐁::Matrix{S}, C::AbstractMatrix{R}, @@ -153,7 +158,7 @@ function run_kalman_iterations(A::Matrix{S}, u₀::AbstractVector{V}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, # timer::TimerOutput = TimerOutput(), verbose::Bool = false) where {S <: Real, R <: Real, V <: Real, U <: AbstractFloat} presample_periods = normalize_presample_periods(presample_periods, size(data_in_deviations, 2)) @@ -215,14 +220,14 @@ function run_kalman_iterations(A::Matrix{S}, # vector of per-observable variances (diagonal H, the common case) or a # full covariance matrix; both are in the innovation (data-row) order, # which matches F's rows and columns. - if measurement_error_variances !== nothing - if measurement_error_variances isa AbstractMatrix + if measurement_error !== nothing + if measurement_error isa AbstractMatrix @inbounds for j in 1:n_obs, i in 1:n_obs - F[i, j] += measurement_error_variances[i, j] + F[i, j] += measurement_error[i, j] end else @inbounds for i in 1:n_obs - F[i, i] += measurement_error_variances[i] + F[i, i] += measurement_error[i] end end end @@ -309,6 +314,9 @@ end # Uses the same workspace buffers but takes per-period sub-views of size m_t # (= number of observed variables in period t). Periods with m_t == 0 become # pure predict steps (no update, no likelihood contribution). +# `measurement_error` carries the same meaning as in `run_kalman_iterations`: the +# covariance H (vector ⇒ per-observable variances, matrix ⇒ full covariance), not +# a standard deviation. Here it is subset to the observed rows of period t. function run_kalman_iterations_missing(A::Matrix{S}, 𝐁::Matrix{S}, C::AbstractMatrix{R}, @@ -319,7 +327,7 @@ function run_kalman_iterations_missing(A::Matrix{S}, u₀::AbstractVector{<:Real}; presample_periods::Int = 0, on_failure_loglikelihood::U = -Inf, - measurement_error_variances::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Nothing,AbstractVector{<:Real},AbstractMatrix{<:Real}} = nothing, verbose::Bool = false)::S where {S <: Float64, R <: Real, U <: AbstractFloat} n_obs = size(C, 1) @@ -383,14 +391,14 @@ function run_kalman_iterations_missing(A::Matrix{S}, # Add the measurement-error covariance restricted to the observed rows # (the conditional block H[idx, idx] of a full covariance matrix). - if measurement_error_variances !== nothing - if measurement_error_variances isa AbstractMatrix + if measurement_error !== nothing + if measurement_error isa AbstractMatrix @inbounds for j in 1:m, i in 1:m - Fv[i, j] += measurement_error_variances[idx[i], idx[j]] + Fv[i, j] += measurement_error[idx[i], idx[j]] end else @inbounds for i in 1:m - Fv[i, i] += measurement_error_variances[idx[i]] + Fv[i, i] += measurement_error[idx[i]] end end end diff --git a/src/filter/particle.jl b/src/filter/particle.jl index b6d822216..20973a63f 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -4,19 +4,35 @@ # # The measurement equation is yₜ = full_stateₜ[observables] + ηₜ, ηₜ ~ N(0, H). # -# H is taken to be diagonal here. Nothing in the algorithm requires that — the -# filters only ever need H⁻¹ and log det H, so a full covariance would just mean -# replacing the elementwise quadratic form below by a triangular solve against a -# Cholesky factor of H (cached per missing-data pattern). It is kept diagonal -# because that is what the elementwise inner loop is optimised for, and because -# correlated measurement error in a DSGE is more naturally written into the model -# itself: adding measurement-error processes to the observation equations makes -# the correlation part of the state transition and leaves H diagonal again. The -# Kalman filter, whose innovation covariance is formed as a matrix anyway, does -# accept an arbitrary `measurement_error_covariance`. The structural shocks -# are i.i.d. standard normal (their standard deviations are baked into the -# solution matrices 𝐒), and the state transition is the perturbation solution's -# `state_update` (first order through pruned third order). +# H is a *covariance*, not a standard deviation: a vector argument is read as the +# per-observable variances, a matrix as the full covariance. Both are supported. +# The filters only ever need H⁻¹ and log det H, so the diagonal case is an +# elementwise loop and the correlated case a triangular solve against a Cholesky +# factor of H restricted to the rows observed in the period, cached per +# missing-data pattern (see `DenseMeasurementError` below). Diagonal is the +# default and the fast path, and is also the more natural modelling choice: a +# persistent, correlated measurement error is usually better written into the +# model itself as measurement-error processes in the observation equations, which +# moves the correlation into the state transition and leaves H diagonal again. +# +# Why the *Kalman* filter needs H at all — it has no degeneracy problem the way a +# particle filter does, and works fine with H = 0. Three reasons it is offered: +# (1) Stochastic singularity. With more observables than structural shocks, the +# model-implied observables lie on a lower-dimensional manifold, C P C' is +# singular, and the likelihood is undefined. H > 0 is the standard fix, and +# what lets a 7-observable model be estimated with fewer than 7 shocks. +# (2) Model misspecification / data revisions. Smets-Wouters-style estimations +# routinely put measurement error on a subset of observables so that series +# the model cannot hope to match exactly do not dominate the likelihood. +# (3) It is what makes the particle filters checkable. A particle filter needs +# H > 0, so validating one against the exact likelihood on a linear model +# requires the Kalman filter to score the *same* H. That comparison is the +# sharpest correctness test available and is what `test_particle_filter.jl` +# and `test_particle_filter_sw07.jl` do. +# +# The structural shocks are i.i.d. standard normal (their standard deviations are +# baked into the solution matrices 𝐒), and the state transition is the +# perturbation solution's `state_update` (first order through pruned third order). # # Three variants are provided, each selected by its own `filter` value: # :bootstrap_particle — sequential importance resampling, i.e. the bootstrap @@ -179,8 +195,22 @@ end # `:theoretical` (default) uses the first-order ergodic (unconditional) state # covariance Σ solving the discrete Lyapunov equation Σ = A Σ A' + B B' (built # from the cached first-order solution, the usual choice for a stationary model); -# `:diagonal` uses 10·I; an -# nVars×nVars matrix is used directly. +# `:diagonal` uses 10·I; an nVars×nVars matrix is used directly. +# +# Σ is deliberately the *first-order* covariance at every perturbation order, and +# does not follow `algorithm`. Elsewhere in the package `:theoretical` is only +# ever reached from the Kalman filter (`get_initial_covariance` in kalman.jl), +# which is first-order-only, so there is no precedent either way — the choice is +# specific to this file. Order-consistent ergodic covariances do exist +# (`calculate_second_order_moments_with_covariance` and the third-order routines +# in moments.jl), but they are expensive, live in the augmented pruned-state basis +# rather than the nVars basis the cloud needs, and buy very little: Σ only seeds +# period 1, and the filter forgets it within a handful of periods (which is what +# `presample_periods` is for). Higher-order terms also perturb the *mean* of the +# ergodic distribution, not just its spread, and that shift is already carried by +# `state` from `get_relevant_steady_state_and_state_update`. Pass an explicit +# matrix, or widen the cloud with `particle_initial_state_scaling`, if the +# first-order spread is too tight for a strongly nonlinear model. function particle_initial_state_covariance(𝓂::ℳ, T, opts::CalculationOptions, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}}) nVars = T.nVars @@ -261,6 +291,93 @@ end end +# ── Non-diagonal measurement error ─────────────────────────────────────────── +# Everything above takes `me_var`, the diagonal of H, and reads it elementwise — +# the fast path, and the default. A correlated H needs the same two quantities, +# vᵀH⁻¹v and log det H, but restricted to the rows observed in the period. Both +# come from one Cholesky factor of H[rows, rows], so we factorise once per +# missing-data pattern and cache it; with complete data that is a single +# factorisation for the whole sample. `DenseMeasurementError` is then accepted +# anywhere `me_var` is, by dispatch, leaving the diagonal path untouched. +struct DenseMeasurementError + H::Matrix{Float64} + factors::Dict{Vector{Int},Tuple{ℒ.Cholesky{Float64,Matrix{Float64}},Float64}} + buf::Vector{Float64} # innovation scratch, sized to the number of observables +end + +function DenseMeasurementError(H::AbstractMatrix{<:Real}) + Hf = Matrix{Float64}(H) + return DenseMeasurementError(Hf, + Dict{Vector{Int},Tuple{ℒ.Cholesky{Float64,Matrix{Float64}},Float64}}(), + Vector{Float64}(undef, size(Hf, 1))) +end + +# Cholesky of H[rows, rows] and log det H[rows, rows], memoised on the row pattern. +@inline function me_factor(me::DenseMeasurementError, rows) + key = collect(Int, rows) + cached = get(me.factors, key, nothing) + cached === nothing || return cached + Hsub = me.H[key, key] + F = ℒ.cholesky(ℒ.Symmetric(Hsub)) + ld = 2 * sum(log, ℒ.diag(F.U)) + me.factors[key] = (F, ld) + return (F, ld) +end + +# vᵀH⁻¹v over the observed rows. `Inf` on a non-finite prediction, matching the +# diagonal version's contract (an impossible particle gets zero weight). +@inline function particle_quadratic_form(full::AbstractVector, data_col, observables_index, + me::DenseMeasurementError, rows) + F, _ = me_factor(me, rows) + v = @view me.buf[1:length(rows)] + @inbounds for (k, r) in enumerate(rows) + f = full[observables_index[r]] + isfinite(f) || return Inf + v[k] = data_col[r] - f + end + ℒ.ldiv!(F.L, v) # v ← L⁻¹v, so ‖v‖² = original vᵀH⁻¹v + return sum(abs2, v) +end + +@inline function particle_measurement_logZ(me::DenseMeasurementError, rows, log2pi::Float64) + _, ld = me_factor(me, rows) + return -0.5 * (length(rows) * log2pi + ld) +end + +@inline function particle_log_measurement_density(full::AbstractVector, data_col, observables_index, + me::DenseMeasurementError, rows, log2pi::Float64) + q = particle_quadratic_form(full, data_col, observables_index, me, rows) + isfinite(q) || return -Inf + return particle_measurement_logZ(me, rows, log2pi) - 0.5 * q +end + +# The diagonal of H: what the auxiliary filter's first-stage preview needs (it +# only has to be a rough predictive variance — the second-stage reweighting is +# exact whatever the preview used). +me_diagonal(me_var::AbstractVector) = me_var +me_diagonal(me::DenseMeasurementError) = ℒ.diag(me.H) + +# Elementwise reciprocals for the batched first-order path; the dense filter +# keeps its factorisation instead and is passed through unchanged. +me_inverse_diagonal(me_var::AbstractVector) = 1.0 ./ me_var +me_inverse_diagonal(me::DenseMeasurementError) = me + +# Build the measurement-error representation the kernels take from whatever +# `resolve_measurement_error` produced. +build_particle_measurement_error(me::AbstractVector{<:Real}) = Float64.(me) +build_particle_measurement_error(me::AbstractMatrix{<:Real}) = ℒ.isdiag(me) ? collect(Float64, ℒ.diag(me)) : DenseMeasurementError(me) + +# Positivity check shared by all kernels. +function assert_positive_measurement_error(me::AbstractVector) + @assert all(x -> x > 0, me) "The particle filters require strictly positive measurement-error variances for every observable." + return nothing +end +function assert_positive_measurement_error(me::DenseMeasurementError) + @assert ℒ.isposdef(ℒ.Symmetric(me.H)) "The particle filters require a positive definite measurement-error covariance." + return nothing +end + + # ── Allocation-free higher-order transitions ───────────────────────────────── # In-place transitions for the nonlinear orders, mirroring the closures built by # `parse_algorithm_to_state_update` but writing into a preallocated `out` with @@ -442,7 +559,7 @@ function run_particle_filter(::Val{algo}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -469,8 +586,8 @@ function run_particle_filter(::Val{algo}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) past_idx = T.past_not_future_and_mixed_idx 𝐒f = [Matrix{Float64}(S) for S in 𝐒] @@ -660,7 +777,7 @@ function run_particle_filter(::Val{algo}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -687,8 +804,8 @@ function run_particle_filter(::Val{algo}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) past_idx = T.past_not_future_and_mixed_idx 𝐒f = [Matrix{Float64}(S) for S in 𝐒] @@ -701,7 +818,7 @@ function run_particle_filter(::Val{algo}, # shock-driven; inflating by the shock spread keeps the proxy well-conditioned. nPast = T.nPast_not_future_and_mixed S₁cache = 𝓂.caches.first_order_solution_matrix - pred_var = Float64[sum(abs2, @view S₁cache[observables_index[i], nPast+1:end]) for i in eachindex(observables_index)] .+ me_var + pred_var = Float64[sum(abs2, @view S₁cache[observables_index[i], nPast+1:end]) for i in eachindex(observables_index)] .+ me_diagonal(me_var) Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) @@ -858,7 +975,7 @@ function run_particle_filter(::Val{algo}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -885,8 +1002,8 @@ function run_particle_filter(::Val{algo}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) r_star = Float64(tempering_target_ratio) c = Float64(tempering_mh_scale) @@ -1176,6 +1293,21 @@ end return q end +# Same, for a correlated H: gather the innovation, then one triangular solve. +@inline function linear_quadform_col(X::Matrix{Float64}, p::Int, data_col, observables_index, + me::DenseMeasurementError, rows) + F, _ = me_factor(me, rows) + v = @view me.buf[1:length(rows)] + @inbounds for k in eachindex(rows) + r = rows[k] + f = X[observables_index[r], p] + isfinite(f) || return Inf + v[k] = data_col[r] - f + end + ℒ.ldiv!(F.L, v) + return sum(abs2, v) +end + # Copy column `src` of `X` into column `dst` of `Y` (contiguous, allocation-free). @inline function copy_col!(Y::Matrix{Float64}, dst::Int, X::Matrix{Float64}, src::Int) @inbounds for i in axes(X, 1) @@ -1203,7 +1335,7 @@ function run_particle_filter(::Val{:first_order}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -1230,9 +1362,9 @@ function run_particle_filter(::Val{:first_order}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." - inv_me_var = 1.0 ./ me_var + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) + inv_me_var = me_inverse_diagonal(me_var) tr = build_linear_particle_transition(𝐒, T) Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) @@ -1309,7 +1441,7 @@ function run_particle_filter(::Val{:first_order}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -1336,16 +1468,17 @@ function run_particle_filter(::Val{:first_order}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." - inv_me_var = 1.0 ./ me_var + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) + inv_me_var = me_inverse_diagonal(me_var) tr = build_linear_particle_transition(𝐒, T) # Predictive variance of each observable (shock spread + measurement error). + me_diag = me_diagonal(me_var) pred_var = Vector{Float64}(undef, length(observables_index)) @inbounds for i in eachindex(observables_index) - pred_var[i] = sum(abs2, @view tr.B[observables_index[i], :]) + me_var[i] + pred_var[i] = sum(abs2, @view tr.B[observables_index[i], :]) + me_diag[i] end inv_pred_var = 1.0 ./ pred_var @@ -1443,7 +1576,7 @@ function run_particle_filter(::Val{:first_order}, constants::constants, state, 𝓂::ℳ, - measurement_error_variances::AbstractVector{<:Real}, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t::Vector{Vector{Int}}, has_missing::Bool; n_particles::Int = DEFAULT_N_PARTICLES, @@ -1470,9 +1603,9 @@ function run_particle_filter(::Val{:first_order}, presample_periods = normalize_presample_periods(presample_periods, nT) log2pi = log(2π) - me_var = Float64.(measurement_error_variances) - @assert all(x -> x > 0, me_var) "The particle filter requires strictly positive measurement-error variances for every observable." - inv_me_var = 1.0 ./ me_var + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) + inv_me_var = me_inverse_diagonal(me_var) r_star = Float64(tempering_target_ratio) c = Float64(tempering_mh_scale) @@ -1637,7 +1770,7 @@ end warmup_iterations::Int = 0, opts::CalculationOptions = merge_calculation_options(), smooth::Bool = true, - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -1662,16 +1795,10 @@ end obs_idx_per_t, has_missing = build_obs_index(dat) nT = size(dat, 2) - # measurement error: same `:auto` convention as the likelihood path - me_std = measurement_error_std === :auto ? - DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION .* vec(sqrt.(sum(abs2, dat .- sum(dat, dims = 2) ./ nT, dims = 2) ./ max(nT - 1, 1))) : - measurement_error_std - me_var = me_std isa AbstractVector ? collect(float.(me_std)) .^ 2 : fill(float(me_std)^2, length(observables)) - @inbounds for i in eachindex(me_var) - if !(isfinite(me_var[i])) || me_var[i] <= 0 - me_var[i] = (DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION)^2 - end - end + # `measurement_error` arrives already resolved (a variance vector or a + # covariance matrix) — `:auto` is handled by the user-facing entry points. + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) # solution matrices and the initial state, exactly as the likelihood path builds them _, _, 𝐒, state, solved = get_relevant_steady_state_and_state_update(Val(algo), 𝓂.parameter_values, 𝓂, opts = opts) diff --git a/src/get_functions.jl b/src/get_functions.jl index b34cda93d..69434f3ad 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -297,7 +297,7 @@ And data, 4×2×40 Array{Float64, 3}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -345,9 +345,9 @@ And data, 4×2×40 Array{Float64, 3}: data_in_deviations = prepare_trimmed_data_in_deviations(data, 𝓂, NSSS; data_in_levels = data_in_levels) extra_kw = marginal_contribution ? (; marginal_contribution = true) : NamedTuple() - warn_unused_measurement_error(filter, measurement_error_std) + warn_unused_measurement_error(filter, measurement_error) if filter ∈ PARTICLE_FILTERS - extra_kw = merge(extra_kw, (; measurement_error_std, n_particles, particle_resampling, + extra_kw = merge(extra_kw, (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng)) end @@ -454,7 +454,7 @@ And data, 1×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -503,10 +503,10 @@ And data, 1×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Shocks = axis1, Periods = 1:0) end - warn_unused_measurement_error(filter, measurement_error_std) + warn_unused_measurement_error(filter, measurement_error) particle_kw = filter ∈ PARTICLE_FILTERS ? - (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng) : NamedTuple() variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), @@ -593,7 +593,7 @@ And data, 4×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -643,10 +643,10 @@ And data, 4×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Variables = axis1, Periods = 1:0) end - warn_unused_measurement_error(filter, measurement_error_std) + warn_unused_measurement_error(filter, measurement_error) particle_kw = filter ∈ PARTICLE_FILTERS ? - (; measurement_error_std, n_particles, particle_resampling, particle_resampling_threshold, + (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng) : NamedTuple() variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), @@ -736,7 +736,7 @@ And data, 5×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -798,9 +798,13 @@ end """ $(SIGNATURES) -Return the standard deviations of the Kalman smoother or filter (depending on the `smooth` keyword argument) estimates of the model variables based on the provided data and first order solution of the model. For the default settings this function relies on the Kalman filter and therefore keeps smoothing enabled. Data is by default assumed to be in levels unless `data_in_levels` is set to `false`. +Return the standard deviations of the smoother or filter (depending on the `smooth` keyword argument) estimates of the model variables based on the provided data. For the default settings this function relies on the Kalman filter and therefore keeps smoothing enabled. Data is by default assumed to be in levels unless `data_in_levels` is set to `false`. -If occasionally binding constraints are present in the model, they are not taken into account here. +The Kalman filter reports the square root of the diagonal of its state covariance, which is exact for a first order solution. The particle filters (`filter = :bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`) instead report the weighted spread of the particle cloud, which is a Monte-Carlo estimate of the same quantity but valid at every perturbation order — so this is the way to get estimation uncertainty for a nonlinear solution. The inversion filter identifies the state exactly and therefore has no dispersion to report. + +Note that the *smoothed* particle spread (`smooth = true`) understates uncertainty in the early part of the sample: the smoother traces the filter's genealogy, and repeated resampling means the surviving ancestral lines coalesce, so few distinct trajectories remain that far back. Raise `n_particles`, or read the filtered spread (`smooth = false`), if the early periods matter. + +If occasionally binding constraints are present in the model, they are not taken into account here. # Arguments - $MODEL® @@ -808,8 +812,11 @@ If occasionally binding constraints are present in the model, they are not taken # Keyword Arguments - $PARAMETERS® - $STEADY_STATE_FUNCTION® +- $ALGORITHM® +- $FILTER® - $DATA_IN_LEVELS® - $SMOOTH® +$PARTICLE_FILTER_KEYWORDS® - $QME® - $LYAPUNOV® - $TOLERANCES® @@ -858,6 +865,14 @@ And data, 4×40 Matrix{Float64}: data::KeyedArray{D}; parameters::ParameterType = nothing, steady_state_function::SteadyStateFunctionType = missing, + algorithm::Symbol = DEFAULT_ALGORITHM, + filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, + n_particles::Int = DEFAULT_N_PARTICLES, + particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, + particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, + particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, + particle_rng::Random.AbstractRNG = Random.default_rng(), data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_FLAG, verbose::Bool = DEFAULT_VERBOSE, @@ -876,13 +891,24 @@ And data, 4×40 Matrix{Float64}: quadratic_matrix_equation_algorithm = quadratic_matrix_equation_algorithm, lyapunov_algorithm = lyapunov_algorithm) - algorithm = :first_order + # The inversion filter recovers the state exactly, so it has no dispersion to + # report. Everything else (Kalman, particle) does. + if filter == :inversion || (algorithm != :first_order && filter ∉ PARTICLE_FILTERS && filter != :kalman) + error("`get_estimated_variable_standard_deviations` needs a filter that reports estimation uncertainty. The inversion filter identifies the state exactly and has none. Use `filter = :kalman` (first order) or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`), which report the spread of the particle cloud and work at every perturbation order.") + end + + filter, smooth, algorithm, _, _, _ = normalize_filtering_options(filter, smooth, algorithm, false, 0) + + if filter == :inversion + error("`get_estimated_variable_standard_deviations` needs a filter that reports estimation uncertainty; `algorithm = :$(algorithm)` fell back to the inversion filter, which identifies the state exactly. Select a particle filter explicitly.") + end solve!(𝓂, parameters = parameters, steady_state_function = steady_state_function, opts = opts, - dynamics = true) + dynamics = true, + algorithm = algorithm) reference_steady_state, NSSS, SSS_delta = get_relevant_steady_states(𝓂, algorithm, opts = opts) @@ -896,9 +922,15 @@ And data, 4×40 Matrix{Float64}: return KeyedArray(zeros(eltype(NSSS), length(axis1), 0); Standard_deviations = axis1, Periods = 1:0) end - variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(:first_order), Val(:kalman), + warn_unused_measurement_error(filter, measurement_error) + particle_kw = filter ∈ PARTICLE_FILTERS ? + (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), + n_particles, particle_resampling, particle_resampling_threshold, + particle_initial_state_scaling, particle_rng) : NamedTuple() + + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), smooth = smooth, - opts = opts) + opts = opts; particle_kw...) if !use_workspaces; 𝓂.workspaces = orig_ws; end @@ -4341,28 +4373,32 @@ function get_statistics(𝓂::ℳ, return ret end -# The Kalman smoother path (`filter_and_smooth`) does not take measurement error, -# so a `measurement_error_std` supplied to the estimate entry points only has an -# effect for the particle filters. Say so rather than silently dropping it. -function warn_unused_measurement_error(filter::Symbol, measurement_error_std; maxlog::Int = DEFAULT_MAXLOG) - if filter ∉ PARTICLE_FILTERS && measurement_error_std !== DEFAULT_MEASUREMENT_ERROR_STD - @info "`measurement_error_std` is only used by the particle filters on this path; it is ignored for `filter = :$(filter)`. Use `get_loglikelihood` if you need measurement error in the Kalman likelihood." maxlog = maxlog +# The Kalman *likelihood* takes measurement error (H enters F = C P C' + H; see +# the header of filter/particle.jl for why that is worth having at all — it is +# what makes an over-identified observation set non-singular, absorbs +# misspecification, and lets the particle filters be validated against an exact +# likelihood under the same H). The Kalman *smoother* path (`filter_and_smooth`) +# does not take it: the Durbin-Koopman backward recursion would need H threaded +# through the disturbance smoother as well, which is not implemented. So a +# `measurement_error` supplied to the estimate entry points only has an effect for +# the particle filters. Say so rather than silently dropping it. +function warn_unused_measurement_error(filter::Symbol, measurement_error; maxlog::Int = DEFAULT_MAXLOG) + if filter ∉ PARTICLE_FILTERS && measurement_error !== DEFAULT_MEASUREMENT_ERROR + @info "`measurement_error` is only used by the particle filters on this path; it is ignored for `filter = :$(filter)`. Use `get_loglikelihood` if you need measurement error in the Kalman likelihood." maxlog = maxlog end return nothing end -# Resolve `measurement_error_std = :auto` for the filter-based `get_loglikelihood` -# path. The Kalman and inversion filters default to no measurement error (their -# historical behaviour). The particle filters are degenerate without measurement -# error — every particle would need to reproduce the observation exactly — so they -# default to a small fraction of each observable's sample standard deviation. -# This is a convenience default: for serious work set `measurement_error_std` -# (or estimate it) explicitly, since the likelihood level depends on it. -function resolve_auto_measurement_error_std(filter_choice::Symbol, data_in_deviations::AbstractMatrix) - filter_choice ∈ PARTICLE_FILTERS || return 0.0 - +# `:auto` measurement error: a fraction of each observable's sample standard +# deviation, returned as variances. The particle filters are degenerate without +# measurement error — every particle would have to reproduce the observation +# exactly — so they need *some* positive default to be usable out of the box, and +# a data-driven one is the only choice that works across models whose observables +# differ in scale by orders of magnitude. This is a convenience only: the +# likelihood level depends on H, so set (or estimate) it explicitly for real work. +function auto_measurement_error_variances(data_in_deviations::AbstractMatrix) n_obs = size(data_in_deviations, 1) - stds = Vector{Float64}(undef, n_obs) + variances = Vector{Float64}(undef, n_obs) @inbounds for i in 1:n_obs # sample standard deviation over the finite (observed) entries of row i @@ -4381,33 +4417,54 @@ function resolve_auto_measurement_error_std(filter_choice::Symbol, data_in_devia s = sqrt(acc / (n - 1)) end # fall back to a unit scale for a (near) constant or unobserved series - stds[i] = DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION * (isfinite(s) && s > 0 ? s : 1.0) + variances[i] = (DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION * (isfinite(s) && s > 0 ? s : 1.0))^2 end - return stds + return variances end -# Validate `measurement_error_std` supplied to the filter-based `get_loglikelihood` -# path and return the per-observable measurement-error variances (in the observable -# / data-row order), or `nothing` when no measurement error is active (all zero). -# A scalar is broadcast to all observables; a per-observable vector is used as is. -# Time-varying (matrix) measurement error is not yet supported on this path. -function build_filter_measurement_error_variances(measurement_error_std, n_obs::Int) - if measurement_error_std isa AbstractMatrix - error("Time-varying (matrix) `measurement_error_std` is not supported on the filter-based `get_loglikelihood` path; provide a scalar, a per-observable vector, or use `measurement_error_covariance` for a full covariance matrix.") +# Resolve the user-facing `measurement_error` into what the filter kernels take: +# `nothing` (no measurement error), a `Vector{Float64}` of per-observable +# variances, or a `Matrix{Float64}` covariance. The `:auto` convention lives here +# rather than in the kernels, so that every kernel receives a concrete H. +# +# `measurement_error` is the covariance H, never a standard deviation: a scalar is +# the common variance of all observables, a vector the per-observable variances, +# and a matrix the full covariance. +function resolve_measurement_error(filter::Symbol, measurement_error, data_in_deviations::AbstractMatrix) + n_obs = size(data_in_deviations, 1) + + if measurement_error === :auto + # The deterministic filters keep their historical behaviour (none). + filter ∈ PARTICLE_FILTERS || return nothing + return auto_measurement_error_variances(data_in_deviations) + end + + @assert !(measurement_error isa Symbol) "`measurement_error` must be `:auto`, a scalar variance, a vector of per-observable variances, or a covariance matrix; got `:$(measurement_error)`." + + if measurement_error isa AbstractMatrix + @assert size(measurement_error) == (n_obs, n_obs) "`measurement_error` given as a covariance matrix must be square with one row/column per observable ($n_obs); got $(size(measurement_error)). (A time-varying, per-period measurement error is not supported on the filter-based path.)" + H = Matrix{Float64}(measurement_error) + @assert all(isfinite, H) "`measurement_error` must contain only finite entries." + @assert isapprox(H, H', rtol = 1e-10) "`measurement_error` given as a covariance matrix must be symmetric." + H = (H + H') / 2 # symmetrise away round-off + @assert ℒ.isposdef(H) "`measurement_error` given as a covariance matrix must be positive definite." + # A diagonal covariance is just a variance vector; hand the kernels the + # cheaper representation so they take their elementwise fast path. + return ℒ.isdiag(H) ? collect(ℒ.diag(H)) : H end - stds = measurement_error_std isa AbstractVector ? collect(float.(measurement_error_std)) : fill(float(measurement_error_std), n_obs) + variances = measurement_error isa AbstractVector ? collect(float.(measurement_error)) : fill(float(measurement_error), n_obs) - @assert length(stds) == n_obs || length(stds) == 1 "`measurement_error_std` vector must have one entry per observable (got $(length(stds)), expected $n_obs) or a single entry that is broadcast to all observables." + @assert length(variances) == n_obs || length(variances) == 1 "`measurement_error` vector must have one entry per observable (got $(length(variances)), expected $n_obs) or a single entry that is broadcast to all observables." - if length(stds) == 1 && n_obs > 1 - stds = fill(stds[1], n_obs) + if length(variances) == 1 && n_obs > 1 + variances = fill(variances[1], n_obs) end - @assert all(s -> isfinite(s) && s >= 0, stds) "`measurement_error_std` entries must be finite and non-negative." + @assert all(v -> isfinite(v) && v >= 0, variances) "`measurement_error` entries are variances and must be finite and non-negative. (If you have standard deviations, square them.)" - return any(s -> s > 0, stds) ? stds .^ 2 : nothing + return any(v -> v > 0, variances) ? variances : nothing end """ @@ -4428,20 +4485,10 @@ If occasionally binding constraints are present in the model, they are not taken - $FILTER® - $WARMUP_ITERATIONS® - `presample_periods` [Default: `0`, Type: `Int`]: periods at the beginning of the retained data sample for which the loglikelihood is discarded. Values above the retained sample length are clamped down automatically with an informational message. -- `initial_covariance` [Default: `:theoretical`, Type: `Union{Symbol,AbstractMatrix{<:Real}}`]: defines the method to initialise the Kalman filters covariance matrix. It can be initialised with the theoretical long run values (option `:theoretical`), large values (10.0) along the diagonal (option `:diagonal`), or a user-supplied matrix of appropriate size (number of observables and states). +- $INITIAL_COVARIANCE® - $INITIAL_STATE® -- `on_failure_loglikelihood` [Default: `-Inf`, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails. Setting this to a finite value can avoid errors in codes that rely on finite loglikelihood values, such as e.g. slice samplers (in Pigeons.jl). -- `measurement_error_std` [Default: `:auto`, Type: `Union{Symbol,Real,AbstractVector{<:Real}}`]: standard deviation of Gaussian measurement error on the observables. A scalar is broadcast to all observables; a vector supplies one entry per observable. `:auto` resolves per filter: no measurement error for the Kalman and inversion filters, and $(DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION) times each observable's sample standard deviation for the particle filters, which are degenerate without it. Measurement error is supported by the Kalman filter and required by the particle filters; it is not available for the inversion filter, which reproduces the observables exactly. -- `measurement_error_covariance` [Default: `nothing`, Type: `Union{Nothing,AbstractMatrix{<:Real}}`]: full measurement-error covariance matrix (one row/column per observable), for correlated measurement error. Supersedes `measurement_error_std` when supplied. Must be symmetric positive definite. The Kalman filter supports an arbitrary covariance; the particle filters currently require it to be diagonal — correlated measurement error can instead be modelled structurally by adding measurement-error processes to the observation equations. -- `n_particles` [Default: `$(DEFAULT_N_PARTICLES)`, Type: `Int`]: number of particles used by the particle filters. More particles reduce the Monte-Carlo variance of the likelihood at roughly linear cost. -- `particle_resampling` [Default: `:$(DEFAULT_PARTICLE_RESAMPLING)`, Type: `Symbol`]: resampling scheme. One of `:systematic`, `:stratified`, `:multinomial`, `:residual`. -- `particle_resampling_threshold` [Default: `$(DEFAULT_PARTICLE_RESAMPLING_THRESHOLD)`, Type: `Real`]: resample whenever the effective sample size falls below `particle_resampling_threshold * n_particles`. -- `particle_initial_state_scaling` [Default: `$(DEFAULT_PARTICLE_INITIAL_STATE_SCALING)`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state. -- `particle_rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filters (pass a seeded RNG for reproducible likelihoods). -- `tempering_target_ratio` [Default: `$(DEFAULT_TEMPERING_TARGET_RATIO)`, Type: `Real`]: target inefficiency ratio that sets the tempering schedule of `filter = :tempered_particle`. -- `tempering_mh_steps` [Default: `$(DEFAULT_TEMPERING_MH_STEPS)`, Type: `Int`]: number of Metropolis-Hastings mutation steps per tempering stage. -- `tempering_max_stages` [Default: `$(DEFAULT_TEMPERING_MAX_STAGES)`, Type: `Int`]: cap on the number of tempering stages per period. -- `tempering_mh_scale` [Default: `$(DEFAULT_TEMPERING_MH_SCALE)`, Type: `Real`]: scale of the random-walk Metropolis-Hastings proposal used in the mutation step. +- $ON_FAILURE_LOGLIKELIHOOD® +$PARTICLE_FILTER_KEYWORDS® - $QME® - $SYLVESTER® - $LYAPUNOV® @@ -4485,13 +4532,12 @@ function get_loglikelihood(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - on_failure_loglikelihood::U = -Inf, + on_failure_loglikelihood::U = DEFAULT_ON_FAILURE_LOGLIKELIHOOD_SELECTOR(filter), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, - measurement_error_covariance::Union{Nothing,AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -4520,8 +4566,7 @@ function get_loglikelihood(𝓂::ℳ, presample_periods = presample_periods, initial_covariance = initial_covariance, filter_algorithm = filter_algorithm, - measurement_error_std = measurement_error_std, - measurement_error_covariance = measurement_error_covariance, + measurement_error = measurement_error, n_particles = n_particles, particle_resampling = particle_resampling, particle_resampling_threshold = particle_resampling_threshold, @@ -4547,13 +4592,12 @@ function get_loglikelihood(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - on_failure_loglikelihood::U = -Inf, + on_failure_loglikelihood::U = DEFAULT_ON_FAILURE_LOGLIKELIHOOD_SELECTOR(filter), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, - measurement_error_covariance::Union{Nothing,AbstractMatrix{<:Real}} = nothing, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, @@ -4699,38 +4743,19 @@ function get_loglikelihood(𝓂::ℳ, is_particle_filter = filter ∈ PARTICLE_FILTERS - # Diagonal Gaussian measurement-error variances (per observable, in data-row - # order), or `nothing` when no measurement error is active. Supported by the - # Kalman and particle filters; the inversion filter recovers shocks exactly - # and does not admit measurement error. `:auto` resolves per filter. - resolved_measurement_error_std = measurement_error_std === :auto ? - resolve_auto_measurement_error_std(filter, data_in_deviations) : measurement_error_std - - @assert !(resolved_measurement_error_std isa Symbol) "`measurement_error_std` must be `:auto`, a scalar, or a per-observable vector; got `:$(resolved_measurement_error_std)`." - - measurement_error_variances = if measurement_error_covariance !== nothing - # A full covariance matrix supersedes the per-observable standard deviations. - n_obs_me = size(data_in_deviations, 1) - @assert size(measurement_error_covariance) == (n_obs_me, n_obs_me) "`measurement_error_covariance` must be a square matrix with one row/column per observable ($(n_obs_me)); got $(size(measurement_error_covariance))." - H = Matrix{Float64}(measurement_error_covariance) - @assert all(isfinite, H) "`measurement_error_covariance` must contain only finite entries." - @assert isapprox(H, H', rtol = 1e-10) "`measurement_error_covariance` must be symmetric." - H = (H + H') / 2 # symmetrise away round-off - @assert ℒ.isposdef(H) "`measurement_error_covariance` must be positive definite." - if is_particle_filter && !ℒ.isdiag(H) - error("The particle filters currently require a diagonal measurement-error covariance; `measurement_error_covariance` is off-diagonal. Either use `filter = :kalman` (which supports a full covariance), or model the correlated measurement error structurally by adding measurement-error processes to the observation equations, which turns it back into an independent (diagonal) one.") - end - H - else - build_filter_measurement_error_variances(resolved_measurement_error_std, size(data_in_deviations, 1)) - end + # Gaussian measurement-error covariance H: `nothing` when no measurement error + # is active, a vector of per-observable variances (in data-row order) when H is + # diagonal, or a full covariance matrix. Supported by the Kalman and particle + # filters; the inversion filter recovers shocks exactly and does not admit + # measurement error. `:auto` resolves per filter. + measurement_error_H = resolve_measurement_error(filter, measurement_error, data_in_deviations) - if filter == :inversion && measurement_error_variances !== nothing - error("`measurement_error_std` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`).") + if filter == :inversion && measurement_error_H !== nothing + error("`measurement_error` is not supported by the inversion filter (`filter = :inversion`). Use `filter = :kalman` (linear) or one of the particle filters (`:bootstrap_particle`, `:auxiliary_particle`, `:tempered_particle`).") end if is_particle_filter - if measurement_error_variances === nothing - error("The particle filters require measurement error (they are degenerate without it); set `measurement_error_std` to a positive value (scalar or per-observable vector), or leave it at `:auto`.") + if measurement_error_H === nothing + error("The particle filters require measurement error (they are degenerate without it); set `measurement_error` to a positive variance (scalar, per-observable vector, or covariance matrix), or leave it at `:auto`.") end # The particle filter evaluates in Float64 and is not differentiable; a # forward-mode `Dual` parameter type would silently yield a zero gradient. @@ -4750,10 +4775,7 @@ function get_loglikelihood(𝓂::ℳ, constants_obj, state, 𝓂, - # the particle filters take the per-observable variances; - # a (necessarily diagonal) covariance matrix is reduced here - measurement_error_variances isa AbstractMatrix ? - collect(ℒ.diag(measurement_error_variances)) : measurement_error_variances, + measurement_error_H, obs_idx_per_t, has_missing; n_particles = n_particles, @@ -4784,7 +4806,7 @@ function get_loglikelihood(𝓂::ℳ, presample_periods = presample_periods, initial_covariance = initial_covariance, filter_algorithm = filter_algorithm, - measurement_error_variances = measurement_error_variances, + measurement_error = measurement_error_H, opts = opts, on_failure_loglikelihood = on_failure_loglikelihood) else @@ -4800,7 +4822,7 @@ function get_loglikelihood(𝓂::ℳ, presample_periods = presample_periods, initial_covariance = initial_covariance, filter_algorithm = filter_algorithm, - measurement_error_variances = measurement_error_variances, + measurement_error = measurement_error_H, opts = opts, on_failure_loglikelihood = on_failure_loglikelihood) end diff --git a/src/rrules.jl b/src/rrules.jl index 6ae0a7afb..b4fe1dd9b 100644 --- a/src/rrules.jl +++ b/src/rrules.jl @@ -1845,12 +1845,12 @@ function rrule(::typeof(get_loglikelihood), steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), - on_failure_loglikelihood::U = -Inf, + on_failure_loglikelihood::U = DEFAULT_ON_FAILURE_LOGLIKELIHOOD_SELECTOR(filter), warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, presample_periods::Int = DEFAULT_PRESAMPLE_PERIODS, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, filter_algorithm::Symbol = :LagrangeNewton, - measurement_error_std::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR_STD, + measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, @@ -1870,15 +1870,15 @@ function rrule(::typeof(get_loglikelihood), end # `:auto` means "no measurement error" for the non-particle filters, so it is # the one symbolic value that can reach here and is always differentiable. - me_active = if measurement_error_std isa Symbol + me_active = if measurement_error isa Symbol false - elseif measurement_error_std isa AbstractArray - any(x -> x != 0, measurement_error_std) + elseif measurement_error isa AbstractArray + any(x -> x != 0, measurement_error) else - measurement_error_std != 0 + measurement_error != 0 end if me_active - error("Reverse-mode automatic differentiation of the Kalman likelihood with measurement error (`measurement_error_std`) is not yet supported. Use forward-mode AD (e.g. `AutoForwardDiff`) or a gradient-free sampler.") + error("Reverse-mode automatic differentiation of the Kalman likelihood with measurement error (`measurement_error`) is not yet supported. Use forward-mode AD (e.g. `AutoForwardDiff`) or a gradient-free sampler.") end opts = merge_calculation_options(tol = tol, verbose = verbose, diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index f9a40a7ef..39a274335 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -40,27 +40,27 @@ threw(f) = try; f(); false; catch; true; end @testset "Measurement error on the Kalman filter" begin llk_no = get_loglikelihood(RBC_pf, data, p; filter = :kalman) - # measurement_error_std = 0 must reduce exactly to the no-ME likelihood - @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = 0.0) == llk_no + # measurement_error = 0 must reduce exactly to the no-ME likelihood + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = 0.0) == llk_no # a positive measurement error changes the likelihood and stays finite - llk_me = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + llk_me = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = me^2) @test isfinite(llk_me) @test llk_me != llk_no # scalar broadcast equals the equivalent per-observable vector - @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = [me, me]) ≈ llk_me + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = [me^2, me^2]) ≈ llk_me # ForwardDiff flows through the Kalman likelihood with measurement error - g = ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, measurement_error_std = me), p) + g = ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, measurement_error = me^2), p) @test all(isfinite, g) end - kal = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + kal = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = me^2) @testset "Bootstrap PF converges to the Kalman likelihood" begin # The bootstrap particle-filter likelihood estimator is unbiased for the # true likelihood, so log L̂ is downward biased by ≈ Var(log L̂)/2 and both # the bias and the variance shrink with the number of particles. pf(N, s) = get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_std = me, n_particles = N, particle_rng = Random.Xoshiro(s)) + measurement_error = me^2, n_particles = N, particle_rng = Random.Xoshiro(s)) nseeds = 24 ll_small = [pf(2_000, 100 + s) for s in 1:nseeds] ll_large = [pf(16_000, 200 + s) for s in 1:nseeds] @@ -80,7 +80,7 @@ threw(f) = try; f(); false; catch; true; end @testset "Variants: correct and ordered by efficiency" begin variant(pf_filter, N, s) = get_loglikelihood(RBC_pf, data, p; filter = pf_filter, algorithm = :first_order, - measurement_error_std = me, + measurement_error = me^2, n_particles = N, particle_rng = Random.Xoshiro(s)) nseeds = 16 boot = [variant(:bootstrap_particle, 3_000, 300 + s) for s in 1:nseeds] @@ -115,13 +115,13 @@ threw(f) = try; f(); false; catch; true; end @testset "Higher-order algorithms run" begin for algo in (:first_order, :second_order, :pruned_second_order, :third_order, :pruned_third_order) llh = get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = algo, - measurement_error_std = me, n_particles = 3_000, particle_rng = Random.Xoshiro(7)) + measurement_error = me^2, n_particles = 3_000, particle_rng = Random.Xoshiro(7)) @test isfinite(llh) end # every variant runs at a pruned nonlinear order for pf_filter in (:bootstrap_particle, :auxiliary_particle, :tempered_particle) llh = get_loglikelihood(RBC_pf, data, p; filter = pf_filter, algorithm = :pruned_second_order, - measurement_error_std = me, + measurement_error = me^2, n_particles = 3_000, particle_rng = Random.Xoshiro(7)) @test isfinite(llh) end @@ -132,9 +132,9 @@ threw(f) = try; f(); false; catch; true; end raw[1, 3:5] .= missing raw[2, 20] = missing datam = KeyedArray(raw, Variable = [:c, :q], Time = 1:size(raw, 2)) - kal_m = get_loglikelihood(RBC_pf, datam, p; filter = :kalman, measurement_error_std = me) + kal_m = get_loglikelihood(RBC_pf, datam, p; filter = :kalman, measurement_error = me^2) pf_m = get_loglikelihood(RBC_pf, datam, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_std = me, n_particles = 16_000, particle_rng = Random.Xoshiro(9)) + measurement_error = me^2, n_particles = 16_000, particle_rng = Random.Xoshiro(9)) @test isfinite(kal_m) @test isfinite(pf_m) @test abs(kal_m - pf_m) < 3.0 @@ -145,32 +145,32 @@ threw(f) = try; f(); false; catch; true; end kal_v = get_estimated_variables(RBC_pf, data; filter = :kalman) for pf_filter in (:bootstrap_particle, :tempered_particle) v = get_estimated_variables(RBC_pf, data; filter = pf_filter, algorithm = :first_order, - measurement_error_std = me, n_particles = 20_000, + measurement_error = me^2, n_particles = 20_000, particle_rng = Random.Xoshiro(1)) @test size(v) == size(kal_v) @test all(isfinite, collect(v)) @test maximum(abs, collect(v) .- collect(kal_v)) < 0.1 end s = get_estimated_shocks(RBC_pf, data; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_std = me, n_particles = 10_000, + measurement_error = me^2, n_particles = 10_000, particle_rng = Random.Xoshiro(1)) @test all(isfinite, collect(s)) # nonlinear orders and the combined estimates entry point @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :pruned_second_order, measurement_error_std = me, + algorithm = :pruned_second_order, measurement_error = me^2, n_particles = 5_000, particle_rng = Random.Xoshiro(1)))) @test all(isfinite, collect(get_model_estimates(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :first_order, measurement_error_std = me, + algorithm = :first_order, measurement_error = me^2, n_particles = 5_000, particle_rng = Random.Xoshiro(1)))) end @testset "Particle smoothing" begin kal_sm = collect(get_estimated_variables(RBC_pf, data; filter = :kalman, smooth = true)) pf_filt = collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :first_order, smooth = false, measurement_error_std = me, + algorithm = :first_order, smooth = false, measurement_error = me^2, n_particles = 20_000, particle_rng = Random.Xoshiro(1))) pf_sm = collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :first_order, smooth = true, measurement_error_std = me, + algorithm = :first_order, smooth = true, measurement_error = me^2, n_particles = 20_000, particle_rng = Random.Xoshiro(1))) @test all(isfinite, pf_sm) @test size(pf_sm) == size(kal_sm) @@ -179,14 +179,14 @@ threw(f) = try; f(); false; catch; true; end # smoothing works for the other variants and at nonlinear orders for pf_filter in (:auxiliary_particle, :tempered_particle) @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = pf_filter, - algorithm = :first_order, smooth = true, measurement_error_std = me, + algorithm = :first_order, smooth = true, measurement_error = me^2, n_particles = 5_000, particle_rng = Random.Xoshiro(2)))) end @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :pruned_second_order, smooth = true, measurement_error_std = me, + algorithm = :pruned_second_order, smooth = true, measurement_error = me^2, n_particles = 3_000, particle_rng = Random.Xoshiro(3)))) @test all(isfinite, collect(get_estimated_shocks(RBC_pf, data; filter = :bootstrap_particle, - algorithm = :first_order, smooth = true, measurement_error_std = me, + algorithm = :first_order, smooth = true, measurement_error = me^2, n_particles = 10_000, particle_rng = Random.Xoshiro(4)))) # the inversion filter still has no smoother @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :inversion, @@ -199,7 +199,7 @@ threw(f) = try; f(); false; catch; true; end # contribution of the initial state. nE = length(get_shocks(RBC_pf)) dec(; kw...) = get_shock_decomposition(RBC_pf, data; filter = :bootstrap_particle, - measurement_error_std = me, n_particles = 6_000, + measurement_error = me^2, n_particles = 6_000, particle_rng = Random.Xoshiro(1), kw...) # available for the filtered *and* the smoothed shock estimates @@ -238,7 +238,7 @@ threw(f) = try; f(); false; catch; true; end (:bootstrap_particle, :pruned_second_order, (; smooth = true, shock_decomposition = true, marginal_contribution = true))) p = plot_model_estimates(RBC_pf, data; filter = pf_filter, algorithm = algo, - measurement_error_std = me, n_particles = 2_000, + measurement_error = me^2, n_particles = 2_000, particle_rng = Random.Xoshiro(1), show_plots = false, save_plots = true, save_plots_path = tmp, save_plots_format = :png, kw...) @@ -250,35 +250,76 @@ threw(f) = try; f(); false; catch; true; end @testset "Full measurement-error covariance" begin Hdiag = [me^2 0.0; 0.0 me^2] # a diagonal covariance reproduces the equivalent per-observable stds - @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = Hdiag) ≈ - get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) + @test get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = Hdiag) ≈ + get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = me^2) # the Kalman filter accepts genuinely correlated measurement error Hfull = [me^2 0.6me^2; 0.6me^2 me^2] - llf = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = Hfull) + llf = get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = Hfull) @test isfinite(llf) - @test llf != get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = me) - # the particle filters take a diagonal covariance but reject an off-diagonal one - @test isfinite(get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_covariance = Hdiag, n_particles = 2_000, - particle_rng = Random.Xoshiro(1))) - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_covariance = Hfull, n_particles = 500, - particle_rng = Random.Xoshiro(1))) + @test llf != get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = me^2) + # a diagonal covariance is equivalent to the variance vector for the + # particle filters too (same RNG ⇒ bit-identical, since a diagonal matrix + # is reduced to the elementwise fast path) + @test get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error = Hdiag, n_particles = 2_000, + particle_rng = Random.Xoshiro(1)) == + get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error = me^2, n_particles = 2_000, + particle_rng = Random.Xoshiro(1)) + # the particle filters also handle genuinely correlated measurement error, + # and on a linear model they must still converge to the Kalman value + pf_full = [get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error = Hfull, n_particles = 40_000, + particle_rng = Random.Xoshiro(100 + s)) for s in 1:4] + @test all(isfinite, pf_full) + @test isapprox(Statistics.mean(pf_full), llf, atol = 1.0) + # ... and must differ from the diagonal answer, i.e. the off-diagonal + # entries are genuinely used rather than silently dropped + @test get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error = Hfull, n_particles = 2_000, + particle_rng = Random.Xoshiro(1)) != + get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + measurement_error = Hdiag, n_particles = 2_000, + particle_rng = Random.Xoshiro(1)) + # every variant accepts a correlated covariance + for f in (:auxiliary_particle, :tempered_particle) + @test isfinite(get_loglikelihood(RBC_pf, data, p; filter = f, algorithm = :first_order, + measurement_error = Hfull, n_particles = 2_000, + particle_rng = Random.Xoshiro(3))) + end # a covariance must be symmetric, positive definite and correctly sized - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = [1.0 2.0; 0.0 1.0])) - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = -Hdiag)) - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_covariance = fill(me^2, 1, 1))) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = [1.0 2.0; 0.0 1.0])) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = -Hdiag)) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = fill(me^2, 1, 1))) + end + + @testset "Estimated variable standard deviations" begin + sk = get_estimated_variable_standard_deviations(RBC_pf, data) + @test size(sk, 2) == size(data, 2) + @test all(isfinite, sk) && all(>=(0), sk) + # the particle filters report the spread of the cloud, at any order + for (f, algo) in ((:bootstrap_particle, :first_order), + (:tempered_particle, :pruned_second_order)) + sp = get_estimated_variable_standard_deviations(RBC_pf, data; filter = f, algorithm = algo, + measurement_error = me^2, n_particles = 3_000, + particle_rng = Random.Xoshiro(11)) + @test size(sp) == size(sk) + @test all(isfinite, sp) && all(>=(0), sp) + @test any(>(0), sp) + end + # the inversion filter identifies the state exactly and has no spread + @test threw(() -> get_estimated_variable_standard_deviations(RBC_pf, data; filter = :inversion)) end @testset "Filter selection and automatic measurement error" begin # `:particle` is an alias for the bootstrap filter: same RNG ⇒ same value @test get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, - measurement_error_std = me, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) == + measurement_error = me^2, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) == get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_std = me, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) + measurement_error = me^2, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) # `:auto` leaves the Kalman filter without measurement error @test get_loglikelihood(RBC_pf, data, p; filter = :kalman) == - get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error_std = :auto) + get_loglikelihood(RBC_pf, data, p; filter = :kalman, measurement_error = :auto) # `:auto` gives the particle filters a workable measurement error @test isfinite(get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, n_particles = 2_000, particle_rng = Random.Xoshiro(5))) @@ -288,20 +329,20 @@ threw(f) = try; f(); false; catch; true; end @testset "Error guards" begin # measurement error is not available for the inversion filter - @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :inversion, measurement_error_std = me)) + @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :inversion, measurement_error = me^2)) # the particle filter requires measurement error @test threw(() -> get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, - measurement_error_std = 0.0)) + measurement_error = 0.0)) # the particle filter is not differentiable (forward or reverse mode) @test threw(() -> ForwardDiff.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :bootstrap_particle, - algorithm = :first_order, measurement_error_std = me, n_particles = 500, + algorithm = :first_order, measurement_error = me^2, n_particles = 500, particle_rng = Random.Xoshiro(1)), p)) @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :bootstrap_particle, - algorithm = :first_order, measurement_error_std = me, n_particles = 500, + algorithm = :first_order, measurement_error = me^2, n_particles = 500, particle_rng = Random.Xoshiro(1)), p)) # reverse-mode AD of the Kalman likelihood with measurement error is guarded @test threw(() -> Zygote.gradient(x -> get_loglikelihood(RBC_pf, data, x; filter = :kalman, - measurement_error_std = me), p)) + measurement_error = me^2), p)) end end diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index c70cd9207..4402bb094 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -36,13 +36,13 @@ using DelimitedFiles, AxisKeys # cloud represents the ergodic distribution, matching `initial_covariance = :theoretical`. kal = get_loglikelihood(m, data(observables), p; filter = :kalman, presample_periods = 4, initial_covariance = :theoretical, - measurement_error_std = me) + measurement_error = me .^ 2) @test isfinite(kal) for (pf_filter, N) in ((:bootstrap_particle, 20_000), (:auxiliary_particle, 20_000), (:tempered_particle, 8_000)) lls = [get_loglikelihood(m, data(observables), p; filter = pf_filter, algorithm = :first_order, presample_periods = 4, initial_covariance = :theoretical, - measurement_error_std = me, n_particles = N, particle_rng = Random.Xoshiro(1000 + s)) for s in 1:6] + measurement_error = me .^ 2, n_particles = N, particle_rng = Random.Xoshiro(1000 + s)) for s in 1:6] @test all(isfinite, lls) # the Monte-Carlo mean matches the Kalman value up to the (downward) Var/2 bias @test abs(kal - Statistics.mean(lls)) < 15 From f1bf9083a8f863f24adda4e871bc963504cbeab8 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 21:04:42 +0200 Subject: [PATCH 13/24] Move particle-filter buffers into the model workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer feedback: the buffers were allocated per call. Inside a sampler the likelihood is evaluated thousands of times at identical dimensions, so they now live in `𝓂.workspaces.particle`, lazily sized by `ensure_particle_workspace!(nVars, nExo, n_particles)` in the same style as the Kalman and Lyapunov workspaces. Six nVars×N and three nExo×N matrices plus the per-particle vectors cover the simultaneous needs of all three first-order kernels (the tempered one needs the most: ancestors, states, Metropolis proposals, and a swap partner for each). Repeat evaluations at N = 20,000 now allocate 18 KiB instead of ~5 MB. The higher-order kernels keep per-call pools: their particles are `Vector{Vector}` whose element layout depends on the pruning order, so they do not share a fixed buffer shape. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- src/filter/particle.jl | 65 ++++++++++++++++++--------------------- src/options_and_caches.jl | 62 +++++++++++++++++++++++++++++++++++++ src/structures.jl | 49 ++++++++++++++++++++++++++++- 3 files changed, 140 insertions(+), 36 deletions(-) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 20973a63f..511301f35 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -1370,14 +1370,13 @@ function run_particle_filter(::Val{:first_order}, Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - X = Matrix{Float64}(undef, nVars, n_particles) - X2 = Matrix{Float64}(undef, nVars, n_particles) - E = Matrix{Float64}(undef, nExo, n_particles) - Z = Matrix{Float64}(undef, nVars, n_particles) - W = fill(1.0 / n_particles, n_particles) - logdens = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) + # Cloud and scratch come from the model's workspace, so a sampler that calls + # this thousands of times pays for them once (see `ensure_particle_workspace!`). + pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) + X, X2, Z = pws.X, pws.X2, pws.Anc + E = pws.E + W, logdens, idx, bins = pws.W, pws.logdens, pws.idx, pws.bins + fill!(W, 1.0 / n_particles) mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) init_linear_particles!(X, rng, mean0, L, Z) @@ -1485,16 +1484,12 @@ function run_particle_filter(::Val{:first_order}, Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - X = Matrix{Float64}(undef, nVars, n_particles) - X2 = Matrix{Float64}(undef, nVars, n_particles) - AncX = Matrix{Float64}(undef, nVars, n_particles) - E = Matrix{Float64}(undef, nExo, n_particles) - W = fill(1.0 / n_particles, n_particles) - logg̃ = Vector{Float64}(undef, n_particles) - logw = Vector{Float64}(undef, n_particles) - lam = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) + pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) + X, X2, AncX = pws.X, pws.X2, pws.Anc + E = pws.E + W, logg̃, logw = pws.W, pws.logdens, pws.logw + lam, idx, bins = pws.lam, pws.idx, pws.bins + fill!(W, 1.0 / n_particles) mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) init_linear_particles!(X, rng, mean0, L, AncX) @@ -1616,23 +1611,23 @@ function run_particle_filter(::Val{:first_order}, Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - # Double-buffered particle pools (columns are particles). - Anc = Matrix{Float64}(undef, nVars, n_particles) - Anc2 = Matrix{Float64}(undef, nVars, n_particles) - Sh = Matrix{Float64}(undef, nExo, n_particles) - Sh2 = Matrix{Float64}(undef, nExo, n_particles) - St = Matrix{Float64}(undef, nVars, n_particles) - St2 = Matrix{Float64}(undef, nVars, n_particles) - dv = Vector{Float64}(undef, n_particles) - dv2 = Vector{Float64}(undef, n_particles) - - Base = Matrix{Float64}(undef, nVars, n_particles) - Sprop = Matrix{Float64}(undef, nVars, n_particles) - Eprop = Matrix{Float64}(undef, nExo, n_particles) - logw = Vector{Float64}(undef, n_particles) - Wn = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) + # Double-buffered particle pools (columns are particles), taken from the + # model's workspace. The locals below are swapped in place of copying, which + # leaves the workspace fields pointing at whichever buffer ends up where — + # harmless, since every buffer is written before it is read. + pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) + Anc, Anc2 = pws.Anc, pws.Anc2 + Sh, Sh2 = pws.E, pws.E2 + St, St2 = pws.St, pws.St2 + dv, dv2 = pws.dv, pws.dv2 + + Base = pws.X + Sprop = pws.X2 + Eprop = pws.Eprop + logw = pws.logw + Wn = pws.Wn + idx = pws.idx + bins = pws.bins mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) init_linear_particles!(St, rng, mean0, L, Anc2) diff --git a/src/options_and_caches.jl b/src/options_and_caches.jl index bc9fd4978..e8f7111b8 100644 --- a/src/options_and_caches.jl +++ b/src/options_and_caches.jl @@ -1263,6 +1263,67 @@ function grow_mat_seq_undef!(seq::Vector{Matrix{T}}, Tt::Int) where T return seq end +""" + Particle_workspace(::Type{TT} = Float64) + +Create a workspace for the particle filters with lazy buffer allocation. All +buffers start empty and are sized on demand by `ensure_particle_workspace!`. +""" +function Particle_workspace(::Type{TT} = Float64) where {TT <: Real} + particle_workspace{TT}( + 0, 0, 0, # nVars, nExo, n_particles + zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # X, X2, Anc + zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # Anc2, St, St2 + zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # E, E2, Eprop + zeros(TT, 0), zeros(TT, 0), zeros(TT, 0), # W, Wn, logdens + zeros(TT, 0), zeros(TT, 0), zeros(TT, 0), # logw, lam, dv + zeros(TT, 0), zeros(TT, 0), # dv2, bins + Int[]) # idx +end + +""" + ensure_particle_workspace!(workspaces, nVars, nExo, n_particles) + +Size the particle-filter buffers for the requested dimensions, reallocating only +when they change. Returns the workspace. Contents are undefined on entry — every +kernel writes each buffer before reading it. +""" +function ensure_particle_workspace!(workspaces::workspaces, nVars::Int, nExo::Int, n_particles::Int) + ws = workspaces.particle + + if ws.nVars != nVars || ws.n_particles != n_particles + ws.X = Matrix{Float64}(undef, nVars, n_particles) + ws.X2 = Matrix{Float64}(undef, nVars, n_particles) + ws.Anc = Matrix{Float64}(undef, nVars, n_particles) + ws.Anc2 = Matrix{Float64}(undef, nVars, n_particles) + ws.St = Matrix{Float64}(undef, nVars, n_particles) + ws.St2 = Matrix{Float64}(undef, nVars, n_particles) + ws.nVars = nVars + end + + if ws.nExo != nExo || ws.n_particles != n_particles + ws.E = Matrix{Float64}(undef, nExo, n_particles) + ws.E2 = Matrix{Float64}(undef, nExo, n_particles) + ws.Eprop = Matrix{Float64}(undef, nExo, n_particles) + ws.nExo = nExo + end + + if ws.n_particles != n_particles + ws.W = Vector{Float64}(undef, n_particles) + ws.Wn = Vector{Float64}(undef, n_particles) + ws.logdens = Vector{Float64}(undef, n_particles) + ws.logw = Vector{Float64}(undef, n_particles) + ws.lam = Vector{Float64}(undef, n_particles) + ws.dv = Vector{Float64}(undef, n_particles) + ws.dv2 = Vector{Float64}(undef, n_particles) + ws.bins = Vector{Float64}(undef, n_particles) + ws.idx = Vector{Int}(undef, n_particles) + ws.n_particles = n_particles + end + + return ws +end + """ Kalman_workspace(::Type{TT} = Float64) @@ -1389,6 +1450,7 @@ function Workspaces(::Type{T} = Float64, ::Type{S} = Float64) where {T <: Real, Find_shocks_workspace(T), # conditional forecast - will be resized Inversion_workspace(T), # inversion filter - will be resized Kalman_workspace(T), # Kalman filter - will be resized + Particle_workspace(T), # particle filters - will be resized NSSSSolverWorkspace()) # NSSS solver scratch buffers end diff --git a/src/structures.jl b/src/structures.jl index 85a54599c..5a652d1a5 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -1187,7 +1187,53 @@ mutable struct inversion_workspace{T <: Real} end -""" +""" +Workspace for particle filter computations. + +Holds the particle cloud and the per-particle scratch the filters in +`src/filter/particle.jl` reuse every period. Sized by `(nVars, nExo, n_particles)` +and lazily (re)allocated by `ensure_particle_workspace!` — inside a sampler the +likelihood is evaluated thousands of times at the same dimensions, so these +buffers are allocated once for the whole run rather than once per evaluation. + +Six `nVars × n_particles` and three `nExo × n_particles` matrices cover the +simultaneous needs of every variant: the bootstrap filter uses the fewest, the +tempered filter the most (ancestors, states, Metropolis proposals, and the +swap partners for each). +""" +mutable struct particle_workspace{T <: Real} + # Dimensions (for reallocation checks) + nVars::Int + nExo::Int + n_particles::Int + + # nVars × n_particles state clouds + X::Matrix{T} + X2::Matrix{T} + Anc::Matrix{T} + Anc2::Matrix{T} + St::Matrix{T} + St2::Matrix{T} + + # nExo × n_particles shock clouds + E::Matrix{T} + E2::Matrix{T} + Eprop::Matrix{T} + + # per-particle scratch + W::Vector{T} # normalised importance weights + Wn::Vector{T} # stage weights (tempered) + logdens::Vector{T} # log measurement density + logw::Vector{T} # log weights + lam::Vector{T} # first-stage weights (auxiliary) + dv::Vector{T} # quadratic forms (tempered) + dv2::Vector{T} # swap partner for dv + bins::Vector{T} # cumulative-weight scratch for resampling + idx::Vector{Int} # ancestor indices from resampling +end + + +""" Workspace for Kalman filter computations. Contains pre-allocated buffers for state estimates, covariances, and matrix operations. Buffers are lazily allocated and resized as needed via ensure_kalman_workspaces!. @@ -1359,6 +1405,7 @@ mutable struct workspaces find_shocks::find_shocks_workspace{Float64} # Conditional forecast shock finding inversion::inversion_workspace{Float64} # Inversion filter kalman::kalman_workspace{Float64} # Kalman filter + particle::particle_workspace{Float64} # Particle filters # NSSS solver shared scratch buffers nsss_solver::NSSSSolverWorkspace end From 7717ebf6a9d40eba28f1a43cd9756a619cc5cb4e Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 21:24:24 +0200 Subject: [PATCH 14/24] Make the tempering controls act on the estimates path too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer feedback: the tempering arguments should be reachable from every function that takes `filter`. They now are — but adding them as inert kwargs would have been worse than not having them, so `filter_data_with_model` learns the tempered recursion rather than only the bootstrap one. `:bootstrap_particle` and `:auxiliary_particle` genuinely need no distinction here: the auxiliary filter's look-ahead proposal changes the variance of the *likelihood* estimate, not the cloud it leaves behind. `:tempered_particle` does change the cloud — within each period it bridges from the prior to the full measurement density, resampling and rejuvenating the shocks by random-walk Metropolis at each stage — so the estimates and the smoother both benefit from the extra distinct support points. The smoother composes the within-period resampling maps with the end-of-period ones (`within[t]` then `parent[t-1]`) so the genealogy still walks correctly. Verified: on the linear model the tempered smoothed path is closer to the Kalman smoother than the filtered path, and the tempering controls demonstrably change the result. Also documents why forward-filtering backward-smoothing is not available: re-pairing a stored x_{t+1} with a different ancestor needs an ε solving g(x_t^i, ε) = x_{t+1}, which is overdetermined when there are fewer shocks than states, so every backward weight is zero. FFBS would need a kernel-regularised transition; the genealogy smoother is exact as written, and tempering is the lever against its path degeneracy. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 6 +- ext/StatsPlotsExt.jl | 14 +++- src/filter/particle.jl | 144 +++++++++++++++++++++++++++++++++-- src/get_functions.jl | 36 ++++++++- test/test_particle_filter.jl | 18 +++++ 5 files changed, 205 insertions(+), 13 deletions(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index 8f6eaf673..dec7dbc87 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -144,7 +144,9 @@ In practice this buys a large variance reduction per particle — several times `smooth = true` returns ``E[x_t \mid y_{1:T}]`` rather than ``E[x_t \mid y_{1:t}]``, i.e. estimates that use the *whole* sample. For the particle filters this is done by **fixed-interval smoothing along the filter's genealogy**: every particle surviving at ``T`` carries the ancestral line that produced it, and those lines are draws from the joint smoothing distribution ``p(x_{1:T} \mid y_{1:T})``, so averaging them with the terminal weights gives the smoothed moments directly. -Why not the textbook backward-kernel smoother? That one reweights particles at ``t`` by the backward transition density ``p(x_{t+1} \mid x_t)``. In a DSGE that density is **singular**: with fewer shocks than states the transition maps ``x_t`` onto a lower-dimensional manifold, so ``p(x_{t+1} \mid x_t)`` is a Dirac on that manifold and the reweighting is undefined. The genealogy is what remains well defined. +Why not forward-filtering backward-smoothing (FFBS)? FFBS — and backward simulation generally — reweights the time-``t`` particles by the backward transition density ``p(\tilde x_{t+1} \mid x_t^i)``, so that any ancestor can be re-paired with any successor and the genealogy's degeneracy disappears. In a DSGE that density does not exist. The transition is ``x_{t+1} = g(x_t, \varepsilon_{t+1})`` with fewer shocks than states, so it maps ``x_t`` onto a lower-dimensional manifold and ``p(x_{t+1} \mid x_t)`` is a Dirac on it. Re-pairing a stored ``\tilde x_{t+1}`` with a *different* ancestor ``x_t^i`` would require an ``\varepsilon`` solving ``g(x_t^i, \varepsilon) = \tilde x_{t+1}`` — an overdetermined system with no solution for almost every ``i``. Every ancestor weight is zero and the backward draw is undefined. (This is the same identification arithmetic that makes the *inversion* filter work when shocks and observables balance, and fail otherwise.) + +Recovering FFBS therefore requires an approximation — a kernel-regularised transition, with a bandwidth that trades bias against the degeneracy it removes — rather than a drop-in replacement. The genealogy smoother is exact for the model as written, so it is the default. The practical lever against degeneracy is `filter = :tempered_particle`, whose within-period Metropolis rejuvenation keeps many more distinct support points alive at the same `n_particles`, which is what the backward pass is short of. #### Shock decomposition @@ -152,7 +154,7 @@ A shock decomposition needs a shock path, and the particle filters supply one One subtlety specific to a Monte-Carlo filter: the smoothed *mean* path is not itself a model trajectory, because averaging does not commute with a nonlinear transition (``E[g(x,\varepsilon)] \neq g(E[x],E[\varepsilon])``). The pruned decomposition therefore attributes the trajectory implied by the smoothed shocks — the same object the inversion filter decomposes — so that the contributions close exactly. -The known limitation is **path degeneracy**: ancestral lines coalesce as one goes back in time, so the earliest periods rest on fewer distinct trajectories than the particle count suggests. More particles push the coalescence point further back. Smoothing also stores the whole cloud, so its memory cost is about ``n_{vars} \times N \times T \times 8`` bytes — worth keeping in mind before raising `n_particles` for a long sample. +The known limitation is **path degeneracy**: ancestral lines coalesce as one goes back in time, so the earliest periods rest on fewer distinct trajectories than the particle count suggests. More particles push the coalescence point further back, and `:tempered_particle` slows the coalescence itself. This is also why the standard deviations reported by `get_estimated_variable_standard_deviations` understate uncertainty early in the sample when `smooth = true`. Smoothing also stores the whole cloud, so its memory cost is about ``n_{vars} \times N \times T \times 8`` bytes — worth keeping in mind before raising `n_particles` for a long sample. ### Resampling schemes diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index 3688bc672..8b5aa69d1 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -640,6 +640,7 @@ If occasionally binding constraints are present in the model, they are not taken - $STEADY_STATE_FUNCTION® - $ALGORITHM® - $FILTER® +$MacroModelling.PARTICLE_FILTER_KEYWORDS® - $(VARIABLES®(DEFAULT_VARIABLES_EXCLUDING_OBC)) - `shocks` [Default: `:all`]: shocks for which to plot the estimates in the respective subplots and in the shock decompositions. Inputs can be either a `Symbol` or `String` (e.g. `:eps_a`, `\"eps_a\"`, or `:all`), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. `:all` selects all shocks in the model. `:none` selects no shocks in the model. If not all shocks are shown, the ommitted shocks will be summarised and netted under the label `Other shocks (net)` in the shock decomposition. - `presample_periods` [Default: `0`, Type: `Int`]: number of initial retained-sample periods omitted from the plot. Useful when filtering the full sample while focusing on a later subperiod. Values above the retained sample length are clamped down automatically with an informational message. @@ -713,6 +714,10 @@ function plot_model_estimates(𝓂::ℳ, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = MacroModelling.DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = MacroModelling.DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = MacroModelling.DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = MacroModelling.DEFAULT_TEMPERING_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -1289,6 +1294,7 @@ This function shares most of the signature and functionality of [`plot_model_est - $STEADY_STATE_FUNCTION® - $ALGORITHM® - $FILTER® +$MacroModelling.PARTICLE_FILTER_KEYWORDS® - $(VARIABLES®(DEFAULT_VARIABLES_EXCLUDING_OBC)) - `shocks` [Default: `:all`]: shocks for which to plot the estimates in the respective subplots. Inputs can be either a `Symbol` or `String` (e.g. `:eps_a`, `\"eps_a\"`, or `:all`), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. `:all` selects all shocks in the model. `:none` selects no shocks in the model. - `presample_periods` [Default: `0`, Type: `Int`]: number of initial retained-sample periods omitted from the plot. Useful when filtering the full sample while focusing on a later subperiod. Values above the retained sample length are clamped down automatically with an informational message. @@ -1379,6 +1385,10 @@ function plot_model_estimates!(𝓂::ℳ, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = MacroModelling.DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = MacroModelling.DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = MacroModelling.DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = MacroModelling.DEFAULT_TEMPERING_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -1503,7 +1513,9 @@ function plot_model_estimates!(𝓂::ℳ, particle_kw = filter ∈ MacroModelling.PARTICLE_FILTERS ? (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, - particle_initial_state_scaling, particle_rng) : NamedTuple() + particle_initial_state_scaling, particle_rng, + tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale) : NamedTuple() variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; particle_kw...) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 511301f35..33a6c4be5 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -1752,19 +1752,31 @@ end # the genealogy smoother in `smooth_particle_trajectories!` below. # # All three particle variants target the same filtering distribution p(xₜ|y₁..ₜ) -# — they differ only in how efficiently they estimate the *likelihood* — so the -# moments below are produced by the standard predict/weight/resample recursion -# whichever particle filter was selected. +# — they differ only in how they get there — so the recursion below is shared. +# `:bootstrap_particle` and `:auxiliary_particle` use the plain +# predict/weight/resample step: the auxiliary filter's look-ahead proposal changes +# the *variance* of the likelihood estimate, not the cloud it leaves behind, so +# there is nothing extra to do for the moments. `:tempered_particle` does change +# the cloud: within each period it bridges from the prior to the full measurement +# density in stages, resampling and rejuvenating the shocks by random-walk +# Metropolis at each one. That leaves a cloud with far more distinct support +# points at the same `n_particles`, which is exactly what the moments (and the +# smoother, which walks the genealogy) benefit from — so the tempering controls +# act here too. # # `decomposition` is the shock decomposition of whichever shock path was # produced — filtered when `smooth = false`, smoothed when `smooth = true`. @unstable function filter_data_with_model(𝓂::ℳ, data_in_deviations::KeyedArray{Float64}, ::Val{algo}, - ::Union{Val{:bootstrap_particle},Val{:auxiliary_particle},Val{:tempered_particle}}; + ::Val{pf}; warmup_iterations::Int = 0, opts::CalculationOptions = merge_calculation_options(), smooth::Bool = true, + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -1772,7 +1784,9 @@ end particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), marginal_contribution::Bool = false, - initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical) where {algo} + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical) where {algo, pf} + + @assert pf ∈ PARTICLE_FILTERS "`filter_data_with_model` was dispatched to the particle path with `filter = :$(pf)`." obs_axis = collect(axiskeys(data_in_deviations, 1)) observables = obs_axis isa String_input ? obs_axis .|> Meta.parse .|> replace_indices : obs_axis @@ -1838,12 +1852,38 @@ end hist_states = smooth ? [Matrix{Float64}(undef, nVars, n_particles) for _ in 1:nT] : Matrix{Float64}[] hist_shocks = smooth ? [Matrix{Float64}(undef, nExo, n_particles) for _ in 1:nT] : Matrix{Float64}[] parent = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] + # `within[t]` is the composition of the tempering stages' resampling maps for + # period t: post-stage slot ↦ pre-stage slot. Empty for the non-tempered + # variants, which do no within-period resampling. + within = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] terminal_weights = smooth ? fill(1.0 / n_particles, n_particles) : Float64[] + tempered = pf == :tempered_particle + r_star = Float64(tempering_target_ratio) + mh_scale = Float64(tempering_mh_scale) + n_mh = tempering_mh_steps + max_stages = tempering_max_stages + # scratch used only by the tempering stages + anc_pool = tempered ? [zeros_like_particle(parts[1]) for _ in 1:n_particles] : typeof(parts)() + anc_pool2 = tempered ? [zeros_like_particle(parts[1]) for _ in 1:n_particles] : typeof(parts)() + shocks2 = tempered ? [Vector{Float64}(undef, nExo) for _ in 1:n_particles] : Vector{Float64}[] + sprop = tempered ? zeros_like_particle(parts[1]) : parts[1] + eprop = tempered ? Vector{Float64}(undef, nExo) : Float64[] + dv = tempered ? Vector{Float64}(undef, n_particles) : Float64[] + dv2 = tempered ? Vector{Float64}(undef, n_particles) : Float64[] + comp = tempered ? collect(1:n_particles) : Int[] + comp2 = tempered ? collect(1:n_particles) : Int[] + for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view dat[:, t] + if tempered + @inbounds for p in 1:n_particles + copy_particle!(anc_pool[p], parts[p]) + end + end + @inbounds for p in 1:n_particles Random.randn!(rng, shocks[p]) propagate!(parts2[p], parts[p], shocks[p]) @@ -1853,6 +1893,85 @@ end if isempty(rows) # nothing observed: the weights are unchanged, the cloud just predicts fill!(logdens, 0.0) + elseif tempered + # Bridge from the prior to the full measurement density in stages, + # resampling and rejuvenating at each one. The likelihood contribution + # is not needed here (that is `run_particle_filter`'s job) — only the + # cloud that comes out, which ends up equally weighted. + @inbounds for p in 1:n_particles + dv[p] = particle_quadratic_form(measurement_full(parts[p], full_buf), data_col, observables_index, me_var, rows) + end + if any(isfinite, dv) + @inbounds for p in 1:n_particles + comp[p] = p + end + d_obs = length(rows) + φ_old = 0.0 + stage = 0 + while φ_old < 1.0 - 1e-12 && stage < max_stages + stage += 1 + φ_new = tempered_next_phi(φ_old, dv, r_star, n_particles) + + lr = 0.5 * d_obs * (φ_old == 0.0 ? log(φ_new) : log(φ_new) - log(φ_old)) + @inbounds for p in 1:n_particles + logdens[p] = lr - 0.5 * (φ_new - φ_old) * dv[p] + end + m = maximum(logdens) + isfinite(m) || break + sw = 0.0 + @inbounds for p in 1:n_particles + sw += exp(logdens[p] - m) + end + (sw > 0 && isfinite(sw)) || break + logsw = m + log(sw) + @inbounds for p in 1:n_particles + W[p] = exp(logdens[p] - logsw) + end + + particle_resample_indices!(idx, bins, rng, W, particle_resampling) + @inbounds for j in 1:n_particles + a = idx[j] + copy_particle!(anc_pool2[j], anc_pool[a]) + copyto!(shocks2[j], shocks[a]) + copy_particle!(parts2[j], parts[a]) + dv2[j] = dv[a] + comp2[j] = comp[a] + end + anc_pool, anc_pool2 = anc_pool2, anc_pool + shocks, shocks2 = shocks2, shocks + parts, parts2 = parts2, parts + dv, dv2 = dv2, dv + comp, comp2 = comp2, comp + + # Rejuvenate: random-walk Metropolis on the shocks, targeting + # π(ε) ∝ N(ε;0,I)·exp(-φ/2·e(ε)ᵀH⁻¹e(ε)). + @inbounds for p in 1:n_particles + shp = shocks[p] + for _ in 1:n_mh + Random.randn!(rng, eprop) + esq_old = 0.0; esq_new = 0.0 + for e in 1:nExo + ep = shp[e] + mh_scale * eprop[e] + eprop[e] = ep + esq_new += ep * ep + esq_old += shp[e] * shp[e] + end + propagate!(sprop, anc_pool[p], eprop) + dprop = particle_quadratic_form(measurement_full(sprop, full_buf), data_col, observables_index, me_var, rows) + if log(rand(rng)) < -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) + copyto!(shp, eprop) + copy_particle!(parts[p], sprop) + dv[p] = dprop + end + end + end + + φ_old = φ_new + end + # the tempering stages leave an equally weighted cloud + fill!(W, 1.0 / n_particles) + if smooth; within[t] = copy(comp); end + end else @inbounds for p in 1:n_particles full = measurement_full(parts[p], full_buf) @@ -1921,7 +2040,7 @@ end end if smooth - smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, parent, terminal_weights) + smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, parent, within, terminal_weights) end # ── Shock decomposition ────────────────────────────────────────────────── @@ -2054,6 +2173,7 @@ function smooth_particle_trajectories!(variables::Matrix{Float64}, hist_states::Vector{Matrix{Float64}}, hist_shocks::Vector{Matrix{Float64}}, parent::Vector{Vector{Int}}, + within::Vector{Vector{Int}}, W::Vector{Float64}) nT = length(hist_states) nT == 0 && return nothing @@ -2086,8 +2206,18 @@ function smooth_particle_trajectories!(variables::Matrix{Float64}, stds[i, t] = sqrt(max(stds[i, t], 0.0)) end - # step the lineage back across the resampling applied at the end of t-1 + # Step the lineage back one period. Two maps may apply, innermost first: + # the tempered filter's within-period resampling (post-stage slot ↦ + # pre-stage slot at t, which is the slot the ancestor occupied after the + # end-of-(t-1) resampling), then the end-of-(t-1) resampling itself + # (↦ the slot indexing `hist_states[t-1]`). Either may be empty. if t > 1 + wit = within[t] + if !isempty(wit) + @inbounds for p in 1:n_particles + lineage[p] = wit[lineage[p]] + end + end par = parent[t-1] if !isempty(par) @inbounds for p in 1:n_particles diff --git a/src/get_functions.jl b/src/get_functions.jl index 69434f3ad..aac92f0dc 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -225,6 +225,7 @@ If occasionally binding constraints are present in the model, they are not taken - $PARAMETERS® - $STEADY_STATE_FUNCTION® - $FILTER® +$PARTICLE_FILTER_KEYWORDS® - $ALGORITHM® - $DATA_IN_LEVELS® - $SMOOTH® @@ -303,6 +304,10 @@ And data, 4×2×40 Array{Float64, 3}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -404,6 +409,7 @@ If occasionally binding constraints are present in the model, they are not taken - $STEADY_STATE_FUNCTION® - $ALGORITHM® - $FILTER® +$PARTICLE_FILTER_KEYWORDS® - $DATA_IN_LEVELS® - $SMOOTH® - $QME® @@ -460,6 +466,10 @@ And data, 1×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -507,7 +517,9 @@ And data, 1×40 Matrix{Float64}: particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, - particle_initial_state_scaling, particle_rng) : NamedTuple() + particle_initial_state_scaling, particle_rng, + tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale) : NamedTuple() variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, @@ -539,6 +551,7 @@ If occasionally binding constraints are present in the model, they are not taken - $STEADY_STATE_FUNCTION® - $ALGORITHM® - $FILTER® +$PARTICLE_FILTER_KEYWORDS® - $DATA_IN_LEVELS® - `levels` [Default: `true`, Type: `Bool`]: $LEVELS® - $SMOOTH® @@ -599,6 +612,10 @@ And data, 4×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, @@ -647,7 +664,9 @@ And data, 4×40 Matrix{Float64}: particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, - particle_initial_state_scaling, particle_rng) : NamedTuple() + particle_initial_state_scaling, particle_rng, + tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale) : NamedTuple() variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, @@ -681,6 +700,7 @@ docstrings of `get_estimated_variables` and `get_estimated_shocks` for details. - $STEADY_STATE_FUNCTION® - $ALGORITHM® - $FILTER® +$PARTICLE_FILTER_KEYWORDS® - $DATA_IN_LEVELS® - `levels` [Default: `true`, Type: `Bool`]: $LEVELS® - $SMOOTH® @@ -742,6 +762,10 @@ And data, 5×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, @@ -873,6 +897,10 @@ And data, 4×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), + tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, + tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, + tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, + tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_FLAG, verbose::Bool = DEFAULT_VERBOSE, @@ -926,7 +954,9 @@ And data, 4×40 Matrix{Float64}: particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, - particle_initial_state_scaling, particle_rng) : NamedTuple() + particle_initial_state_scaling, particle_rng, + tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale) : NamedTuple() variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), smooth = smooth, diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 39a274335..815489018 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -191,6 +191,24 @@ threw(f) = try; f(); false; catch; true; end # the inversion filter still has no smoother @test all(isfinite, collect(get_estimated_variables(RBC_pf, data; filter = :inversion, algorithm = :first_order, smooth = true))) + + # The tempered filter rejuvenates the cloud within each period, so its + # estimates path runs a different recursion — it must still land on the + # same smoothing distribution the bootstrap filter targets, and hence + # close to the Kalman smoother on this linear model. + tp_sm = collect(get_estimated_variables(RBC_pf, data; filter = :tempered_particle, + algorithm = :first_order, smooth = true, measurement_error = me^2, + n_particles = 20_000, particle_rng = Random.Xoshiro(1))) + @test size(tp_sm) == size(kal_sm) + @test maximum(abs, tp_sm .- kal_sm) < maximum(abs, pf_filt .- kal_sm) + # ... and the tempering controls must actually reach the recursion + tp_coarse = collect(get_estimated_variables(RBC_pf, data; filter = :tempered_particle, + algorithm = :first_order, smooth = true, measurement_error = me^2, + n_particles = 20_000, particle_rng = Random.Xoshiro(1), + tempering_target_ratio = 50.0, tempering_mh_steps = 3, + tempering_mh_scale = 0.9)) + @test tp_coarse != tp_sm + @test all(isfinite, tp_coarse) end @testset "Shock decomposition" begin From a491d6e4028fd460340d762e70975fdb2cb46bb6 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 21:28:12 +0200 Subject: [PATCH 15/24] Correct the filter comparison table for smoothing and correlated measurement error Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index dec7dbc87..efb07e860 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -26,11 +26,11 @@ get_loglikelihood(model, data, parameters; filter = :kalman) | filter | models | likelihood | differentiable | measurement error | smoothing | relative cost | |---|---|---|---|---|---|---| -| `:kalman` | linear (`:first_order`) | exact | yes | optional (incl. correlated) | yes | 1× | -| `:inversion` | linear and nonlinear | exact given the shocks | yes | not available | no | ~1–10× | -| `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~10³× | -| `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~2× bootstrap | -| `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required | yes (genealogy) | ~5–10× bootstrap | +| `:kalman` | linear (`:first_order`) | exact | yes | optional (incl. correlated) | yes (Durbin–Koopman) | 1× | +| `:inversion` | linear and nonlinear | exact given the shocks | yes | not available | n/a (filtered = smoothed) | ~1–10× | +| `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~10³× | +| `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~2× bootstrap | +| `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~5–10× bootstrap | A short decision rule: From b32b2d0f3f4803dd12e6fdf76c08669f564738bc Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 21:38:56 +0200 Subject: [PATCH 16/24] Make the correlated-measurement-error path allocation-free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut looked up the Cholesky factor through a `Dict` keyed on the observed-row pattern, once per particle per period, and solved against a view of the scratch buffer. Both allocate: 87 MB per evaluation at N = 20,000 over 40 periods. The pattern is constant inside a period, so a one-entry memo guarded by an allocation-free comparison skips the dictionary entirely, and the triangular solve is a hand-written forward substitution over a plain matrix — for the handful of observables a DSGE has, faster than dispatching to BLAS and with no view to heap-allocate. `vᵀH⁻¹v = ‖L⁻¹v‖²`, so the substitution produces the solve and the quadratic form in one pass. 87 MB → 20 KiB, same likelihood. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- src/filter/particle.jl | 90 +++-- test/Guerrieri_Iacoviello_2017.jl | 381 ++++++++++++++++++++ test/Guerrieri_Iacoviello_2017.mod | 545 +++++++++++++++++++++++++++++ test/NAWM_EAUS_2008_parameters.csv | 148 ++++++++ test/SW07_nonlinear.jl | 211 +++++++++++ test/SW07_nonlinear.mod | 261 ++++++++++++++ 6 files changed, 1613 insertions(+), 23 deletions(-) create mode 100644 test/Guerrieri_Iacoviello_2017.jl create mode 100644 test/Guerrieri_Iacoviello_2017.mod create mode 100644 test/NAWM_EAUS_2008_parameters.csv create mode 100644 test/SW07_nonlinear.jl create mode 100644 test/SW07_nonlinear.mod diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 33a6c4be5..47282f2fc 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -299,49 +299,94 @@ end # missing-data pattern and cache it; with complete data that is a single # factorisation for the whole sample. `DenseMeasurementError` is then accepted # anywhere `me_var` is, by dispatch, leaving the diagonal path untouched. -struct DenseMeasurementError +mutable struct DenseMeasurementError H::Matrix{Float64} - factors::Dict{Vector{Int},Tuple{ℒ.Cholesky{Float64,Matrix{Float64}},Float64}} - buf::Vector{Float64} # innovation scratch, sized to the number of observables + # rows pattern → (lower Cholesky factor of H[rows, rows], log det H[rows, rows]). + # Kept as a plain matrix rather than a `Cholesky`: the solve below is a hand + # written forward substitution, which for the handful of observables a DSGE + # has is both faster and allocation-free (a triangular `ldiv!` on a view of + # the scratch buffer heap-allocates the view, once per particle per period). + factors::Dict{Vector{Int},Tuple{Matrix{Float64},Float64}} + buf::Vector{Float64} # innovation scratch, sized to the current pattern + # The row pattern is constant across a period's inner loop, so the dictionary + # lookup (which needs a freshly allocated key vector) is guarded by a + # one-entry memo on the last pattern seen. + last_rows::Vector{Int} + last_L::Matrix{Float64} + last_logdet::Float64 end function DenseMeasurementError(H::AbstractMatrix{<:Real}) Hf = Matrix{Float64}(H) return DenseMeasurementError(Hf, - Dict{Vector{Int},Tuple{ℒ.Cholesky{Float64,Matrix{Float64}},Float64}}(), - Vector{Float64}(undef, size(Hf, 1))) + Dict{Vector{Int},Tuple{Matrix{Float64},Float64}}(), + Float64[], + Int[], + zeros(Float64, 0, 0), + 0.0) end -# Cholesky of H[rows, rows] and log det H[rows, rows], memoised on the row pattern. -@inline function me_factor(me::DenseMeasurementError, rows) +# Allocation-free comparison of the memoised pattern against the current `rows` +# (which may be a Vector, a range, or any iterable of indices). +@inline function same_rows(last::Vector{Int}, rows) + length(last) == length(rows) || return false + @inbounds for (k, r) in enumerate(rows) + last[k] == r || return false + end + return true +end + +# Point the memo at the factorisation for `rows`, computing it on first sight. +@inline function me_sync!(me::DenseMeasurementError, rows) + same_rows(me.last_rows, rows) && return nothing + key = collect(Int, rows) cached = get(me.factors, key, nothing) - cached === nothing || return cached - Hsub = me.H[key, key] - F = ℒ.cholesky(ℒ.Symmetric(Hsub)) - ld = 2 * sum(log, ℒ.diag(F.U)) - me.factors[key] = (F, ld) - return (F, ld) + if cached === nothing + F = ℒ.cholesky(ℒ.Symmetric(me.H[key, key])) + cached = (Matrix{Float64}(F.L), 2 * sum(log, ℒ.diag(F.U))) + me.factors[key] = cached + end + me.last_rows = key + me.last_L = cached[1] + me.last_logdet = cached[2] + resize!(me.buf, length(key)) + return nothing end # vᵀH⁻¹v over the observed rows. `Inf` on a non-finite prediction, matching the # diagonal version's contract (an impossible particle gets zero weight). +# vᵀH⁻¹v = ‖L⁻¹v‖², so one forward substitution gives both the solve and the norm. @inline function particle_quadratic_form(full::AbstractVector, data_col, observables_index, me::DenseMeasurementError, rows) - F, _ = me_factor(me, rows) - v = @view me.buf[1:length(rows)] + me_sync!(me, rows) + v = me.buf @inbounds for (k, r) in enumerate(rows) f = full[observables_index[r]] isfinite(f) || return Inf v[k] = data_col[r] - f end - ℒ.ldiv!(F.L, v) # v ← L⁻¹v, so ‖v‖² = original vᵀH⁻¹v - return sum(abs2, v) + return dense_me_quadform!(v, me.last_L) +end + +# In-place forward substitution L y = v, returning ‖y‖². +@inline function dense_me_quadform!(v::Vector{Float64}, L::Matrix{Float64}) + q = 0.0 + @inbounds for i in eachindex(v) + acc = v[i] + for j in 1:i-1 + acc -= L[i, j] * v[j] + end + y = acc / L[i, i] + v[i] = y + q += y * y + end + return q end @inline function particle_measurement_logZ(me::DenseMeasurementError, rows, log2pi::Float64) - _, ld = me_factor(me, rows) - return -0.5 * (length(rows) * log2pi + ld) + me_sync!(me, rows) + return -0.5 * (length(rows) * log2pi + me.last_logdet) end @inline function particle_log_measurement_density(full::AbstractVector, data_col, observables_index, @@ -1296,16 +1341,15 @@ end # Same, for a correlated H: gather the innovation, then one triangular solve. @inline function linear_quadform_col(X::Matrix{Float64}, p::Int, data_col, observables_index, me::DenseMeasurementError, rows) - F, _ = me_factor(me, rows) - v = @view me.buf[1:length(rows)] + me_sync!(me, rows) + v = me.buf @inbounds for k in eachindex(rows) r = rows[k] f = X[observables_index[r], p] isfinite(f) || return Inf v[k] = data_col[r] - f end - ℒ.ldiv!(F.L, v) - return sum(abs2, v) + return dense_me_quadform!(v, me.last_L) end # Copy column `src` of `X` into column `dst` of `Y` (contiguous, allocation-free). diff --git a/test/Guerrieri_Iacoviello_2017.jl b/test/Guerrieri_Iacoviello_2017.jl new file mode 100644 index 000000000..e436278a9 --- /dev/null +++ b/test/Guerrieri_Iacoviello_2017.jl @@ -0,0 +1,381 @@ +using MacroModelling + +@model Guerrieri_Iacoviello_2017 begin + c[0] + c1[0] + ik[0] = y[0] + + uc[0] = BETA * r[0] / dp[1] * uc[1] + + uc[0] * w[0] / xw[0] = az[0] * n[0] ^ ETA + + uc[0] * q[0] = uh[0] + BETA * uc[1] * q[1] + + c1[0] + q[0] * (h1[0] - h1[-1]) + r[-1] * b[-1] / dp[0] = w1[0] * n1[0] + b[0] + INDTR * log(ap[0]) + + uc1[0] * (1 - lm[0]) = BETA1 * (r[0] / dp[1] - RHOD * lm[1] / dp[1]) * uc1[1] + + w1[0] * uc1[0] / xw1[0] = az[0] * n1[0] ^ ETA + + q[0] * uc1[0] = uh1[0] + BETA1 * q[1] * uc1[1] + lm[0] * q[0] * uc1[0] * (1 - RHOD) * M + + y[0] = n[0] ^ ((1 - ALPHA) * (1 - SIGMA)) * n1[0] ^ ((1 - ALPHA) * SIGMA) * k[-1] ^ ALPHA + + (1 - SIGMA) * y[0] * (1 - ALPHA) = w[0] * n[0] * xp[0] + + SIGMA * y[0] * (1 - ALPHA) = w1[0] * n1[0] * xp[0] + + log(dp[0] / PIBAR) - LAGP * log(dp[-1] / PIBAR) = BETA * (log(dp[1] / PIBAR) - log(dp[0] / PIBAR) * LAGP) - (1 - TETAP) * (1 - BETA * TETAP) / TETAP * log(xp[0] / XP_SS) + log(ap[0]) * (1 - INDTR) + + log(dw[0] / PIBAR) - LAGW * log(dw[-1] / PIBAR) = BETA * (log(dw[1] / PIBAR) - log(dw[0] / PIBAR) * LAGW) - (1 - TETAW) * (1 - BETA * TETAW) / TETAW * log(xw[0] / XW_SS) + log(aw[0]) + + log(dw1[0] / PIBAR) - LAGW * log(dw1[-1] / PIBAR) = log(aw[0]) + BETA * (log(dw1[1] / PIBAR) - LAGW * log(dw1[0] / PIBAR)) - (1 - TETAW) * (1 - BETA * TETAW) / TETAW * log(xw1[0] / XW_SS) + + log(rnot[0]) = log(arr[0]) + (1 - TAYLOR_R) * log(PIBAR / BETA) + (1 - TAYLOR_R) * TAYLOR_Q / 4 * log(q[0] / q[-1]) + (1 - TAYLOR_R) * TAYLOR_Y * log(y[0] / lly) + TAYLOR_R * log(r[-1]) + (1 - TAYLOR_R) * TAYLOR_P * (log(dp[0] / PIBAR) * 0.25 + log(dp[-1] / PIBAR) * 0.25 + 0.25 * log(AUX_ENDO_LAG_10_1[-1] / PIBAR) + 0.25 * log(AUX_ENDO_LAG_10_2[-1] / PIBAR)) + + uc[0] = (1 - EC) / (1 - BETA * EC) * (az[0] / (c[0] - EC * c[-1]) - BETA * EC * az[1] / (c[1] - c[0] * EC)) + + uc1[0] = (1 - EC) / (1 - BETA1 * EC) * (az[0] / (c1[0] - EC * c1[-1]) - EC * BETA1 * az[1] / (c1[1] - c1[0] * EC)) + + uh[0] = (1 - EH) / (1 - BETA * EH) * JEI * (az[0] * aj[0] / (1 - h1[0] - EH * (1 - h1[-1])) - EH * BETA * az[1] * aj[1] / (1 - h1[1] - EH * (1 - h1[0]))) + + uh1[0] = (1 - EH) * JEI / (1 - BETA1 * EH) * (az[0] * aj[0] / (h1[0] - h1[-1] * EH) - EH * BETA1 * az[1] * aj[1] / (h1[1] - h1[0] * EH)) + + uc[0] * qk[0] * (1 - PHIK * (ik[0] - ik[-1]) / llik) = uc[0] - uc[1] * BETA * PHIK * qk[1] * (ik[1] - ik[0]) / llik + + uc[0] * qk[0] / ak[0] = BETA * uc[1] * (rk[1] + qk[1] * (1 - DK) / ak[1]) + + k[0] / ak[0] = ik[0] + k[-1] * (1 - DK) / ak[0] + + y[0] * ALPHA = k[-1] * xp[0] * rk[0] + + dw[0] = w[0] * dp[0] / w[-1] + + dw1[0] = dp[0] * w1[0] / w1[-1] + + log(aj[0]) = RHO_J * log(aj[-1]) + z_j[0] + + z_j[0] = RHO_J2 * z_j[-1] + STD_J * eps_j[x] + + log(ak[0]) = RHO_K * log(ak[-1]) + STD_K * eps_k[x] + + log(ap[0]) = RHO_P * log(ap[-1]) + STD_P * eps_p[x] + + log(aw[0]) = RHO_W * log(aw[-1]) + STD_W * eps_w[x] + + log(arr[0]) = RHO_R * log(arr[-1]) + STD_R * eps_r[x] + + log(az[0]) = RHO_Z * log(az[-1]) + STD_Z * eps_z[x] + + chi__o__b__c__minus_____1_____l[0] = bnot[0] - b[0] + + chi__o__b__c__minus_____1_____r[0] = lm[0] + + Chi__o__b__c__minus____1___[0] = min(chi__o__b__c__minus_____1_____l[0],chi__o__b__c__minus_____1_____r[0]) + + Chi__o__b__c__minus____1___[0] - epsilon__o__b__c__minus____1___[0] = 0 + + bnot[0] = M * (1 - RHOD) * q[0] * h1[0] + b[-1] * RHOD / dp[0] + + maxlev[0] = b[0] - bnot[0] + + chi__o__b__c__plus_____2_____l[0] = RBAR - r[0] + + chi__o__b__c__plus_____2_____r[0] = rnot[0] - r[0] + + Chi__o__b__c__plus____2___[0] = max(chi__o__b__c__plus_____2_____l[0],chi__o__b__c__plus_____2_____r[0]) + + Chi__o__b__c__plus____2___[0] - epsilon__o__b__c__plus____2___[0] = 0 + + epsilon__o__b__c__minus____1___[0] = epsilon__o__b__c__minus_____1_____L_____minus__4__0___[0] + + epsilon__o__b__c__minus_____1_____L_____minus__0___[0] = active_o__b__c_shocks * epsilon__o__b__c__minus_____1________4__0___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1___[0] = epsilon__o__b__c__minus_____1_____L_____minus__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__9___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__8___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__7___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__4___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__6___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__5___[0] = epsilon__o__b__c__minus_____1_____L_____minus__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__5___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__6___[0] = epsilon__o__b__c__minus_____1_____L_____minus__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__4___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__7___[0] = epsilon__o__b__c__minus_____1_____L_____minus__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__3___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__8___[0] = epsilon__o__b__c__minus_____1_____L_____minus__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__2___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__9___[0] = epsilon__o__b__c__minus_____1_____L_____minus__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__1___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__0___[0] = epsilon__o__b__c__minus_____1_____L_____minus__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__0___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__1___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__9___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__2___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__8___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__3___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__7___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__4___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__6___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__5___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__5___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__6___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__4___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__7___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__3___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__8___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__2___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__1__9___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__1___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__0___[0] = epsilon__o__b__c__minus_____1_____L_____minus__1__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__0___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__1___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__9___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__2___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__8___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__3___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__7___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__4___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__6___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__5___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__5___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__6___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__4___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__7___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__3___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__8___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__2___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__2__9___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__1___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__0___[0] = epsilon__o__b__c__minus_____1_____L_____minus__2__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__0___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__1___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________9___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__2___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________8___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__3___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________7___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__4___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________6___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__5___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________5___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__6___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________4___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__7___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__8___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__3__9___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1___[x] + + epsilon__o__b__c__minus_____1_____L_____minus__4__0___[0] = epsilon__o__b__c__minus_____1_____L_____minus__3__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________0___[x] + + epsilon__o__b__c__plus____2___[0] = epsilon__o__b__c__plus_____2_____L_____minus__4__0___[0] + + epsilon__o__b__c__plus_____2_____L_____minus__0___[0] = active_o__b__c_shocks * epsilon__o__b__c__plus_____2________4__0___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1___[0] = epsilon__o__b__c__plus_____2_____L_____minus__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__9___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__8___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__7___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__4___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__6___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__5___[0] = epsilon__o__b__c__plus_____2_____L_____minus__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__5___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__6___[0] = epsilon__o__b__c__plus_____2_____L_____minus__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__4___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__7___[0] = epsilon__o__b__c__plus_____2_____L_____minus__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__3___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__8___[0] = epsilon__o__b__c__plus_____2_____L_____minus__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__2___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__9___[0] = epsilon__o__b__c__plus_____2_____L_____minus__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__1___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__0___[0] = epsilon__o__b__c__plus_____2_____L_____minus__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__0___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__1___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__9___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__2___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__8___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__3___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__7___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__4___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__6___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__5___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__5___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__6___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__4___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__7___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__3___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__8___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__2___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__1__9___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__1___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__0___[0] = epsilon__o__b__c__plus_____2_____L_____minus__1__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__0___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__1___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__9___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__2___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__8___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__3___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__7___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__4___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__6___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__5___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__5___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__6___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__4___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__7___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__3___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__8___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__2___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__2__9___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__1___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__0___[0] = epsilon__o__b__c__plus_____2_____L_____minus__2__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__0___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__1___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__0___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________9___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__2___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__1___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________8___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__3___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__2___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________7___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__4___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__3___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________6___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__5___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__4___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________5___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__6___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__5___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________4___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__7___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__6___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__8___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__7___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__3__9___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__8___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1___[x] + + epsilon__o__b__c__plus_____2_____L_____minus__4__0___[0] = epsilon__o__b__c__plus_____2_____L_____minus__3__9___[-1] + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________0___[x] + + AUX_ENDO_LAG_10_1[0] = dp[-1] + + AUX_ENDO_LAG_10_2[0] = AUX_ENDO_LAG_10_1[-1] + +end + + +@parameters Guerrieri_Iacoviello_2017 begin + RBAR = 1.0 + + BETA = 0.995 + + BETA1 = 0.9921849949330452 + + EC = 0.6841688730310923 + + EH = 0.8798650668795864 + + ETA = 1.0 + + JEI = 0.04 + + M = 0.9 + + ALPHA = 0.3 + + PHIK = 4.120924218703865 + + DK = 0.025 + + LAGP = 0.0 + + LAGW = 0.0 + + PIBAR = 1.005 + + INDTR = 0.0 + + SIGMA = 0.5012798413194606 + + TAYLOR_P = 1.719559906725518 + + TAYLOR_Q = 0.0 + + TAYLOR_R = 0.5508743735338286 + + TAYLOR_Y = 0.09436959071018983 + + TETAP = 0.9182319022631061 + + TETAW = 0.9162909334165672 + + XP_SS = 1.2 + + XW_SS = 1.2 + + RHO_J = 0.983469150669198 + + RHO_K = 0.7859395713107814 + + RHO_P = 0.0 + + RHO_R = 0.623204934949152 + + RHO_W = 0.0 + + RHO_Z = 0.7555575007590176 + + STD_J = 0.07366860797541266 + + STD_K = 0.03601489154765812 + + STD_P = 0.002964296803248907 + + STD_R = 0.001315097718876929 + + STD_W = 0.00996414482032244 + + STD_Z = 0.01633680112129254 + + RHO_J2 = 0.0 + + RHOD = 0.6945068431131589 + + active_o__b__c_shocks = 0.0 + + llr = 1 / BETA + + llrk = llr - (1 - DK) + + llxp = XP_SS + + llxw = XW_SS + + llxw1 = XW_SS + + lllm = (1 - BETA1 / BETA) / (1 - (BETA1 * RHOD) / PIBAR) + + QHTOC = JEI / (1 - BETA) + + QH1TOC1 = JEI / ((1 - BETA1) - lllm * M * (1 - RHOD)) + + KTOY = ALPHA / (llxp * llrk) + + BTOQH1 = (M * (1 - RHOD)) / (1 - RHOD / PIBAR) + + C1TOY = (((1 - ALPHA) * SIGMA) / (1 + (1 / BETA - 1) * BTOQH1 * QH1TOC1)) * (1 / llxp) + + CTOY = (1 - C1TOY) - DK * KTOY + + lln = (((1 - SIGMA) * (1 - ALPHA)) / (llxp * llxw * CTOY)) ^ (1 / (1 + ETA)) + + lln1 = ((SIGMA * (1 - ALPHA)) / (llxp * llxw1 * C1TOY)) ^ (1 / (1 + ETA)) + + lly = KTOY^(ALPHA/(1-ALPHA))*lln^(1-SIGMA)*lln1^SIGMA + + llctot = lly - DK * KTOY * lly + + llik = KTOY*DK*lly + + llk = KTOY * lly + + llq = QHTOC * CTOY * lly + QH1TOC1 * C1TOY * lly + +end + diff --git a/test/Guerrieri_Iacoviello_2017.mod b/test/Guerrieri_Iacoviello_2017.mod new file mode 100644 index 000000000..dfef9cc36 --- /dev/null +++ b/test/Guerrieri_Iacoviello_2017.mod @@ -0,0 +1,545 @@ +var +aj ak ap arr aw az b bnot c c1 dp dw dw1 h1 ik k lm maxlev n n1 q qk r rk rnot uc uc1 uh uh1 w w1 xp xw xw1 y z_j Chi__o__b__c__plus____2___ Chi__o__b__c__minus____1___ chi__o__b__c__plus_____2_____r chi__o__b__c__plus_____2_____l chi__o__b__c__minus_____1_____r chi__o__b__c__minus_____1_____l epsilon__o__b__c__plus____2___ epsilon__o__b__c__plus_____2_____L_____minus__2__2___ epsilon__o__b__c__plus_____2_____L_____minus__2__3___ epsilon__o__b__c__plus_____2_____L_____minus__2__1___ epsilon__o__b__c__plus_____2_____L_____minus__2__0___ epsilon__o__b__c__plus_____2_____L_____minus__2__4___ epsilon__o__b__c__plus_____2_____L_____minus__2__5___ epsilon__o__b__c__plus_____2_____L_____minus__2__6___ epsilon__o__b__c__plus_____2_____L_____minus__2__7___ epsilon__o__b__c__plus_____2_____L_____minus__2__8___ epsilon__o__b__c__plus_____2_____L_____minus__2__9___ epsilon__o__b__c__plus_____2_____L_____minus__2___ epsilon__o__b__c__plus_____2_____L_____minus__3__2___ epsilon__o__b__c__plus_____2_____L_____minus__3__3___ epsilon__o__b__c__plus_____2_____L_____minus__3__1___ epsilon__o__b__c__plus_____2_____L_____minus__3__0___ epsilon__o__b__c__plus_____2_____L_____minus__3__4___ epsilon__o__b__c__plus_____2_____L_____minus__3__5___ epsilon__o__b__c__plus_____2_____L_____minus__3__6___ epsilon__o__b__c__plus_____2_____L_____minus__3__7___ epsilon__o__b__c__plus_____2_____L_____minus__3__8___ epsilon__o__b__c__plus_____2_____L_____minus__3__9___ epsilon__o__b__c__plus_____2_____L_____minus__3___ epsilon__o__b__c__plus_____2_____L_____minus__1__2___ epsilon__o__b__c__plus_____2_____L_____minus__1__3___ epsilon__o__b__c__plus_____2_____L_____minus__1__1___ epsilon__o__b__c__plus_____2_____L_____minus__1__0___ epsilon__o__b__c__plus_____2_____L_____minus__1__4___ epsilon__o__b__c__plus_____2_____L_____minus__1__5___ epsilon__o__b__c__plus_____2_____L_____minus__1__6___ epsilon__o__b__c__plus_____2_____L_____minus__1__7___ epsilon__o__b__c__plus_____2_____L_____minus__1__8___ epsilon__o__b__c__plus_____2_____L_____minus__1__9___ epsilon__o__b__c__plus_____2_____L_____minus__1___ epsilon__o__b__c__plus_____2_____L_____minus__0___ epsilon__o__b__c__plus_____2_____L_____minus__4__0___ epsilon__o__b__c__plus_____2_____L_____minus__4___ epsilon__o__b__c__plus_____2_____L_____minus__5___ epsilon__o__b__c__plus_____2_____L_____minus__6___ epsilon__o__b__c__plus_____2_____L_____minus__7___ epsilon__o__b__c__plus_____2_____L_____minus__8___ epsilon__o__b__c__plus_____2_____L_____minus__9___ epsilon__o__b__c__minus____1___ epsilon__o__b__c__minus_____1_____L_____minus__2__2___ epsilon__o__b__c__minus_____1_____L_____minus__2__3___ epsilon__o__b__c__minus_____1_____L_____minus__2__1___ epsilon__o__b__c__minus_____1_____L_____minus__2__0___ epsilon__o__b__c__minus_____1_____L_____minus__2__4___ epsilon__o__b__c__minus_____1_____L_____minus__2__5___ epsilon__o__b__c__minus_____1_____L_____minus__2__6___ epsilon__o__b__c__minus_____1_____L_____minus__2__7___ epsilon__o__b__c__minus_____1_____L_____minus__2__8___ epsilon__o__b__c__minus_____1_____L_____minus__2__9___ epsilon__o__b__c__minus_____1_____L_____minus__2___ epsilon__o__b__c__minus_____1_____L_____minus__3__2___ epsilon__o__b__c__minus_____1_____L_____minus__3__3___ epsilon__o__b__c__minus_____1_____L_____minus__3__1___ epsilon__o__b__c__minus_____1_____L_____minus__3__0___ epsilon__o__b__c__minus_____1_____L_____minus__3__4___ epsilon__o__b__c__minus_____1_____L_____minus__3__5___ epsilon__o__b__c__minus_____1_____L_____minus__3__6___ epsilon__o__b__c__minus_____1_____L_____minus__3__7___ epsilon__o__b__c__minus_____1_____L_____minus__3__8___ epsilon__o__b__c__minus_____1_____L_____minus__3__9___ epsilon__o__b__c__minus_____1_____L_____minus__3___ epsilon__o__b__c__minus_____1_____L_____minus__1__2___ epsilon__o__b__c__minus_____1_____L_____minus__1__3___ epsilon__o__b__c__minus_____1_____L_____minus__1__1___ epsilon__o__b__c__minus_____1_____L_____minus__1__0___ epsilon__o__b__c__minus_____1_____L_____minus__1__4___ epsilon__o__b__c__minus_____1_____L_____minus__1__5___ epsilon__o__b__c__minus_____1_____L_____minus__1__6___ epsilon__o__b__c__minus_____1_____L_____minus__1__7___ epsilon__o__b__c__minus_____1_____L_____minus__1__8___ epsilon__o__b__c__minus_____1_____L_____minus__1__9___ epsilon__o__b__c__minus_____1_____L_____minus__1___ epsilon__o__b__c__minus_____1_____L_____minus__0___ epsilon__o__b__c__minus_____1_____L_____minus__4__0___ epsilon__o__b__c__minus_____1_____L_____minus__4___ epsilon__o__b__c__minus_____1_____L_____minus__5___ epsilon__o__b__c__minus_____1_____L_____minus__6___ epsilon__o__b__c__minus_____1_____L_____minus__7___ epsilon__o__b__c__minus_____1_____L_____minus__8___ epsilon__o__b__c__minus_____1_____L_____minus__9___ ; + +varexo +eps_j eps_k eps_p eps_r eps_w eps_z epsilon__o__b__c__plus_____2________2__2___ epsilon__o__b__c__plus_____2________2__3___ epsilon__o__b__c__plus_____2________2__1___ epsilon__o__b__c__plus_____2________2__0___ epsilon__o__b__c__plus_____2________2__4___ epsilon__o__b__c__plus_____2________2__5___ epsilon__o__b__c__plus_____2________2__6___ epsilon__o__b__c__plus_____2________2__7___ epsilon__o__b__c__plus_____2________2__8___ epsilon__o__b__c__plus_____2________2__9___ epsilon__o__b__c__plus_____2________2___ epsilon__o__b__c__plus_____2________3__2___ epsilon__o__b__c__plus_____2________3__3___ epsilon__o__b__c__plus_____2________3__1___ epsilon__o__b__c__plus_____2________3__0___ epsilon__o__b__c__plus_____2________3__4___ epsilon__o__b__c__plus_____2________3__5___ epsilon__o__b__c__plus_____2________3__6___ epsilon__o__b__c__plus_____2________3__7___ epsilon__o__b__c__plus_____2________3__8___ epsilon__o__b__c__plus_____2________3__9___ epsilon__o__b__c__plus_____2________3___ epsilon__o__b__c__plus_____2________1__2___ epsilon__o__b__c__plus_____2________1__3___ epsilon__o__b__c__plus_____2________1__1___ epsilon__o__b__c__plus_____2________1__0___ epsilon__o__b__c__plus_____2________1__4___ epsilon__o__b__c__plus_____2________1__5___ epsilon__o__b__c__plus_____2________1__6___ epsilon__o__b__c__plus_____2________1__7___ epsilon__o__b__c__plus_____2________1__8___ epsilon__o__b__c__plus_____2________1__9___ epsilon__o__b__c__plus_____2________1___ epsilon__o__b__c__plus_____2________0___ epsilon__o__b__c__plus_____2________4__0___ epsilon__o__b__c__plus_____2________4___ epsilon__o__b__c__plus_____2________5___ epsilon__o__b__c__plus_____2________6___ epsilon__o__b__c__plus_____2________7___ epsilon__o__b__c__plus_____2________8___ epsilon__o__b__c__plus_____2________9___ epsilon__o__b__c__minus_____1________2__2___ epsilon__o__b__c__minus_____1________2__3___ epsilon__o__b__c__minus_____1________2__1___ epsilon__o__b__c__minus_____1________2__0___ epsilon__o__b__c__minus_____1________2__4___ epsilon__o__b__c__minus_____1________2__5___ epsilon__o__b__c__minus_____1________2__6___ epsilon__o__b__c__minus_____1________2__7___ epsilon__o__b__c__minus_____1________2__8___ epsilon__o__b__c__minus_____1________2__9___ epsilon__o__b__c__minus_____1________2___ epsilon__o__b__c__minus_____1________3__2___ epsilon__o__b__c__minus_____1________3__3___ epsilon__o__b__c__minus_____1________3__1___ epsilon__o__b__c__minus_____1________3__0___ epsilon__o__b__c__minus_____1________3__4___ epsilon__o__b__c__minus_____1________3__5___ epsilon__o__b__c__minus_____1________3__6___ epsilon__o__b__c__minus_____1________3__7___ epsilon__o__b__c__minus_____1________3__8___ epsilon__o__b__c__minus_____1________3__9___ epsilon__o__b__c__minus_____1________3___ epsilon__o__b__c__minus_____1________1__2___ epsilon__o__b__c__minus_____1________1__3___ epsilon__o__b__c__minus_____1________1__1___ epsilon__o__b__c__minus_____1________1__0___ epsilon__o__b__c__minus_____1________1__4___ epsilon__o__b__c__minus_____1________1__5___ epsilon__o__b__c__minus_____1________1__6___ epsilon__o__b__c__minus_____1________1__7___ epsilon__o__b__c__minus_____1________1__8___ epsilon__o__b__c__minus_____1________1__9___ epsilon__o__b__c__minus_____1________1___ epsilon__o__b__c__minus_____1________0___ epsilon__o__b__c__minus_____1________4__0___ epsilon__o__b__c__minus_____1________4___ epsilon__o__b__c__minus_____1________5___ epsilon__o__b__c__minus_____1________6___ epsilon__o__b__c__minus_____1________7___ epsilon__o__b__c__minus_____1________8___ epsilon__o__b__c__minus_____1________9___ ; + +parameters +ALPHA BETA BETA1 DK EC EH ETA INDTR JEI LAGP LAGW M PHIK PIBAR RBAR RHOD RHO_J RHO_J2 RHO_K RHO_P RHO_R RHO_W RHO_Z SIGMA STD_J STD_K STD_P STD_R STD_W STD_Z TAYLOR_P TAYLOR_Q TAYLOR_R TAYLOR_Y TETAP TETAW XP_SS XW_SS active_o__b__c_shocks llik lly ; + +% Parameter definitions: + RBAR = 1.0; + BETA = 0.995; + BETA1 = 0.9921849949330452; + EC = 0.6841688730310923; + EH = 0.8798650668795864; + ETA = 1.0; + JEI = 0.04; + M = 0.9; + ALPHA = 0.3; + PHIK = 4.120924218703865; + DK = 0.025; + LAGP = 0.0; + LAGW = 0.0; + PIBAR = 1.005; + INDTR = 0.0; + SIGMA = 0.5012798413194606; + TAYLOR_P = 1.719559906725518; + TAYLOR_Q = 0.0; + TAYLOR_R = 0.5508743735338286; + TAYLOR_Y = 0.09436959071018983; + TETAP = 0.9182319022631061; + TETAW = 0.9162909334165672; + XP_SS = 1.2; + XW_SS = 1.2; + RHO_J = 0.983469150669198; + RHO_K = 0.7859395713107814; + RHO_P = 0.0; + RHO_R = 0.623204934949152; + RHO_W = 0.0; + RHO_Z = 0.7555575007590176; + STD_J = 0.07366860797541266; + STD_K = 0.03601489154765812; + STD_P = 0.002964296803248907; + STD_R = 0.001315097718876929; + STD_W = 0.00996414482032244; + STD_Z = 0.01633680112129254; + RHO_J2 = 0.0; + RHOD = 0.6945068431131589; + active_o__b__c_shocks = 0.0; + llr = 1 / BETA; + llrk = llr - (1 - DK); + llxp = XP_SS; + llxw = XW_SS; + llxw1 = XW_SS; + lllm = (1 - BETA1 / BETA) / (1 - (BETA1 * RHOD) / PIBAR); + QHTOC = JEI / (1 - BETA); + QH1TOC1 = JEI / ((1 - BETA1) - lllm * M * (1 - RHOD)); + KTOY = ALPHA / (llxp * llrk); + BTOQH1 = (M * (1 - RHOD)) / (1 - RHOD / PIBAR); + C1TOY = (((1 - ALPHA) * SIGMA) / (1 + (1 / BETA - 1) * BTOQH1 * QH1TOC1)) * (1 / llxp); + CTOY = (1 - C1TOY) - DK * KTOY; + lln = (((1 - SIGMA) * (1 - ALPHA)) / (llxp * llxw * CTOY)) ^ (1 / (1 + ETA)); + lln1 = ((SIGMA * (1 - ALPHA)) / (llxp * llxw1 * C1TOY)) ^ (1 / (1 + ETA)); + lly = KTOY ^ (ALPHA / (1 - ALPHA)) * lln ^ (1 - SIGMA) * lln1 ^ SIGMA; + llctot = lly - DK * KTOY * lly; + llik = KTOY * DK * lly; + llk = KTOY * lly; + llq = QHTOC * CTOY * lly + QH1TOC1 * C1TOY * lly; + +model; + c(0) + c1(0) + ik(0) = y(0); + + uc(0) = ((BETA * r(0)) / dp(1)) * uc(1); + + (uc(0) * w(0)) / xw(0) = az(0) * n(0) ^ ETA; + + uc(0) * q(0) = uh(0) + uc(1) * BETA * q(1); + + c1(0) + q(0) * (h1(0) - h1(-1)) + (r(-1) * b(-1)) / dp(0) = w1(0) * n1(0) + b(0) + INDTR * log(ap(0)); + + uc1(0) * (1 - lm(0)) = BETA1 * (r(0) / dp(1) - (RHOD * lm(1)) / dp(1)) * uc1(1); + + (w1(0) * uc1(0)) / xw1(0) = az(0) * n1(0) ^ ETA; + + q(0) * uc1(0) = uh1(0) + uc1(1) * q(1) * BETA1 + q(0) * uc1(0) * lm(0) * (1 - RHOD) * M; + + y(0) = n(0) ^ ((1 - ALPHA) * (1 - SIGMA)) * n1(0) ^ ((1 - ALPHA) * SIGMA) * k(-1) ^ ALPHA; + + y(0) * (1 - ALPHA) * (1 - SIGMA) = n(0) * w(0) * xp(0); + + y(0) * (1 - ALPHA) * SIGMA = n1(0) * w1(0) * xp(0); + + log(dp(0) / PIBAR) - LAGP * log(dp(-1) / PIBAR) = (BETA * (log(dp(1) / PIBAR) - log(dp(0) / PIBAR) * LAGP) - (((1 - TETAP) * (1 - BETA * TETAP)) / TETAP) * log(xp(0) / XP_SS)) + log(ap(0)) * (1 - INDTR); + + log(dw(0) / PIBAR) - LAGW * log(dw(-1) / PIBAR) = (BETA * (log(dw(1) / PIBAR) - log(dw(0) / PIBAR) * LAGW) - (((1 - TETAW) * (1 - BETA * TETAW)) / TETAW) * log(xw(0) / XW_SS)) + log(aw(0)); + + log(dw1(0) / PIBAR) - LAGW * log(dw1(-1) / PIBAR) = (log(aw(0)) + BETA * (log(dw1(1) / PIBAR) - LAGW * log(dw1(0) / PIBAR))) - (((1 - TETAW) * (1 - BETA * TETAW)) / TETAW) * log(xw1(0) / XW_SS); + + log(rnot(0)) = TAYLOR_R * log(r(-1)) + (1 - TAYLOR_R) * TAYLOR_P * (log(dp(0) / PIBAR) * 0.25 + 0.25 * log(dp(-1) / PIBAR) + 0.25 * log(dp(-2) / PIBAR) + 0.25 * log(dp(-3) / PIBAR)) + (1 - TAYLOR_R) * TAYLOR_Y * log(y(0) / lly) + (((1 - TAYLOR_R) * TAYLOR_Q) / 4) * log(q(0) / q(-1)) + (1 - TAYLOR_R) * log(PIBAR / BETA) + log(arr(0)); + + uc(0) = ((1 - EC) / (1 - BETA * EC)) * (az(0) / (c(0) - EC * c(-1)) - (BETA * EC * az(1)) / (c(1) - c(0) * EC)); + + uc1(0) = ((1 - EC) / (1 - BETA1 * EC)) * (az(0) / (c1(0) - EC * c1(-1)) - (az(1) * BETA1 * EC) / (c1(1) - c1(0) * EC)); + + uh(0) = ((1 - EH) / (1 - BETA * EH)) * JEI * ((az(0) * aj(0)) / ((1 - h1(0)) - EH * (1 - h1(-1))) - (az(1) * BETA * EH * aj(1)) / ((1 - h1(1)) - EH * (1 - h1(0)))); + + uh1(0) = ((JEI * (1 - EH)) / (1 - BETA1 * EH)) * ((az(0) * aj(0)) / (h1(0) - h1(-1) * EH) - (aj(1) * az(1) * BETA1 * EH) / (h1(1) - h1(0) * EH)); + + uc(0) * qk(0) * (1 - (PHIK * (ik(0) - ik(-1))) / llik) = uc(0) - (PHIK * BETA * uc(1) * qk(1) * (ik(1) - ik(0))) / llik; + + (uc(0) * qk(0)) / ak(0) = BETA * uc(1) * (rk(1) + (qk(1) * (1 - DK)) / ak(1)); + + k(0) / ak(0) = ik(0) + (k(-1) * (1 - DK)) / ak(0); + + y(0) * ALPHA = k(-1) * xp(0) * rk(0); + + dw(0) = (w(0) * dp(0)) / w(-1); + + dw1(0) = (dp(0) * w1(0)) / w1(-1); + + log(aj(0)) = RHO_J * log(aj(-1)) + z_j(0); + + z_j(0) = RHO_J2 * z_j(-1) + STD_J * eps_j; + + log(ak(0)) = RHO_K * log(ak(-1)) + STD_K * eps_k; + + log(ap(0)) = RHO_P * log(ap(-1)) + STD_P * eps_p; + + log(aw(0)) = RHO_W * log(aw(-1)) + STD_W * eps_w; + + log(arr(0)) = RHO_R * log(arr(-1)) + STD_R * eps_r; + + log(az(0)) = RHO_Z * log(az(-1)) + STD_Z * eps_z; + + chi__o__b__c__minus_____1_____l(0) = (bnot(0) - b(0)) - 0; + + chi__o__b__c__minus_____1_____r(0) = lm(0) - 0; + + Chi__o__b__c__minus____1___(0) = min(chi__o__b__c__minus_____1_____l(0), chi__o__b__c__minus_____1_____r(0)); + + Chi__o__b__c__minus____1___(0) - epsilon__o__b__c__minus____1___(0); + + bnot(0) = h1(0) * q(0) * (1 - RHOD) * M + (b(-1) * RHOD) / dp(0); + + maxlev(0) = b(0) - bnot(0); + + chi__o__b__c__plus_____2_____l(0) = RBAR - r(0); + + chi__o__b__c__plus_____2_____r(0) = rnot(0) - r(0); + + Chi__o__b__c__plus____2___(0) = max(chi__o__b__c__plus_____2_____l(0), chi__o__b__c__plus_____2_____r(0)); + + Chi__o__b__c__plus____2___(0) - epsilon__o__b__c__plus____2___(0); + + epsilon__o__b__c__minus____1___(0) = epsilon__o__b__c__minus_____1_____L_____minus__4__0___(0); + + epsilon__o__b__c__minus_____1_____L_____minus__0___(0) = active_o__b__c_shocks * epsilon__o__b__c__minus_____1________4__0___; + + epsilon__o__b__c__minus_____1_____L_____minus__1___(0) = epsilon__o__b__c__minus_____1_____L_____minus__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__9___; + + epsilon__o__b__c__minus_____1_____L_____minus__2___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__8___; + + epsilon__o__b__c__minus_____1_____L_____minus__3___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__7___; + + epsilon__o__b__c__minus_____1_____L_____minus__4___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__6___; + + epsilon__o__b__c__minus_____1_____L_____minus__5___(0) = epsilon__o__b__c__minus_____1_____L_____minus__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__5___; + + epsilon__o__b__c__minus_____1_____L_____minus__6___(0) = epsilon__o__b__c__minus_____1_____L_____minus__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__4___; + + epsilon__o__b__c__minus_____1_____L_____minus__7___(0) = epsilon__o__b__c__minus_____1_____L_____minus__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__3___; + + epsilon__o__b__c__minus_____1_____L_____minus__8___(0) = epsilon__o__b__c__minus_____1_____L_____minus__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__2___; + + epsilon__o__b__c__minus_____1_____L_____minus__9___(0) = epsilon__o__b__c__minus_____1_____L_____minus__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__1___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__0___(0) = epsilon__o__b__c__minus_____1_____L_____minus__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3__0___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__1___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__9___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__2___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__8___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__3___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__7___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__4___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__6___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__5___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__5___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__6___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__4___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__7___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__3___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__8___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__2___; + + epsilon__o__b__c__minus_____1_____L_____minus__1__9___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__1___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__0___(0) = epsilon__o__b__c__minus_____1_____L_____minus__1__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2__0___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__1___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__9___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__2___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__8___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__3___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__7___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__4___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__6___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__5___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__5___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__6___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__4___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__7___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__3___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__8___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__2___; + + epsilon__o__b__c__minus_____1_____L_____minus__2__9___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__1___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__0___(0) = epsilon__o__b__c__minus_____1_____L_____minus__2__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1__0___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__1___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________9___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__2___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________8___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__3___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________7___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__4___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________6___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__5___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________5___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__6___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________4___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__7___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________3___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__8___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________2___; + + epsilon__o__b__c__minus_____1_____L_____minus__3__9___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________1___; + + epsilon__o__b__c__minus_____1_____L_____minus__4__0___(0) = epsilon__o__b__c__minus_____1_____L_____minus__3__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__minus_____1________0___; + + epsilon__o__b__c__plus____2___(0) = epsilon__o__b__c__plus_____2_____L_____minus__4__0___(0); + + epsilon__o__b__c__plus_____2_____L_____minus__0___(0) = active_o__b__c_shocks * epsilon__o__b__c__plus_____2________4__0___; + + epsilon__o__b__c__plus_____2_____L_____minus__1___(0) = epsilon__o__b__c__plus_____2_____L_____minus__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__9___; + + epsilon__o__b__c__plus_____2_____L_____minus__2___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__8___; + + epsilon__o__b__c__plus_____2_____L_____minus__3___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__7___; + + epsilon__o__b__c__plus_____2_____L_____minus__4___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__6___; + + epsilon__o__b__c__plus_____2_____L_____minus__5___(0) = epsilon__o__b__c__plus_____2_____L_____minus__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__5___; + + epsilon__o__b__c__plus_____2_____L_____minus__6___(0) = epsilon__o__b__c__plus_____2_____L_____minus__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__4___; + + epsilon__o__b__c__plus_____2_____L_____minus__7___(0) = epsilon__o__b__c__plus_____2_____L_____minus__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__3___; + + epsilon__o__b__c__plus_____2_____L_____minus__8___(0) = epsilon__o__b__c__plus_____2_____L_____minus__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__2___; + + epsilon__o__b__c__plus_____2_____L_____minus__9___(0) = epsilon__o__b__c__plus_____2_____L_____minus__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__1___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__0___(0) = epsilon__o__b__c__plus_____2_____L_____minus__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3__0___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__1___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__9___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__2___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__8___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__3___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__7___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__4___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__6___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__5___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__5___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__6___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__4___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__7___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__3___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__8___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__2___; + + epsilon__o__b__c__plus_____2_____L_____minus__1__9___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__1___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__0___(0) = epsilon__o__b__c__plus_____2_____L_____minus__1__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2__0___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__1___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__9___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__2___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__8___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__3___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__7___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__4___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__6___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__5___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__5___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__6___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__4___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__7___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__3___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__8___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__2___; + + epsilon__o__b__c__plus_____2_____L_____minus__2__9___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__1___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__0___(0) = epsilon__o__b__c__plus_____2_____L_____minus__2__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1__0___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__1___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__0___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________9___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__2___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__1___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________8___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__3___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__2___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________7___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__4___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__3___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________6___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__5___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__4___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________5___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__6___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__5___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________4___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__7___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__6___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________3___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__8___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__7___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________2___; + + epsilon__o__b__c__plus_____2_____L_____minus__3__9___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__8___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________1___; + + epsilon__o__b__c__plus_____2_____L_____minus__4__0___(0) = epsilon__o__b__c__plus_____2_____L_____minus__3__9___(-1) + active_o__b__c_shocks * epsilon__o__b__c__plus_____2________0___; + +end; + +shocks; +var eps_j = 1; +var eps_k = 1; +var eps_p = 1; +var eps_r = 1; +var eps_w = 1; +var eps_z = 1; +var epsilon__o__b__c__plus_____2________2__2___ = 1; +var epsilon__o__b__c__plus_____2________2__3___ = 1; +var epsilon__o__b__c__plus_____2________2__1___ = 1; +var epsilon__o__b__c__plus_____2________2__0___ = 1; +var epsilon__o__b__c__plus_____2________2__4___ = 1; +var epsilon__o__b__c__plus_____2________2__5___ = 1; +var epsilon__o__b__c__plus_____2________2__6___ = 1; +var epsilon__o__b__c__plus_____2________2__7___ = 1; +var epsilon__o__b__c__plus_____2________2__8___ = 1; +var epsilon__o__b__c__plus_____2________2__9___ = 1; +var epsilon__o__b__c__plus_____2________2___ = 1; +var epsilon__o__b__c__plus_____2________3__2___ = 1; +var epsilon__o__b__c__plus_____2________3__3___ = 1; +var epsilon__o__b__c__plus_____2________3__1___ = 1; +var epsilon__o__b__c__plus_____2________3__0___ = 1; +var epsilon__o__b__c__plus_____2________3__4___ = 1; +var epsilon__o__b__c__plus_____2________3__5___ = 1; +var epsilon__o__b__c__plus_____2________3__6___ = 1; +var epsilon__o__b__c__plus_____2________3__7___ = 1; +var epsilon__o__b__c__plus_____2________3__8___ = 1; +var epsilon__o__b__c__plus_____2________3__9___ = 1; +var epsilon__o__b__c__plus_____2________3___ = 1; +var epsilon__o__b__c__plus_____2________1__2___ = 1; +var epsilon__o__b__c__plus_____2________1__3___ = 1; +var epsilon__o__b__c__plus_____2________1__1___ = 1; +var epsilon__o__b__c__plus_____2________1__0___ = 1; +var epsilon__o__b__c__plus_____2________1__4___ = 1; +var epsilon__o__b__c__plus_____2________1__5___ = 1; +var epsilon__o__b__c__plus_____2________1__6___ = 1; +var epsilon__o__b__c__plus_____2________1__7___ = 1; +var epsilon__o__b__c__plus_____2________1__8___ = 1; +var epsilon__o__b__c__plus_____2________1__9___ = 1; +var epsilon__o__b__c__plus_____2________1___ = 1; +var epsilon__o__b__c__plus_____2________0___ = 1; +var epsilon__o__b__c__plus_____2________4__0___ = 1; +var epsilon__o__b__c__plus_____2________4___ = 1; +var epsilon__o__b__c__plus_____2________5___ = 1; +var epsilon__o__b__c__plus_____2________6___ = 1; +var epsilon__o__b__c__plus_____2________7___ = 1; +var epsilon__o__b__c__plus_____2________8___ = 1; +var epsilon__o__b__c__plus_____2________9___ = 1; +var epsilon__o__b__c__minus_____1________2__2___ = 1; +var epsilon__o__b__c__minus_____1________2__3___ = 1; +var epsilon__o__b__c__minus_____1________2__1___ = 1; +var epsilon__o__b__c__minus_____1________2__0___ = 1; +var epsilon__o__b__c__minus_____1________2__4___ = 1; +var epsilon__o__b__c__minus_____1________2__5___ = 1; +var epsilon__o__b__c__minus_____1________2__6___ = 1; +var epsilon__o__b__c__minus_____1________2__7___ = 1; +var epsilon__o__b__c__minus_____1________2__8___ = 1; +var epsilon__o__b__c__minus_____1________2__9___ = 1; +var epsilon__o__b__c__minus_____1________2___ = 1; +var epsilon__o__b__c__minus_____1________3__2___ = 1; +var epsilon__o__b__c__minus_____1________3__3___ = 1; +var epsilon__o__b__c__minus_____1________3__1___ = 1; +var epsilon__o__b__c__minus_____1________3__0___ = 1; +var epsilon__o__b__c__minus_____1________3__4___ = 1; +var epsilon__o__b__c__minus_____1________3__5___ = 1; +var epsilon__o__b__c__minus_____1________3__6___ = 1; +var epsilon__o__b__c__minus_____1________3__7___ = 1; +var epsilon__o__b__c__minus_____1________3__8___ = 1; +var epsilon__o__b__c__minus_____1________3__9___ = 1; +var epsilon__o__b__c__minus_____1________3___ = 1; +var epsilon__o__b__c__minus_____1________1__2___ = 1; +var epsilon__o__b__c__minus_____1________1__3___ = 1; +var epsilon__o__b__c__minus_____1________1__1___ = 1; +var epsilon__o__b__c__minus_____1________1__0___ = 1; +var epsilon__o__b__c__minus_____1________1__4___ = 1; +var epsilon__o__b__c__minus_____1________1__5___ = 1; +var epsilon__o__b__c__minus_____1________1__6___ = 1; +var epsilon__o__b__c__minus_____1________1__7___ = 1; +var epsilon__o__b__c__minus_____1________1__8___ = 1; +var epsilon__o__b__c__minus_____1________1__9___ = 1; +var epsilon__o__b__c__minus_____1________1___ = 1; +var epsilon__o__b__c__minus_____1________0___ = 1; +var epsilon__o__b__c__minus_____1________4__0___ = 1; +var epsilon__o__b__c__minus_____1________4___ = 1; +var epsilon__o__b__c__minus_____1________5___ = 1; +var epsilon__o__b__c__minus_____1________6___ = 1; +var epsilon__o__b__c__minus_____1________7___ = 1; +var epsilon__o__b__c__minus_____1________8___ = 1; +var epsilon__o__b__c__minus_____1________9___ = 1; +end; + +initval; + aj = 1.0; + ak = 1.0; + ap = 1.0; + arr = 1.0; + aw = 1.0; + az = 1.0; + b = 3.7449037697454393; + bnot = 3.7449037697454393; + c = 1.0105380669741733; + c1 = 0.5618297241145294; + dp = 1.005; + dw = 1.0050000000000001; + dw1 = 1.0050000000000001; + h1 = 0.3423315782326022; + ik = 0.41334371258474567; + k = 16.533748503389837; + lm = 0.009000031630409243; + maxlev = 0.0; + n = 0.690204795456776; + n1 = 0.9280334149064987; + q = 12.292371456831656; + qk = 1.0; + r = 1.0100502512562815; + rk = 0.030025125628140636; + rnot = 1.0100502512562815; + uc = 0.9895718258237147; + uc1 = 1.7798987078799509; + uh = 0.06082092233120329; + uh1 = 0.11684577919020209; + w = 0.836973863780634; + w1 = 0.6256761089591792; + xp = 1.2000000000000002; + xw = 1.2; + xw1 = 1.2; + y = 1.9857115036734478; + z_j = 0.0; + Chi__o__b__c__plus____2___ = 0.0; + Chi__o__b__c__minus____1___ = 0.0; + chi__o__b__c__plus_____2_____r = -1.7092628571364783e-22; + chi__o__b__c__plus_____2_____l = -0.01005025125628141; + chi__o__b__c__minus_____1_____r = 0.009000031630409243; + chi__o__b__c__minus_____1_____l = 2.021379878515203e-22; + epsilon__o__b__c__plus____2___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__2___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__3___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__1___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__0___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__4___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__5___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__6___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__7___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__8___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2__9___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__2___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__2___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__3___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__1___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__0___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__4___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__5___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__6___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__7___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__8___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3__9___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__3___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__2___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__3___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__1___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__0___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__4___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__5___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__6___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__7___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__8___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1__9___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__1___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__0___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__4__0___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__4___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__5___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__6___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__7___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__8___ = 0.0; + epsilon__o__b__c__plus_____2_____L_____minus__9___ = 0.0; + epsilon__o__b__c__minus____1___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__2___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__3___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__1___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__0___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__4___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__5___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__6___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__7___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__8___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2__9___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__2___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__2___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__3___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__1___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__0___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__4___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__5___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__6___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__7___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__8___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3__9___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__3___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__2___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__3___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__1___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__0___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__4___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__5___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__6___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__7___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__8___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1__9___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__1___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__0___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__4__0___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__4___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__5___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__6___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__7___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__8___ = 0.0; + epsilon__o__b__c__minus_____1_____L_____minus__9___ = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/NAWM_EAUS_2008_parameters.csv b/test/NAWM_EAUS_2008_parameters.csv new file mode 100644 index 000000000..c88466d1e --- /dev/null +++ b/test/NAWM_EAUS_2008_parameters.csv @@ -0,0 +1,148 @@ +Parameter,Value +σ_EA_R,1.0 +σ_US_R,1.0 +σ_EA_Z,1.0 +σ_EA_G,1.0 +σ_EA_TR,1.0 +σ_EA_TAUC,1.0 +σ_EA_TAUD,1.0 +σ_EA_TAUK,1.0 +σ_EA_TAUN,1.0 +σ_EA_TAUWH,1.0 +σ_EA_TAUWF,1.0 +σ_US_Z,1.0 +σ_US_G,1.0 +σ_US_TR,1.0 +σ_US_TAUC,1.0 +σ_US_TAUD,1.0 +σ_US_TAUK,1.0 +σ_US_TAUN,1.0 +σ_US_TAUWH,1.0 +σ_US_TAUWF,1.0 +σ_EA_RP,1.0 +EA_SIZE,0.4194 +EA_OMEGA,0.25 +EA_BETA,0.992638 +EA_SIGMA,2.0 +EA_KAPPA,0.6 +EA_ZETA,2.0 +EA_DELTA,0.025 +EA_ETA,6.0 +EA_ETAI,6.0 +EA_ETAJ,6.0 +EA_XII,0.75 +EA_XIJ,0.75 +EA_CHII,0.75 +EA_CHIJ,0.75 +EA_ALPHA,0.3 +EA_THETA,6.0 +EA_XIH,0.9 +EA_XIX,0.3 +EA_CHIH,0.5 +EA_CHIX,0.5 +EA_NUC,0.919622 +EA_MUC,1.5 +EA_NUI,0.418629 +EA_MUI,1.5 +EA_GAMMAV1,0.289073 +EA_GAMMAV2,0.150339 +EA_GAMMAI1,3.0 +EA_GAMMAU2,0.007 +EA_GAMMAIMC1,2.5 +EA_GAMMAIMI1,0.0 +EA_GAMMAB1,0.01 +EA_BYTARGET,2.4 +EA_PHITB,0.1 +EA_GYBAR,0.18 +EA_TRYBAR,0.195161 +EA_TAUCBAR,0.183 +EA_TAUKBAR,0.184123 +EA_TAUNBAR,0.122 +EA_TAUWHBAR,0.118 +EA_TAUWFBAR,0.219 +EA_UPSILONT,1.2 +EA_UPSILONTR,0.6666666666666666 +EA_PI4TARGET,1.02 +EA_PHIRR,0.95 +EA_PHIRPI,2.0 +EA_PHIRGY,0.1 +EA_BFYTARGET,0.0 +EA_RHOZ,0.9 +EA_RHOG,0.9 +EA_RHOTR,0.9 +EA_RHOTAUC,0.9 +EA_RHOTAUK,0.9 +EA_RHOTAUN,0.9 +EA_RHOTAUD,0.9 +EA_RHOTAUWH,0.9 +EA_RHOTAUWF,0.9 +EA_PYBAR,1.00645740523434 +EA_YBAR,3.62698111871356 +EA_RHORP,0.9 +EA_PIBAR,0.961117319822928 +EA_PSIBAR,0.725396223742712 +EA_QBAR,0.961117319822928 +EA_TAUDBAR,0.0 +EA_ZBAR,1.0 +US_SIZE,0.5806 +US_OMEGA,0.25 +US_BETA,0.992638 +US_SIGMA,2.0 +US_KAPPA,0.6 +US_ZETA,2.0 +US_DELTA,0.025 +US_ETA,6.0 +US_ETAI,6.0 +US_ETAJ,6.0 +US_XII,0.75 +US_XIJ,0.75 +US_CHII,0.75 +US_CHIJ,0.75 +US_ALPHA,0.3 +US_THETA,6.0 +US_XIH,0.9 +US_XIX,0.3 +US_CHIH,0.5 +US_CHIX,0.5 +US_NUC,0.899734 +US_MUC,1.5 +US_NUI,0.673228 +US_MUI,1.5 +US_GAMMAV1,0.028706 +US_GAMMAV2,0.150339 +US_GAMMAI1,3.0 +US_GAMMAU2,0.007 +US_GAMMAIMC1,2.5 +US_GAMMAIMI1,0.0 +US_BYTARGET,2.4 +US_PHITB,0.1 +US_GYBAR,0.16 +US_TRYBAR,0.079732 +US_TAUCBAR,0.077 +US_TAUKBAR,0.184123 +US_TAUNBAR,0.154 +US_TAUWHBAR,0.071 +US_TAUWFBAR,0.071 +US_UPSILONT,1.2 +US_UPSILONTR,0.6666666666666666 +US_PI4TARGET,1.02 +US_PHIRR,0.95 +US_PHIRPI,2.0 +US_PHIRGY,0.1 +US_RHOZ,0.9 +US_RHOG,0.9 +US_RHOTR,0.9 +US_RHOTAUC,0.9 +US_RHOTAUK,0.9 +US_RHOTAUN,0.9 +US_RHOTAUD,0.9 +US_RHOTAUWH,0.9 +US_RHOTAUWF,0.9 +US_PYBAR,0.992282866960427 +US_TAUDBAR,0.0 +US_YBAR,3.92445610588497 +US_PIBAR,1.01776829477927 +US_PSIBAR,0.784891221176995 +US_QBAR,1.01776829477927 +US_ZBAR,1.0 +US_RER,1.0 diff --git a/test/SW07_nonlinear.jl b/test/SW07_nonlinear.jl new file mode 100644 index 000000000..f34ccb2f9 --- /dev/null +++ b/test/SW07_nonlinear.jl @@ -0,0 +1,211 @@ +using MacroModelling + +@model SW07_nonlinear begin + y[0] = c[0] + inve[0] + y[ss] * gy[0] + afunc[0] * kp[-1] / (1 + ctrend / 100) + + y[0] * (pdot[0] + curvp * (1 - cfc) / cfc) / (1 + curvp * (1 - cfc) / cfc) = a[0] * k[0] ^ calfa * lab[0] ^ (1 - calfa) - y[ss] * (cfc - 1) + + k[0] = kp[-1] * zcap[0] / (1 + ctrend / 100) + + kp[0] = inve[0] * qs[0] * (1 - Sfunc[0]) + kp[-1] * (1 - ctou) / (1 + ctrend / 100) + + pdot[0] = (1 - cprobp) * (Pratio[0] / dp[0]) ^ ((1 + curvp * (1 - cfc) / cfc) * ( - cfc) / (cfc - 1)) + cprobp * pdot[-1] * (dp[-1] / dp[0] * pinf[-1] ^ cindp * pinf[ss] ^ (1 - cindp) / pinf[0]) ^ ((1 + curvp * (1 - cfc) / cfc) * ( - cfc) / (cfc - 1)) + + wdot[0] = (1 - cprobw) * (wnew[0] / dw[0]) ^ (( - clandaw) * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) + cprobw * wdot[-1] * (dw[-1] / dw[0] * pinf[-1] ^ cindw * pinf[ss] ^ (1 - cindw) / pinf[0]) ^ (( - clandaw) * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) + + 1 = (1 - cprobp) * (Pratio[0] / dp[0]) ^ (( - (1 + curvp * (1 - cfc))) / (cfc - 1)) + cprobp * (dp[-1] / dp[0] * pinf[-1] ^ cindp * pinf[ss] ^ (1 - cindp) / pinf[0]) ^ (( - (1 + curvp * (1 - cfc))) / (cfc - 1)) + + 1 = (1 - cprobw) * (wnew[0] / dw[0]) ^ (( - (1 + curvw * (1 - clandaw))) / (clandaw - 1)) + cprobw * (dw[-1] / dw[0] * pinf[-1] ^ cindw * pinf[ss] ^ (1 - cindw) / pinf[0]) ^ (( - (1 + curvw * (1 - clandaw))) / (clandaw - 1)) + + 1 = dp[0] * (1 + (1 - cfc) * curvp * pdotl[0] / cfc) / (1 + curvp * (1 - cfc) / cfc) + + w[0] = dw[0] * (1 + curvw * (1 - clandaw) / clandaw * wdotl[0]) / (1 + curvw * (1 - clandaw) / clandaw) + + pdotl[0] = (1 - cprobp) * Pratio[0] / dp[0] + pinf[ss] ^ (1 - cindp) * pinf[-1] ^ cindp * cprobp * dp[-1] / dp[0] / pinf[0] * pdotl[-1] + + wdotl[0] = (1 - cprobw) * wnew[0] / dw[0] + pinf[ss] ^ (1 - cindw) * pinf[-1] ^ cindw * cprobw * dw[-1] / dw[0] / pinf[0] * wdotl[-1] + + xi[0] = exp((csigma - 1) / (1 + csigl) * (lab[0] * (wdot[0] + curvw * (1 - clandaw) / clandaw) / (1 + curvw * (1 - clandaw) / clandaw)) ^ (1 + csigl)) * (c[0] - c[-1] * chabb / (1 + ctrend / 100)) ^ (-csigma) + + 1 = qs[0] * pk[0] * (1 - Sfunc[0] - inve[0] * (1 + ctrend / 100) * SfuncD[0] / inve[-1]) + SfuncD[1] * xi[1] / xi[0] * qsaux[0] * pk[1] * ((1 + ctrend / 100) * inve[1] / inve[0]) ^ 2 / (1 + constebeta / 100) * (1 + ctrend / 100) ^ (-csigma) + + xi[0] = (1 + ctrend / 100) ^ (-csigma) * xi[1] * b[0] * r[0] / (1 + constebeta / 100) / pinf[1] + + rk[0] = afuncD[0] + + pk[0] = (1 + ctrend / 100) ^ (-csigma) * xi[1] * (rk[1] * zcap[1] - afunc[1] + (1 - ctou) * pk[1]) / (1 + constebeta / 100) / xi[0] + + k[0] = calfa * lab[0] * w[0] / (1 - calfa) / rk[0] + + mc[0] = w[0] ^ (1 - calfa) * rk[0] ^ calfa / (a[0] * calfa ^ calfa * (1 - calfa) ^ (1 - calfa)) + + (1 + curvw * (1 - clandaw)) * wnew[0] * gamw1[0] / (1 + curvw * (1 - clandaw) / clandaw) = clandaw * gamw2[0] + (clandaw - 1) * (1 - clandaw) * curvw * gamw3[0] / clandaw / (1 + curvw * (1 - clandaw) / clandaw) * wnew[0] ^ (1 + clandaw * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) + + gamw1[0] = lab[0] * dw[0] ^ (clandaw * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) + (1 + ctrend / 100) ^ (-csigma) * cprobw * (1 + ctrend / 100) * xi[1] * gamw1[1] * (pinf[ss] ^ (1 - cindw) * pinf[0] ^ cindw / pinf[1]) ^ (( - (1 + curvw * (1 - clandaw))) / (clandaw - 1)) / xi[0] / (1 + constebeta / 100) + + gamw2[0] = dw[0] ^ (clandaw * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) * lab[0] * (c[0] - c[-1] * chabb / (1 + ctrend / 100)) * sw[0] * (lab[0] * (wdot[0] + curvw * (1 - clandaw) / clandaw) / (1 + curvw * (1 - clandaw) / clandaw)) ^ csigl + (1 + ctrend / 100) ^ (-csigma) * cprobw * (1 + ctrend / 100) * xi[1] * gamw2[1] * (pinf[ss] ^ (1 - cindw) * pinf[0] ^ cindw / pinf[1]) ^ (( - clandaw) * (1 + curvw * (1 - clandaw) / clandaw) / (clandaw - 1)) / xi[0] / (1 + constebeta / 100) + + gamw3[0] = lab[0] + (1 + ctrend / 100) ^ (-csigma) * cprobw * (1 + ctrend / 100) * xi[1] * pinf[0] ^ cindw * pinf[ss] ^ (1 - cindw) * gamw3[1] / pinf[1] / xi[0] / (1 + constebeta / 100) + + (1 + curvp * (1 - cfc)) * Pratio[0] * gam1[0] / (1 + curvp * (1 - cfc) / cfc) = cfc * gam2[0] + (1 - cfc) * curvp * (cfc - 1) * gam3[0] / cfc / (1 + curvp * (1 - cfc) / cfc) * Pratio[0] ^ (1 + cfc * (1 + curvp * (1 - cfc) / cfc) / (cfc - 1)) + + gam1[0] = y[0] * dp[0] ^ (cfc * (1 + curvp * (1 - cfc) / cfc) / (cfc - 1)) + (1 + ctrend / 100) ^ (-csigma) * cprobp * (1 + ctrend / 100) * xi[1] * gam1[1] / xi[0] / (1 + constebeta / 100) * (pinf[ss] ^ (1 - cindp) * pinf[0] ^ cindp / pinf[1]) ^ (( - (1 + curvp * (1 - cfc))) / (cfc - 1)) + + gam2[0] = dp[0] ^ (cfc * (1 + curvp * (1 - cfc) / cfc) / (cfc - 1)) * y[0] * mc[0] * spinf[0] + (1 + ctrend / 100) ^ (-csigma) * cprobp * (1 + ctrend / 100) * xi[1] * gam2[1] / xi[0] / (1 + constebeta / 100) * (pinf[ss] ^ (1 - cindp) * pinf[0] ^ cindp / pinf[1]) ^ ((1 + curvp * (1 - cfc) / cfc) * ( - cfc) / (cfc - 1)) + + gam3[0] = y[0] + (1 + ctrend / 100) ^ (-csigma) * cprobp * (1 + ctrend / 100) * xi[1] * pinf[0] ^ cindp * pinf[ss] ^ (1 - cindp) * gam3[1] / pinf[1] / xi[0] / (1 + constebeta / 100) + + qsaux[0] = qs[1] + + r[0] = r[ss] ^ (1 - crr) * r[-1] ^ crr * (pinf[0] / pinfss) ^ ((1 - crr) * crpi) * (y[0] / yflex[0]) ^ ((1 - crr) * cry) * (y[0] / yflex[0] / (y[-1] / yflex[-1])) ^ crdy * ms[0] + + afunc[0] = rk[ss] / (czcap / (1 - czcap)) * (exp(czcap / (1 - czcap) * (zcap[0] - 1)) - 1) + + afuncD[0] = rk[ss] * exp(czcap / (1 - czcap) * (zcap[0] - 1)) + + Sfunc[0] = csadjcost / 2 * (inve[0] * (1 + ctrend / 100) / inve[-1] - (1 + ctrend / 100)) ^ 2 + + SfuncD[0] = csadjcost * (inve[0] * (1 + ctrend / 100) / inve[-1] - (1 + ctrend / 100)) + + a[0] = 1 - crhoa + crhoa * a[-1] + ea[x] / 100 + + b[0] = 1 - crhob + crhob * b[-1] + eb[x] * ( - (((1 - chabb / (1 + ctrend / 100)) / (csigma * (1 + chabb / (1 + ctrend / 100)))) ^ (-1))) / 100 + + gy[0] - cg = crhog * (gy[-1] - cg) + egy[x] / 100 + ea[x] * cgy / 100 + + qs[0] = 1 - crhoqs + crhoqs * qs[-1] + csadjcost * eqs[x] * (1 + ctrend / 100) ^ 2 * (1 + 1 / (1 + constebeta / 100) * (1 + ctrend / 100) ^ (1 - csigma)) / 100 + + ms[0] = 1 - crhoms + crhoms * ms[-1] + ems[x] / 100 + + spinf[0] = 1 - crhopinf + crhopinf * spinf[-1] + epinfma[0] - cmap * epinfma[-1] + + epinfma[0] = epinf[x] / ((1 - cprobp) * 1 / (1 + (1 + ctrend / 100) ^ (-csigma) * (1 + ctrend / 100) * cindp / (1 + constebeta / 100)) * (1 - (1 + ctrend / 100) ^ (-csigma) * (1 + ctrend / 100) * cprobp / (1 + constebeta / 100)) / cprobp / (1 + curvp * (cfc - 1))) / 100 + + sw[0] = 1 - crhow + crhow * sw[-1] + ewma[0] - cmaw * ewma[-1] + + ewma[0] = ew[x] / ((1 - cprobw) * 1 / (1 + curvw * (clandaw - 1)) * (1 - (1 + ctrend / 100) ^ (-csigma) * (1 + ctrend / 100) * cprobw / (1 + constebeta / 100)) / (cprobw * (1 + (1 + ctrend / 100) ^ (-csigma) * (1 + ctrend / 100) / (1 + constebeta / 100)))) / 100 + + yflex[0] = cflex[0] + inveflex[0] + gy[0] * yflex[ss] + afuncflex[0] * kpflex[-1] / (1 + ctrend / 100) + + yflex[0] = a[0] * kflex[0] ^ calfa * labflex[0] ^ (1 - calfa) - (cfc - 1) * yflex[ss] + + kflex[0] = kpflex[-1] * zcapflex[0] / (1 + ctrend / 100) + + kpflex[0] = qs[0] * inveflex[0] * (1 - Sfuncflex[0]) + (1 - ctou) * kpflex[-1] / (1 + ctrend / 100) + + xiflex[0] = exp((csigma - 1) / (1 + csigl) * labflex[0] ^ (1 + csigl)) * (cflex[0] - chabb * cflex[-1] / (1 + ctrend / 100)) ^ (-csigma) + + 1 = qs[0] * pkflex[0] * (1 - Sfuncflex[0] - (1 + ctrend / 100) * inveflex[0] * SfuncDflex[0] / inveflex[-1]) + (1 + ctrend / 100) ^ (-csigma) * qsaux[0] * SfuncDflex[1] * xiflex[1] / xiflex[0] * pkflex[1] * ((1 + ctrend / 100) * inveflex[1] / inveflex[0]) ^ 2 / (1 + constebeta / 100) + + xiflex[0] = (1 + ctrend / 100) ^ (-csigma) * b[0] * xiflex[1] * rrflex[0] / (1 + constebeta / 100) + + rkflex[0] = afuncDflex[0] + + pkflex[0] = (1 + ctrend / 100) ^ (-csigma) * xiflex[1] * (rkflex[1] * zcapflex[1] - afuncflex[1] + (1 - ctou) * pkflex[1]) / (1 + constebeta / 100) / xiflex[0] + + kflex[0] = calfa * labflex[0] / (1 - calfa) * wflex[0] / rkflex[0] + + mcflex = wflex[0] ^ (1 - calfa) * rkflex[0] ^ calfa / (a[0] * calfa ^ calfa * (1 - calfa) ^ (1 - calfa)) + + (1 + curvw * (1 - clandaw)) * wflex[0] / (1 + curvw * (1 - clandaw) / clandaw) = sw[ss] * ((cflex[0] - chabb * cflex[-1] / (1 + ctrend / 100)) * clandaw * labflex[0] ^ csigl + (clandaw - 1) * (1 - clandaw) * curvw * wflex[0] / clandaw / (1 + curvw * (1 - clandaw) / clandaw)) + + afuncflex[0] = rkflex[ss] / (czcap / (1 - czcap)) * (exp(czcap / (1 - czcap) * (zcapflex[0] - 1)) - 1) + + afuncDflex[0] = rkflex[ss] * exp(czcap / (1 - czcap) * (zcapflex[0] - 1)) + + Sfuncflex[0] = csadjcost / 2 * ((1 + ctrend / 100) * inveflex[0] / inveflex[-1] - (1 + ctrend / 100)) ^ 2 + + SfuncDflex[0] = csadjcost * ((1 + ctrend / 100) * inveflex[0] / inveflex[-1] - (1 + ctrend / 100)) + + ygap[0] = 100 * log(y[0] / yflex[0]) + + dy[0] = ctrend + 100 * (y[0] / y[-1] - 1) + + dc[0] = ctrend + 100 * (c[0] / c[-1] - 1) + + dinve[0] = ctrend + 100 * (inve[0] / inve[-1] - 1) + + pinfobs[0] = 100 * (pinf[0] - pinf[ss]) + constepinf + + robs[0] = 100 * (r[0] - 1) + + dwobs[0] = ctrend + 100 * (w[0] / w[-1] - 1) + + labobs[0] = 100 * (lab[0] / lab[ss] - 1) + +end + + +@parameters SW07_nonlinear begin + ctou = 0.025 + + cg = 0.18 + + clandaw = 1.5 + + curvw = 10.0 + + crhoa = 0.95827 + + crhob = 0.22137 + + crhog = 0.97391 + + crhoqs = 0.70524 + + crhoms = 0.11421 + + crhopinf = 0.83954 + + crhow = 0.9745 + + cmap = 0.69414 + + cmaw = 0.93617 + + csadjcost = 5.5811 + + csigma = 1.4103 + + chabb = 0.68049 + + cprobw = 0.80501 + + csigl = 2.2061 + + cindw = 0.56351 + + cindp = 0.24165 + + czcap = 0.49552 + + cfc = 1.3443 + + crpi = 1.931 + + crr = 0.82512 + + cry = 0.097844 + + crdy = 0.25114 + + constepinf = 0.8731 + + constebeta = 0.12575 + + ctrend = 0.4419 + + cgy = 0.53817 + + calfa = 0.18003 + + curvp = 64.5595 + + cprobp = 0.667 + + mcflex = 0.7438815740534109 + + pinfss = 1.008731 + +end + diff --git a/test/SW07_nonlinear.mod b/test/SW07_nonlinear.mod new file mode 100644 index 000000000..1cc33b7f4 --- /dev/null +++ b/test/SW07_nonlinear.mod @@ -0,0 +1,261 @@ +var +Pratio Sfunc SfuncD SfuncDflex Sfuncflex a afunc afuncD afuncDflex afuncflex b c cflex dc dinve dp dw dwobs dy epinfma ewma gam1 gam2 gam3 gamw1 gamw2 gamw3 gy inve inveflex k kflex kp kpflex lab labflex labobs mc ms pdot pdotl pinf pinfobs pk pkflex qs qsaux r rk rkflex robs rrflex spinf sw w wdot wdotl wflex wnew xi xiflex y yflex ygap zcap zcapflex ; + +varexo +ea eb egy ems epinf eqs ew ; + +parameters +calfa cfc cg cgy chabb cindp cindw clandaw cmap cmaw constebeta constepinf cprobp cprobw crdy crhoa crhob crhog crhoms crhopinf crhoqs crhow crpi crr cry csadjcost csigl csigma ctou ctrend curvp curvw czcap mcflex pinfss ; + +% Parameter definitions: + ctou = 0.025; + cg = 0.18; + clandaw = 1.5; + curvw = 10.0; + crhoa = 0.95827; + crhob = 0.22137; + crhog = 0.97391; + crhoqs = 0.70524; + crhoms = 0.11421; + crhopinf = 0.83954; + crhow = 0.9745; + cmap = 0.69414; + cmaw = 0.93617; + csadjcost = 5.5811; + csigma = 1.4103; + chabb = 0.68049; + cprobw = 0.80501; + csigl = 2.2061; + cindw = 0.56351; + cindp = 0.24165; + czcap = 0.49552; + cfc = 1.3443; + crpi = 1.931; + crr = 0.82512; + cry = 0.097844; + crdy = 0.25114; + constepinf = 0.8731; + constebeta = 0.12575; + ctrend = 0.4419; + cgy = 0.53817; + calfa = 0.18003; + curvp = 64.5595; + cprobp = 0.667; + mcflex = 0.7438815740534109; + pinfss = 1.008731; + +model; + y(0) = c(0) + inve(0) + STEADY_STATE(y) * gy(0) + (afunc(0) * kp(-1)) / (1 + ctrend / 100); + + (y(0) * (pdot(0) + (curvp * (1 - cfc)) / cfc)) / (1 + (curvp * (1 - cfc)) / cfc) = a(0) * k(0) ^ calfa * lab(0) ^ (1 - calfa) - (cfc - 1) * STEADY_STATE(y); + + k(0) = (kp(-1) * zcap(0)) / (1 + ctrend / 100); + + kp(0) = inve(0) * qs(0) * (1 - Sfunc(0)) + (kp(-1) * (1 - ctou)) / (1 + ctrend / 100); + + pdot(0) = (1 - cprobp) * (Pratio(0) / dp(0)) ^ ((-cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)) + pdot(-1) * cprobp * (((dp(-1) / dp(0)) * pinf(-1) ^ cindp * STEADY_STATE(pinf) ^ (1 - cindp)) / pinf(0)) ^ ((-cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)); + + wdot(0) = (1 - cprobw) * (wnew(0) / dw(0)) ^ ((-clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)) + wdot(-1) * cprobw * (((dw(-1) / dw(0)) * pinf(-1) ^ cindw * STEADY_STATE(pinf) ^ (1 - cindw)) / pinf(0)) ^ ((-clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)); + + 1 = (1 - cprobp) * (Pratio(0) / dp(0)) ^ (-((1 + curvp * (1 - cfc))) / (cfc - 1)) + cprobp * (((dp(-1) / dp(0)) * pinf(-1) ^ cindp * STEADY_STATE(pinf) ^ (1 - cindp)) / pinf(0)) ^ (-((1 + curvp * (1 - cfc))) / (cfc - 1)); + + 1 = (1 - cprobw) * (wnew(0) / dw(0)) ^ (-((1 + curvw * (1 - clandaw))) / (clandaw - 1)) + cprobw * (((dw(-1) / dw(0)) * pinf(-1) ^ cindw * STEADY_STATE(pinf) ^ (1 - cindw)) / pinf(0)) ^ (-((1 + curvw * (1 - clandaw))) / (clandaw - 1)); + + 1 = (dp(0) * (1 + (pdotl(0) * curvp * (1 - cfc)) / cfc)) / (1 + (curvp * (1 - cfc)) / cfc); + + w(0) = (dw(0) * (1 + ((curvw * (1 - clandaw)) / clandaw) * wdotl(0))) / (1 + (curvw * (1 - clandaw)) / clandaw); + + pdotl(0) = ((1 - cprobp) * Pratio(0)) / dp(0) + ((((cprobp * dp(-1)) / dp(0)) * pinf(-1) ^ cindp * STEADY_STATE(pinf) ^ (1 - cindp)) / pinf(0)) * pdotl(-1); + + wdotl(0) = ((1 - cprobw) * wnew(0)) / dw(0) + ((((cprobw * dw(-1)) / dw(0)) * pinf(-1) ^ cindw * STEADY_STATE(pinf) ^ (1 - cindw)) / pinf(0)) * wdotl(-1); + + xi(0) = exp(((csigma - 1) / (1 + csigl)) * ((lab(0) * ((curvw * (1 - clandaw)) / clandaw + wdot(0))) / (1 + (curvw * (1 - clandaw)) / clandaw)) ^ (1 + csigl)) * (c(0) - (c(-1) * chabb) / (1 + ctrend / 100)) ^ -csigma; + + 1 = qs(0) * pk(0) * ((1 - Sfunc(0)) - ((1 + ctrend / 100) * inve(0) * SfuncD(0)) / inve(-1)) + ((((SfuncD(1) * xi(1)) / xi(0)) * qsaux(0) * pk(1) * (((1 + ctrend / 100) * inve(1)) / inve(0)) ^ 2 * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + xi(0) = (((xi(1) * b(0) * r(0) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma) / pinf(1); + + rk(0) = afuncD(0); + + pk(0) = (((((rk(1) * zcap(1) - afunc(1)) + (1 - ctou) * pk(1)) * xi(1) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma) / xi(0); + + k(0) = ((lab(0) * w(0) * calfa) / (1 - calfa)) / rk(0); + + mc(0) = (w(0) ^ (1 - calfa) * rk(0) ^ calfa) / (a(0) * calfa ^ calfa * (1 - calfa) ^ (1 - calfa)); + + (wnew(0) * gamw1(0) * (1 + curvw * (1 - clandaw))) / (1 + (curvw * (1 - clandaw)) / clandaw) = clandaw * gamw2(0) + ((((gamw3(0) * curvw * (1 - clandaw)) / clandaw) * (clandaw - 1)) / (1 + (curvw * (1 - clandaw)) / clandaw)) * wnew(0) ^ (1 + (clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)); + + gamw1(0) = lab(0) * dw(0) ^ ((clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)) + ((((gamw1(1) * ((STEADY_STATE(pinf) ^ (1 - cindw) * pinf(0) ^ cindw) / pinf(1)) ^ (-((1 + curvw * (1 - clandaw))) / (clandaw - 1)) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobw * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + gamw2(0) = (c(0) - (c(-1) * chabb) / (1 + ctrend / 100)) * lab(0) * sw(0) * dw(0) ^ ((clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)) * ((lab(0) * ((curvw * (1 - clandaw)) / clandaw + wdot(0))) / (1 + (curvw * (1 - clandaw)) / clandaw)) ^ csigl + ((((gamw2(1) * ((STEADY_STATE(pinf) ^ (1 - cindw) * pinf(0) ^ cindw) / pinf(1)) ^ ((-clandaw * (1 + (curvw * (1 - clandaw)) / clandaw)) / (clandaw - 1)) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobw * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + gamw3(0) = lab(0) + ((((((gamw3(1) * STEADY_STATE(pinf) ^ (1 - cindw) * pinf(0) ^ cindw) / pinf(1)) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobw * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + (Pratio(0) * gam1(0) * (1 + curvp * (1 - cfc))) / (1 + (curvp * (1 - cfc)) / cfc) = cfc * gam2(0) + (((gam3(0) * (cfc - 1) * curvp * (1 - cfc)) / cfc) / (1 + (curvp * (1 - cfc)) / cfc)) * Pratio(0) ^ (1 + (cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)); + + gam1(0) = y(0) * dp(0) ^ ((cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)) + ((((gam1(1) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobp * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma * ((STEADY_STATE(pinf) ^ (1 - cindp) * pinf(0) ^ cindp) / pinf(1)) ^ (-((1 + curvp * (1 - cfc))) / (cfc - 1)); + + gam2(0) = y(0) * mc(0) * spinf(0) * dp(0) ^ ((cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)) + ((((gam2(1) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobp * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma * ((STEADY_STATE(pinf) ^ (1 - cindp) * pinf(0) ^ cindp) / pinf(1)) ^ ((-cfc * (1 + (curvp * (1 - cfc)) / cfc)) / (cfc - 1)); + + gam3(0) = y(0) + ((((((gam3(1) * STEADY_STATE(pinf) ^ (1 - cindp) * pinf(0) ^ cindp) / pinf(1)) * xi(1)) / xi(0)) * (1 + ctrend / 100) * cprobp * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + qsaux(0) = qs(1); + + r(0) = STEADY_STATE(r) ^ (1 - crr) * r(-1) ^ crr * (pinf(0) / pinfss) ^ ((1 - crr) * crpi) * (y(0) / yflex(0)) ^ ((1 - crr) * cry) * ((y(0) / yflex(0)) / (y(-1) / yflex(-1))) ^ crdy * ms(0); + + afunc(0) = ((STEADY_STATE(rk) * 1) / (czcap / (1 - czcap))) * (exp((czcap / (1 - czcap)) * (zcap(0) - 1)) - 1); + + afuncD(0) = STEADY_STATE(rk) * exp((czcap / (1 - czcap)) * (zcap(0) - 1)); + + Sfunc(0) = (csadjcost / 2) * (((1 + ctrend / 100) * inve(0)) / inve(-1) - (1 + ctrend / 100)) ^ 2; + + SfuncD(0) = csadjcost * (((1 + ctrend / 100) * inve(0)) / inve(-1) - (1 + ctrend / 100)); + + a(0) = (1 - crhoa) + crhoa * a(-1) + ea / 100; + + b(0) = (1 - crhob) + crhob * b(-1) + (eb * -(((1 - chabb / (1 + ctrend / 100)) / (csigma * (1 + chabb / (1 + ctrend / 100)))) ^ -1)) / 100; + + gy(0) - cg = crhog * (gy(-1) - cg) + egy / 100 + (ea * cgy) / 100; + + qs(0) = (1 - crhoqs) + crhoqs * qs(-1) + (eqs * csadjcost * (1 + ctrend / 100) ^ 2 * (1 + (1 / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ (1 - csigma))) / 100; + + ms(0) = (1 - crhoms) + crhoms * ms(-1) + ems / 100; + + spinf(0) = ((1 - crhopinf) + crhopinf * spinf(-1) + epinfma(0)) - cmap * epinfma(-1); + + epinfma(0) = ((epinf * 1) / ((((1 / (1 + ((cindp * (1 + ctrend / 100) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma)) * (1 - cprobp) * (1 - ((cprobp * (1 + ctrend / 100) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma)) / cprobp) / (1 + curvp * (cfc - 1)))) / 100; + + sw(0) = ((1 - crhow) + crhow * sw(-1) + ewma(0)) - cmaw * ewma(-1); + + ewma(0) = ((ew * 1) / (((1 / (1 + curvw * (clandaw - 1))) * (1 - cprobw) * (1 - ((cprobw * (1 + ctrend / 100) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma)) / (cprobw * (1 + (((1 + ctrend / 100) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma)))) / 100; + + yflex(0) = cflex(0) + inveflex(0) + gy(0) * STEADY_STATE(yflex) + (afuncflex(0) * kpflex(-1)) / (1 + ctrend / 100); + + yflex(0) = a(0) * kflex(0) ^ calfa * labflex(0) ^ (1 - calfa) - (cfc - 1) * STEADY_STATE(yflex); + + kflex(0) = (kpflex(-1) * zcapflex(0)) / (1 + ctrend / 100); + + kpflex(0) = inveflex(0) * qs(0) * (1 - Sfuncflex(0)) + (kpflex(-1) * (1 - ctou)) / (1 + ctrend / 100); + + xiflex(0) = exp(((csigma - 1) / (1 + csigl)) * labflex(0) ^ (1 + csigl)) * (cflex(0) - (cflex(-1) * chabb) / (1 + ctrend / 100)) ^ -csigma; + + 1 = qs(0) * pkflex(0) * ((1 - Sfuncflex(0)) - ((1 + ctrend / 100) * inveflex(0) * SfuncDflex(0)) / inveflex(-1)) + ((((SfuncDflex(1) * qsaux(0) * xiflex(1)) / xiflex(0)) * pkflex(1) * (((1 + ctrend / 100) * inveflex(1)) / inveflex(0)) ^ 2 * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + xiflex(0) = ((xiflex(1) * b(0) * rrflex(0) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma; + + rkflex(0) = afuncDflex(0); + + pkflex(0) = (((((rkflex(1) * zcapflex(1) - afuncflex(1)) + (1 - ctou) * pkflex(1)) * xiflex(1) * 1) / (1 + constebeta / 100)) * (1 + ctrend / 100) ^ -csigma) / xiflex(0); + + kflex(0) = (((labflex(0) * calfa) / (1 - calfa)) * wflex(0)) / rkflex(0); + + mcflex = (wflex(0) ^ (1 - calfa) * rkflex(0) ^ calfa) / (a(0) * calfa ^ calfa * (1 - calfa) ^ (1 - calfa)); + + (wflex(0) * (1 + curvw * (1 - clandaw))) / (1 + (curvw * (1 - clandaw)) / clandaw) = STEADY_STATE(sw) * (labflex(0) ^ csigl * clandaw * (cflex(0) - (cflex(-1) * chabb) / (1 + ctrend / 100)) + (((wflex(0) * curvw * (1 - clandaw)) / clandaw) * (clandaw - 1)) / (1 + (curvw * (1 - clandaw)) / clandaw)); + + afuncflex(0) = ((STEADY_STATE(rkflex) * 1) / (czcap / (1 - czcap))) * (exp((czcap / (1 - czcap)) * (zcapflex(0) - 1)) - 1); + + afuncDflex(0) = STEADY_STATE(rkflex) * exp((czcap / (1 - czcap)) * (zcapflex(0) - 1)); + + Sfuncflex(0) = (csadjcost / 2) * (((1 + ctrend / 100) * inveflex(0)) / inveflex(-1) - (1 + ctrend / 100)) ^ 2; + + SfuncDflex(0) = csadjcost * (((1 + ctrend / 100) * inveflex(0)) / inveflex(-1) - (1 + ctrend / 100)); + + ygap(0) = 100 * log(y(0) / yflex(0)); + + dy(0) = ctrend + 100 * (y(0) / y(-1) - 1); + + dc(0) = ctrend + 100 * (c(0) / c(-1) - 1); + + dinve(0) = ctrend + 100 * (inve(0) / inve(-1) - 1); + + pinfobs(0) = 100 * (pinf(0) - STEADY_STATE(pinf)) + constepinf; + + robs(0) = 100 * (r(0) - 1); + + dwobs(0) = ctrend + 100 * (w(0) / w(-1) - 1); + + labobs(0) = 100 * (lab(0) / STEADY_STATE(lab) - 1); + +end; + +shocks; +var ea = 1; +var eb = 1; +var egy = 1; +var ems = 1; +var epinf = 1; +var eqs = 1; +var ew = 1; +end; + +initval; + Pratio = 1.0; + Sfunc = 0.0; + SfuncD = 0.0; + SfuncDflex = 0.0; + Sfuncflex = 0.0; + a = 1.0; + afunc = 2.6707642871043906e-18; + afuncD = 0.03250310455837918; + afuncDflex = 0.032503104558379174; + afuncflex = 2.9540420520779154e-18; + b = 1.0; + c = 0.8963673008108926; + cflex = 0.8963673008108919; + dc = 0.4419; + dinve = 0.4419; + dp = 1.0; + dw = 0.8323624555939105; + dwobs = 0.4419; + dy = 0.4419; + epinfma = 0.0; + ewma = 0.0; + gam1 = 4.071805532645228; + gam2 = 3.0289411088635187; + gam3 = 4.071805532645226; + gamw1 = 24.58768372203227; + gamw2 = 13.643909866824808; + gamw3 = 6.806204556019938; + gy = 0.18; + inve = 0.22229717097859672; + inveflex = 0.22229717097859789; + k = 7.55624497700795; + kflex = 7.556244977007963; + kp = 7.589636023561346; + kpflex = 7.58963602356136; + lab = 1.3439139854552384; + labflex = 1.3439139854552407; + labobs = 0.0; + mc = 0.7438815740534109; + ms = 1.0; + pdot = 1.0; + pdotl = 1.0; + pinf = 1.008731; + pinfobs = 0.8731; + pk = 1.0; + pkflex = 1.0; + qs = 1.0; + qsaux = 1.0; + r = 1.0162996141642786; + rk = 0.032503104558379174; + rkflex = 0.032503104558379174; + robs = 1.6299614164278609; + rrflex = 1.0075031045583793; + spinf = 1.0; + sw = 1.0; + w = 0.8323624555939108; + wdot = 1.0; + wdotl = 1.0000000000000002; + wflex = 0.8323624555939109; + wnew = 0.8323624555939105; + xi = 8.007548000200039; + xiflex = 8.007548000200062; + y = 1.3642249655969378; + yflex = 1.3642249655969414; + ygap = -2.553512956637863e-13; + zcap = 1.0000000000000002; + zcapflex = 1.0000000000000002; +end; + +stoch_simul(order = 1, irf = 40); From df31ddd930663f9b583f4bfad83dcee50417959c Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 22:54:31 +0200 Subject: [PATCH 17/24] Fix the inversion filter's Jacobian term, which entered at half weight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The change of variables y -> eps contributes -log|det Z| (Z = CB) to the loglikelihood, but `logabsdets` was placed inside the -1/2 alongside the quadratic form, so it contributed -1/2 log|det Z|. This is not a harmless constant: log|det Z| carries the shock standard deviations, so halving it halves the likelihood's penalty against large shocks. On an AR(1) the profile likelihood peaks at sig^2 = 2Q/T instead of Q/T — estimated shock standard deviations inflate by exactly sqrt(2), confirmed numerically at 1.4151 vs sqrt(2) = 1.4142. The inversion filter is the default for every nonlinear algorithm, so this affected higher-order estimation throughout. Verified on an AR(1) observed directly, where the conditional likelihood is closed form: Kalman and inversion now both return 959.0753, matching exactly; before, inversion returned 270.6024, short by exactly half the Jacobian. Fixed in all ten primal branches (first order, pruned 2nd/3rd, missing-data variants) and the ten copies in the rrules, together with every hand-written Jacobian cotangent, which simply doubles when the primal term goes from -1/2 L to -L (sign conventions are preserved by scaling). The gradient cross-checks (ForwardDiff vs Zygote vs FiniteDifferences, all five orders including the under-identified case) stay at 264/264, so the primal and the pullbacks moved together. Adds test/test_inversion_filter_likelihood.jl, which pins the *level* against closed-form answers rather than only checking finiteness or cross-path agreement — the bug was invisible to both, since every path shared it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 2 +- src/filter/inversion.jl | 20 ++--- src/rrules.jl | 66 +++++++------- test/runtests.jl | 1 + test/test_inversion_filter_likelihood.jl | 109 +++++++++++++++++++++++ 5 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 test/test_inversion_filter_likelihood.jl diff --git a/docs/src/filters.md b/docs/src/filters.md index efb07e860..a5de22046 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -174,6 +174,6 @@ Resampling only happens when the effective sample size ``1/\sum_i W_i^2`` falls - **Particle → Kalman.** On a *linear* model with Gaussian shocks, the particle filters estimate exactly the quantity the Kalman filter computes in closed form. As ``N \to \infty`` the particle log-likelihood converges to the Kalman log-likelihood (from below, by the Jensen bias above). This is the sharpest correctness check available and is exactly what the package's tests do, on both a small RBC model and Smets-Wouters (2007). - **Kalman → particle.** The Kalman filter is the special case where the transition and observation are linear and the noise Gaussian, so the "cloud" is fully described by its first two moments. - **Inversion → particle.** Both handle nonlinear models, but they make opposite trades. The inversion filter assumes measurement error is *zero* and recovers the shocks exactly; the particle filter assumes measurement error is *positive* and integrates the shocks out. As the measurement error goes to zero the particle filter degenerates towards the inversion filter's problem — and this is precisely where it needs the most particles. -- **Inversion → Kalman.** On a *first-order* solution with as many shocks as observables and no measurement error, the two agree. The Kalman filter's innovation covariance is then ``F_t = C P_t C'`` with ``P_t = BB'`` — the state is exactly identified by the data, so ``P_t`` never accumulates uncertainty and the update is a deterministic inversion. Both filters end up scoring the same shock path, the inversion filter directly as ``-\tfrac12(\varepsilon_t'\varepsilon_t + \log 2\pi)`` plus a Jacobian term, the Kalman filter through ``v_t'F_t^{-1}v_t + \log\det F_t``, and these are the same number written two ways. They part company as soon as either assumption breaks: with *more observables than shocks* the system is stochastically singular and only the Kalman filter (with measurement error) is defined; with *fewer* observables than shocks the state is no longer pinned down by the data, ``P_t`` is genuinely non-degenerate, and only the Kalman filter integrates over it. Add measurement error and the inversion filter is not defined at all. At higher order the inversion filter's per-period Newton solve has no Kalman counterpart, which is why it — not the Kalman filter — is the default for nonlinear algorithms. +- **Inversion → Kalman.** The relationship is exact and worth stating precisely. Writing ``Z = CB``, the inversion filter's per-period score is ``\log N(v_t; 0, ZZ')`` with ``v_t = y_t - CA\hat x_{t-1}`` — in *both* the square case (``Z^{-1}``) and the under-determined case (minimum norm, ``Z^{+} = Z'(ZZ')^{-1}``), since ``\|Z^{+}v\|^2 = v'(ZZ')^{-1}v``. The minimum-norm choice is not an arbitrary tie-break: for Gaussian shocks ``Z^{+}v = E[\varepsilon \mid v]``, the conditional mean. That expression is exactly the Kalman contribution with the posterior state covariance **clamped to zero** (``P_{t|t-1} = BB'``), and correspondingly the gains coincide: the inversion filter's is ``BZ^{+}``, the Kalman's is ``P_tC'F_t^{-1}``, equal iff ``P_{t|t-1} = BB'``. So: *the inversion filter is the Kalman filter that assumes the state is known exactly.* Whether that is legitimate is precisely whether ``P_{t|t} = P - PC'(CPC')^{-1}CP`` really vanishes, which needs ``\mathrm{rank}(CB) = n_\varepsilon`` — **at least as many observables as shocks**. With *more observables than shocks* the system is stochastically singular and only the Kalman filter (with measurement error) is defined. With *more shocks than observables* the clamp is simply false: ``P_{t|t} > 0`` necessarily, so ``F_t^{\text{kal}} = ZZ' + CAP_{t|t}A'C' \supsetneq ZZ' = F^{\text{inv}}`` and the inversion filter understates the innovation covariance — it treats innovations as more surprising than they are, because it pretends to know a state it cannot know. The size of the discrepancy is governed not by the shock/observable counts as such but by how much of the *unidentified* subspace propagates into the next period's observables, ``\|CAP_{t|t}A'C'\|`` relative to ``\|ZZ'\|``; when the unidentified directions barely propagate the two nearly agree anyway. There is a second, related gap: the inversion filter minimises ``\|\varepsilon_t\|`` *greedily*, period by period, ignoring that the null-space component moves ``x_t`` and hence the cost of matching later observations, whereas the Kalman disturbance smoother solves the same minimum-norm problem globally over the whole path. Finally, even when ``n_y = n_\varepsilon`` the agreement is only asymptotic: the state-estimate error obeys ``\delta_t = (I - BZ^{-1}C)A\,\delta_{t-1}``, whose spectral radius is the invertibility (fundamentalness) condition, so a near-unit-root inverse system takes many periods to forget the initial condition. Add measurement error and the inversion filter is not defined at all. At higher order its per-period Newton solve has no Kalman counterpart, which is why it — not the Kalman filter — is the default for nonlinear algorithms. - **Correlated measurement error.** All filters that admit measurement error accept an arbitrary covariance: pass `measurement_error` a matrix instead of a vector of variances. The Kalman filter adds it to ``F_t`` directly; the particle filters factorise ``H`` once per missing-data pattern and score against the resulting triangular solve. The diagonal case is detected and takes a faster elementwise path, so there is no cost to the common case. A third option is to write the correlation into the model itself as measurement-error processes in the observation equations, which moves it into the state transition and makes ``H`` diagonal again — worth doing when the measurement errors are persistent rather than merely contemporaneously correlated. diff --git a/src/filter/inversion.jl b/src/filter/inversion.jl index 0c8773cb4..346a98d3d 100644 --- a/src/filter/inversion.jl +++ b/src/filter/inversion.jl @@ -819,7 +819,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # timeit_debug # end # timeit_debug - return -(logabsdets + shocks² + (length(observables_index) * (n_obs - presample_periods) + hidden_warmup_shock_dimension(T.nExo, warmup_iterations)) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (length(observables_index) * (n_obs - presample_periods) + hidden_warmup_shock_dimension(T.nExo, warmup_iterations)) * log(2π)) / 2 # return -(logabsdets + (length(observables) * (warmup_iterations + n_obs - presample_periods)) * log(2π)) / 2 end @@ -1137,7 +1137,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf and Fair and Taylor (1983) - return -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @@ -1418,7 +1418,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf - return -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 end function calculate_loglikelihood(::Val{:inversion}, @@ -1921,7 +1921,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf - return -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @@ -2306,7 +2306,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf - return -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @unstable function filter_data_with_model(𝓂::ℳ, @@ -4959,7 +4959,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:first_or ℒ.mul!(state, 𝐒past, state_concat) end - return -(logabsdets + shocks² + (n_hidden_warmup_shock_dims + n_obs_total) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (n_hidden_warmup_shock_dims + n_obs_total) * log(2π)) / 2 end @@ -5150,7 +5150,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end - return -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @@ -5323,7 +5323,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) end - return -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @@ -5589,7 +5589,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t ℒ.mul!(st3, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) end - return -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 end @@ -5807,7 +5807,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or ℒ.mul!(st, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) end - return -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + return -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 end end # @stable diff --git a/src/rrules.jl b/src/rrules.jl index b4fe1dd9b..7366edfa3 100644 --- a/src/rrules.jl +++ b/src/rrules.jl @@ -8848,7 +8848,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ℒ.mul!(state_seq[t+1], 𝐒, concat_buf) end - llh = -(logabsdets + shocks² + (n_hidden_warmup_shock_dims + n_obs_total) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (n_hidden_warmup_shock_dims + n_obs_total) * log(2π)) / 2 if llh < -1e12 || !isfinite(llh) return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) end @@ -8937,15 +8937,15 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if t > presample_periods - # logabsdet[t] term: ∂jac_v += -∂llh/2 * pinv(jac_v)' + # logabsdet[t] term: ∂jac_v += -∂llh * pinv(jac_v)' if m == n_exo invjac_v = invjac_v_seq[t] - ∂jac_v = ∂jac_v .+ (-∂llh / 2) .* invjac_v' + ∂jac_v = ∂jac_v .+ (-∂llh) .* invjac_v' else G = G_seq[t] jac_v = jac_full[idx, :] pinvA_T = G * jac_v - ∂jac_v = ∂jac_v .+ (-∂llh / 2) .* pinvA_T + ∂jac_v = ∂jac_v .+ (-∂llh) .* pinvA_T end end @@ -9328,7 +9328,7 @@ function rrule(::typeof(calculate_loglikelihood), # state[i+1] = 𝐒 * vcat(state[i][t⁻], x[i]) (only t⁻ rows are ever read) end - llh = -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + hidden_warmup_shock_dimension(T.nExo, warmup_iterations)) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + hidden_warmup_shock_dimension(T.nExo, warmup_iterations)) * log(2π)) / 2 if llh < -1e12 return on_failure_loglikelihood, x -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -9394,9 +9394,9 @@ function rrule(::typeof(calculate_loglikelihood), col_off = n_cols - T.nExo # Constant-jac logabsdet contribution (scales with Tt - presample). - # Square: ∂jac += -(Tt - p)/2 * invjac' - # Fat : ∂jac += -(Tt - p)/2 * (G * jac) (since d log|det(JJt)|/2 / d jac = G * jac) - factor = -(Tt - presample_periods) / 2 + # Square: ∂jac += -(Tt - p) * invjac' + # Fat : ∂jac += -(Tt - p) * (G * jac) (since d log|det(JJt)|/2 / d jac = G * jac) + factor = -(Tt - presample_periods) if T.nExo == n_obs_loc invjac_T = invjac' for j in 1:T.nExo @@ -9695,7 +9695,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(state₂_seq[t+1], state₂) end - llh = -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 if !isfinite(llh) || llh < -1e12 return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -9802,13 +9802,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} @inbounds for k in 1:n_exo ∂x[k] += -x[k] end - # logabsdet pullback: ∂jac_v += -1/2 * pinv(jac_v)' + # logabsdet pullback: ∂jac_v += -1 * pinv(jac_v)' if m == n_exo invjac_v = inv(jac_v_local) - ∂jac_v .+= (-0.5) .* invjac_v' + ∂jac_v .+= (-1.0) .* invjac_v' else G = inv(jac_v_local * jac_v_local') - ∂jac_v .+= (-0.5) .* (G * jac_v_local) + ∂jac_v .+= (-1.0) .* (G * jac_v_local) end # Add ∂jac_v's contribution to ∂x via the (I⊗x) term in jac_v. # d jac_v[i,r] / d x_l = 2 𝐒ⁱ²ᵉ_v[i, (r-1)n_exo + l] @@ -11878,10 +11878,10 @@ function rrule(::typeof(calculate_loglikelihood), if !ℒ.issuccess(jacc_lu) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end - copyto!(∂jacc_buf, inv(jacc_lu)') + copyto!(∂jacc_buf, 2 .* inv(jacc_lu)') ∂jacc = ∂jacc_buf else - ∂jacc = ℒ.pinv(jacc[i])' + ∂jacc = 2 .* ℒ.pinv(jacc[i])' if !all(isfinite, ∂jacc) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end @@ -12090,7 +12090,7 @@ function rrule(::typeof(calculate_loglikelihood), end # See: https://pcubaborda.net/documents/CGIZ-final.pdf - llh = -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 return llh, inversion_filter_loglikelihood_pullback end @@ -12277,7 +12277,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(st_seq[t+1], st) end - llh = -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 if !isfinite(llh) || llh < -1e12 return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -12361,10 +12361,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if m == n_exo invjac_v = inv(jac_v_local) - ∂jac_v .+= (-0.5) .* invjac_v' + ∂jac_v .+= (-1.0) .* invjac_v' else G = inv(jac_v_local * jac_v_local') - ∂jac_v .+= (-0.5) .* (G * jac_v_local) + ∂jac_v .+= (-1.0) .* (G * jac_v_local) end # Indirect channel: ∂jac_v → x via the (I⊗x) term in jac_v. # d jac_v[i,r] / d x_l = 2 𝐒ⁱ²ᵉ_v[i, (r-1)n_exo + l] @@ -12984,10 +12984,10 @@ function rrule(::typeof(calculate_loglikelihood), if !ℒ.issuccess(jacc_lu) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end - copyto!(∂jacc_buf, inv(jacc_lu)') + copyto!(∂jacc_buf, 2 .* inv(jacc_lu)') ∂jacc = ∂jacc_buf else - ∂jacc = ℒ.pinv(jacc[i])' + ∂jacc = 2 .* ℒ.pinv(jacc[i])' if !all(isfinite, ∂jacc) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end @@ -13169,7 +13169,7 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf - llh = -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 return llh, inversion_filter_loglikelihood_pullback end @@ -13460,7 +13460,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(state₃_seq[t+1], state₃) end - llh = -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 if !isfinite(llh) || llh < -1e12 return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -13606,10 +13606,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if m == n_exo invjac_v = inv(jac_v_local) - ∂jac_v .+= (-0.5) .* invjac_v' + ∂jac_v .+= (-1.0) .* invjac_v' else G = inv(jac_v_local * jac_v_local') - ∂jac_v .+= (-0.5) .* (G * jac_v_local) + ∂jac_v .+= (-1.0) .* (G * jac_v_local) end # Indirect channel: ∂jac_v → ∂x via the (J⊗x) and (J⊗ kron(x,x)) terms in jac_v. # d jac_v[i,r]/dx_l = 2 𝐒ⁱ²ᵉ_v[i,(r-1)n+l] + 3 (Σ_q 𝐒ⁱ³ᵉ_v[i,(r-1)n²+(l-1)n+q] x_q + Σ_p 𝐒ⁱ³ᵉ_v[i,(r-1)n²+(p-1)n+l] x_p) @@ -14309,7 +14309,7 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # See: https://pcubaborda.net/documents/CGIZ-final.pdf - llh = -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 ∂𝐒 = [zero(𝐒[1]), zero(𝐒[2]), zero(𝐒[3])] @@ -14488,10 +14488,10 @@ function rrule(::typeof(calculate_loglikelihood), if !ℒ.issuccess(jacc_lu) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end - copyto!(∂jacc_buf, inv(jacc_lu)') + copyto!(∂jacc_buf, 2 .* inv(jacc_lu)') ∂jacc = ∂jacc_buf else - ∂jacc = ℒ.pinv(jacc[i])' + ∂jacc = 2 .* ℒ.pinv(jacc[i])' if !all(isfinite, ∂jacc) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end @@ -14999,7 +14999,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(st_seq[t+1], st) end - llh = -(logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (n_obs_total + n_hidden_warmup_shock_dims) * log(2π)) / 2 if !isfinite(llh) || llh < -1e12 return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -15103,10 +15103,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if m == n_exo invjac_v = inv(jac_v_local) - ∂jac_v .+= (-0.5) .* invjac_v' + ∂jac_v .+= (-1.0) .* invjac_v' else G = inv(jac_v_local * jac_v_local') - ∂jac_v .+= (-0.5) .* (G * jac_v_local) + ∂jac_v .+= (-1.0) .* (G * jac_v_local) end # Indirect channel: ∂jac_v → ∂x @inbounds for l in 1:n_exo @@ -16403,7 +16403,7 @@ function rrule(::typeof(calculate_loglikelihood), end # See: https://pcubaborda.net/documents/CGIZ-final.pdf - llh = -(logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 + llh = -(2 * logabsdets + shocks² + (length(observables_index) * n_effective_obs + n_hidden_warmup_shock_dims) * log(2π)) / 2 # end # timeit_debug # end # timeit_debug @@ -16540,10 +16540,10 @@ function rrule(::typeof(calculate_loglikelihood), if !ℒ.issuccess(jacc_lu) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end - copyto!(∂jacc_buf, inv(jacc_lu)') + copyto!(∂jacc_buf, 2 .* inv(jacc_lu)') ∂jacc = ∂jacc_buf else - ∂jacc = ℒ.pinv(jacc[i])' + ∂jacc = 2 .* ℒ.pinv(jacc[i])' if !all(isfinite, ∂jacc) return NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent() end diff --git a/test/runtests.jl b/test/runtests.jl index 2709478c9..0d58ed59c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,6 +63,7 @@ elseif test_set == "gradient_checks" include("test_missing_data.jl") include("test_rrule_robustness.jl") include("test_inversion_filter_gradients.jl") + include("test_inversion_filter_likelihood.jl") include("test_filter_free_gradients.jl") include("test_initial_state.jl") elseif test_set == "update_equations" diff --git a/test/test_inversion_filter_likelihood.jl b/test/test_inversion_filter_likelihood.jl new file mode 100644 index 000000000..c1641be44 --- /dev/null +++ b/test/test_inversion_filter_likelihood.jl @@ -0,0 +1,109 @@ +using Test +using MacroModelling +using Random +using AxisKeys +import LinearAlgebra as ℒ + +# ----------------------------------------------------------------------------- +# Level checks for the inversion-filter loglikelihood. +# +# The inversion filter recovers the shocks that reproduce the data exactly and +# scores them under their own standard normal prior, with a Jacobian term for the +# change of variables y -> eps: +# +# log p(y_t | x_{t-1}) = -1/2 (eps' eps + n log 2pi) - log|det Z|, Z = C B +# +# The Jacobian enters with weight one, not one half. Getting that weight wrong is +# invisible in a gradient cross-check (every path shares the same primal) and +# invisible in any test that only asserts finiteness, but it biases estimation: +# log|det Z| carries the shock standard deviations, so halving it halves the +# likelihood's penalty against large shocks and inflates their estimates by +# sqrt(2). These tests pin the level against closed-form answers instead. +# ----------------------------------------------------------------------------- + +@testset "inversion filter loglikelihood level" begin + + # An AR(1) observed directly: the conditional likelihood is available in + # closed form, so this pins the absolute level rather than a comparison. + @model AR1_lik begin + z[0] = rho * z[-1] + sig * e_z[x] + y[0] = z[0] + 0 * y[1] + end + + @parameters AR1_lik begin + rho = 0.5 + sig = 0.01 + end + + Random.seed!(7) + nT = 300 + rho_true, sig_true = 0.5, 0.01 + z = zeros(nT) + for t in 2:nT + z[t] = rho_true * z[t-1] + sig_true * randn() + end + data = KeyedArray(reshape(z, 1, nT); Variable = [:y], Time = 1:nT) + + # p(y_2..y_T | y_1); presample_periods = 1 drops the first observation, whose + # treatment differs between the filters (ergodic prior vs. known initial state) + exact = -0.5 * sum(((z[t] - rho_true * z[t-1]) / sig_true)^2 + log(sig_true^2) + log(2π) + for t in 2:nT) + + llh_inv = get_loglikelihood(AR1_lik, data, AR1_lik.parameter_values; + filter = :inversion, presample_periods = 1) + llh_kal = get_loglikelihood(AR1_lik, data, AR1_lik.parameter_values; + filter = :kalman, presample_periods = 1) + + @test isapprox(llh_inv, exact, rtol = 1e-8) + @test isapprox(llh_kal, exact, rtol = 1e-8) + + # The shock standard deviation must be identified correctly. With the + # Jacobian at half weight the profile likelihood peaks at sqrt(2) times the + # true value, so this is the sharpest guard against that regression. + grid = range(0.006, 0.020, length = 701) + prof = [get_loglikelihood(AR1_lik, data, [rho_true, s]; + filter = :inversion, presample_periods = 1) for s in grid] + Q = sum((z[t] - rho_true * z[t-1])^2 for t in 2:nT) + @test isapprox(grid[argmax(prof)], sqrt(Q / (nT - 1)), rtol = 2e-3) + + # On a linear model with as many shocks as observables and no measurement + # error the inversion filter and the Kalman filter identify the same state, + # so their likelihoods agree once the initial-condition transient has died. + @model RBC_lik begin + 1 / c[0] = (β / c[1]) * (α * exp(z[1]) * k[0]^(α - 1) + (1 - δ)) + c[0] + k[0] = (1 - δ) * k[-1] + q[0] + q[0] = exp(z[0]) * k[-1]^α * exp(g[0]) + z[0] = ρz * z[-1] + std_z * eps_z[x] + g[0] = ρg * g[-1] + std_g * eps_g[x] + end + + @parameters RBC_lik begin + std_z = 0.01 + std_g = 0.01 + ρz = 0.4 + ρg = 0.6 + δ = 0.02 + α = 0.5 + β = 0.95 + end + + Random.seed!(12345) + sim = simulate(RBC_lik, periods = 200) + d2 = sim([:c, :q], :, :simulate) # 2 observables, 2 shocks + p = RBC_lik.parameter_values + + k2 = get_loglikelihood(RBC_lik, d2, p; filter = :kalman, presample_periods = 50) + i2 = get_loglikelihood(RBC_lik, d2, p; filter = :inversion, presample_periods = 50) + @test isapprox(i2, k2, atol = 0.5) + + # With more shocks than observables the state is no longer pinned down by the + # data. The inversion filter clamps the state covariance to zero, so it uses + # a strictly smaller innovation covariance than the Kalman filter and the two + # part company. They stay in the same ballpark here only because little of the + # unidentified subspace propagates into the next period's observables. + d1 = sim([:c], :, :simulate) # 1 observable, 2 shocks + k1 = get_loglikelihood(RBC_lik, d1, p; filter = :kalman, presample_periods = 50) + i1 = get_loglikelihood(RBC_lik, d1, p; filter = :inversion, presample_periods = 50) + @test isfinite(i1) && isfinite(k1) + @test isapprox(i1, k1, atol = 5.0) +end From 0622332ed20167c420e98be5be2cda685557c84f Mon Sep 17 00:00:00 2001 From: thorek1 Date: Sun, 26 Jul 2026 23:12:35 +0200 Subject: [PATCH 18/24] Add filter-equivalence tests anchored on the initial state covariance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sharp statement of how the filters relate is that the inversion filter *is* the Kalman filter started at P₁ = BB'. The inversion filter assumes x₀ is known exactly, so the only uncertainty about x₁ is that period's shocks; given at least as many observables as shocks the update then drives the posterior covariance to exactly zero and it stays there, so the two agree period by period rather than merely asymptotically. Verified to machine precision: identical on the small RBC, and 1.6e-9 apart on Smets-Wouters (2007) with 7 shocks, 7 observables and 184 periods. The ergodic prior is a genuinely different starting point — 489 log points away on SW07 — so the test also asserts that gap, guarding against passing for the trivial reason that the initial covariance does not matter. The particle-filter side of the same statement was already covered: the initial cloud is drawn with the ergodic covariance, Var(x₁) = AΣA' + BB' = Σ, which is `initial_covariance = :theoretical`. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- test/test_inversion_filter_likelihood.jl | 18 ++++++ test/test_particle_filter_sw07.jl | 71 ++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/test/test_inversion_filter_likelihood.jl b/test/test_inversion_filter_likelihood.jl index c1641be44..ca9084767 100644 --- a/test/test_inversion_filter_likelihood.jl +++ b/test/test_inversion_filter_likelihood.jl @@ -96,6 +96,24 @@ import LinearAlgebra as ℒ i2 = get_loglikelihood(RBC_lik, d2, p; filter = :inversion, presample_periods = 50) @test isapprox(i2, k2, atol = 0.5) + # Sharper: the whole difference between the two filters on a square system is + # the initial state covariance. The inversion filter assumes x₀ is known, so + # Var(x₁) = BB'; hand the Kalman filter that same prior and the two agree to + # machine precision, period by period, with no presample needed. + get_loglikelihood(RBC_lik, d2, p) # populate the solution cache + Tc = RBC_lik.constants.post_model_macro + S1 = RBC_lik.caches.first_order_solution_matrix + nP = Tc.nPast_not_future_and_mixed + ssn = RBC_lik.constants.post_complete_parameters.SS_and_pars_names + oas = sort(union(Tc.past_not_future_and_mixed_idx, + convert(Vector{Int}, indexin([:c, :q], ssn)))) + Bm = S1[oas, nP+1:end] + + @test isapprox(get_loglikelihood(RBC_lik, d2, p; filter = :inversion), + get_loglikelihood(RBC_lik, d2, p; filter = :kalman, + initial_covariance = Bm * Bm'), + rtol = 1e-10) + # With more shocks than observables the state is no longer pinned down by the # data. The inversion filter clamps the state covariance to zero, so it uses # a strictly smaller innovation covariance than the Kalman filter and the two diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index 4402bb094..473f63447 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -48,3 +48,74 @@ using DelimitedFiles, AxisKeys @test abs(kal - Statistics.mean(lls)) < 15 end end + +# ----------------------------------------------------------------------------- +# Filter equivalences on SW07, expressed through the initial state covariance. +# +# `initial_covariance` is the prior on the *state* at the start of the sample — +# where the economy was before the first observation. It is a different object +# from `measurement_error`, which is noise on the *observation* and enters every +# period forever. P₁ is transient: its influence decays at the rate of the +# filter's own error dynamics, which is exactly why the three filters agree only +# once their initial conditions are made to match. +# +# inversion <=> Kalman with P₁ = BB' +# The inversion filter assumes the state is known exactly at t = 0, so the +# only uncertainty about x₁ is the first period's shocks: Var(x₁) = BB'. +# Given at least as many observables as shocks, the update then drives the +# posterior covariance to exactly zero and it stays there, so the two +# filters coincide period by period, not merely asymptotically. +# +# particle <=> Kalman with P₁ = ergodic +# The initial cloud is drawn around the mean with the ergodic covariance Σ, +# so Var(x₁) = AΣA' + BB' = Σ, matching `initial_covariance = :theoretical`. +# (Covered by the measurement-error testset above.) +# ----------------------------------------------------------------------------- +@testset "SW07: inversion equals Kalman started at BB'" begin + dat, header = readdlm(joinpath(@__DIR__, "data", "usmodel.csv"), ',', header = true) + dat = Float64.(dat) + csv_names = vec(Symbol.(strip.(header))) + data = KeyedArray(dat', Variable = csv_names, Time = axes(dat, 1)) + data = data([:dy, :dc, :dinve, :labobs, :pinfobs, :dw, :robs], 47:230) + observables = [:dy, :dc, :dinve, :labobs, :pinfobs, :dwobs, :robs] + data = rekey(data, :Variable => observables) + + include("../models/Smets_Wouters_2007_linear.jl") + SS(Smets_Wouters_2007_linear, parameters = [:crhoms => 0.01, :crhopinf => 0.01, :crhow => 0.01, :cmap => 0.01, :cmaw => 0.01]) + m = Smets_Wouters_2007_linear + p = m.parameter_values + + # populate the first-order solution cache at these parameters + get_loglikelihood(m, data, p) + + # Var(x₁ | x₀ known) = BB', over the Kalman filter's state ordering + # (`union(past states, observables)`, sorted). + T = m.constants.post_model_macro + S1 = m.caches.first_order_solution_matrix + nP = T.nPast_not_future_and_mixed + past = T.past_not_future_and_mixed_idx + ssn = m.constants.post_complete_parameters.SS_and_pars_names + obs_idx = convert(Vector{Int}, indexin(observables, ssn)) + oas = sort(union(past, obs_idx)) + B = S1[oas, nP+1:end] + P1 = B * B' + + llh_inv = get_loglikelihood(m, data, p; filter = :inversion) + llh_kal = get_loglikelihood(m, data, p; filter = :kalman, initial_covariance = P1) + + @test isfinite(llh_inv) + # 7 shocks, 7 observables, no measurement error: the two are the same filter + @test isapprox(llh_inv, llh_kal, rtol = 1e-9) + + # The ergodic prior is a genuinely different starting point, and on SW07 the + # difference is large and persistent — this guards against the test passing + # for the trivial reason that every initial covariance gives the same answer. + llh_kal_erg = get_loglikelihood(m, data, p; filter = :kalman) + @test abs(llh_kal_erg - llh_inv) > 100 + + # Dropping the first observations does not reconcile them either: SW07's + # inverse-system dynamics are slow, so the initial condition is still felt. + @test !isapprox(get_loglikelihood(m, data, p; filter = :inversion, presample_periods = 20), + get_loglikelihood(m, data, p; filter = :kalman, presample_periods = 20), + rtol = 1e-3) +end From c937998217b92092445164ef1a274baff72a6d10 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Mon, 27 Jul 2026 04:56:50 +0200 Subject: [PATCH 19/24] Pin the particle filter's initial-covariance timing convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The particle filters' `initial_covariance` is Var(x₀) — the cloud is drawn around the initial state and *then* propagated — while the Kalman filter's argument of the same name is P₁ = Var(x₁), the first predicted state. They correspond as P₁ = A·Var(x₀)·A' + BB'. The distinction is invisible at the `:theoretical` default, because the ergodic covariance is the fixed point of exactly that map and so is carried to itself. That is why passing `:theoretical` to both filters lines them up, and why the difference went unnoticed. It bites as soon as an explicit matrix is supplied: reproducing a Kalman run with P₁ = BB' (i.e. the inversion filter) needs a *zero* matrix here, not BB'. Verified on SW07 at both ends of the correspondence, with measurement error and 20,000 particles: Var(x₀)=0 vs Kalman P₁=BB' agree to 1.55 log points, and Var(x₀)=BB' vs Kalman P₁=A BB' A'+BB' to 3.34 — both within Monte-Carlo error and on the expected (downward) side of it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- src/filter/particle.jl | 11 ++++++++++ test/test_particle_filter_sw07.jl | 34 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 47282f2fc..3deb3a2b2 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -197,6 +197,17 @@ end # from the cached first-order solution, the usual choice for a stationary model); # `:diagonal` uses 10·I; an nVars×nVars matrix is used directly. # +# Note the timing convention, which differs from the Kalman filter's argument of +# the same name. Here Σ is the covariance of x₀, the cloud the filter starts +# from *before* the first transition; the Kalman filter's `initial_covariance` is +# P₁ = Var(x₁), the covariance of the first *predicted* state. The two therefore +# correspond as P₁ = A Σ A' + BB'. This is invisible at the `:theoretical` +# default — the ergodic covariance is the fixed point of that very map, so it is +# carried to itself — which is why passing `:theoretical` to both filters lines +# them up. It matters as soon as an explicit matrix is supplied: to reproduce a +# Kalman run with P₁ = BB' (i.e. the inversion filter), pass a zero matrix here, +# not BB'. +# # Σ is deliberately the *first-order* covariance at every perturbation order, and # does not follow `algorithm`. Elsewhere in the package `:theoretical` is only # ever reached from the Kalman filter (`get_initial_covariance` in kalman.jl), diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index 473f63447..6b542fb80 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -3,6 +3,7 @@ using Test import Random import Statistics using DelimitedFiles, AxisKeys +import LinearAlgebra as ℒ # Validate the particle filter on the Smets & Wouters (2007) linear model and the # US data used by the estimation tests: in the linear (first-order) case every @@ -118,4 +119,37 @@ end @test !isapprox(get_loglikelihood(m, data, p; filter = :inversion, presample_periods = 20), get_loglikelihood(m, data, p; filter = :kalman, presample_periods = 20), rtol = 1e-3) + + # The particle filters' `initial_covariance` is Var(x₀) — the cloud is drawn + # around the initial state and *then* propagated — whereas the Kalman filter's + # is Var(x₁), the covariance of the first predicted state. The two therefore + # correspond as P₁ = A·Var(x₀)·A' + BB'. This is invisible at the + # `:theoretical` default, because the ergodic Σ is the fixed point of + # Σ = AΣA' + BB' and the shift maps it to itself, which is why the + # measurement-error testset above can pass the same symbol to both. Pin both + # ends of the correspondence so the distinction cannot drift. + Ak = S1[oas, 1:nP] * Matrix(1.0 * ℒ.I, T.nVars, T.nVars)[past, oas] + me = 2.0 .* [Statistics.std(collect(data(o))) for o in observables] + + # Var(x₀) = 0 ⇒ P₁ = BB' + kal_BB = get_loglikelihood(m, data, p; filter = :kalman, + initial_covariance = P1, measurement_error = me .^ 2) + pf_0 = [get_loglikelihood(m, data, p; filter = :bootstrap_particle, algorithm = :first_order, + initial_covariance = zeros(T.nVars, T.nVars), + measurement_error = me .^ 2, n_particles = 20_000, + particle_rng = Random.Xoshiro(300 + s)) for s in 1:4] + @test all(isfinite, pf_0) + @test abs(kal_BB - Statistics.mean(pf_0)) < 8 + + # Var(x₀) = BB' ⇒ P₁ = A BB' A' + BB' + Bfull = S1[:, nP+1:end] + kal_shift = get_loglikelihood(m, data, p; filter = :kalman, + initial_covariance = Ak * P1 * Ak' + P1, + measurement_error = me .^ 2) + pf_BB = [get_loglikelihood(m, data, p; filter = :bootstrap_particle, algorithm = :first_order, + initial_covariance = Bfull * Bfull', + measurement_error = me .^ 2, n_particles = 20_000, + particle_rng = Random.Xoshiro(400 + s)) for s in 1:4] + @test all(isfinite, pf_BB) + @test abs(kal_shift - Statistics.mean(pf_BB)) < 8 end From 446a57514473cb18885cc291f1623684bc169a06 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Mon, 27 Jul 2026 09:31:04 +0200 Subject: [PATCH 20/24] Document measurement error, the initial covariance, and the filter-free path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filters.md explained each filter but not the two inputs that cut across all of them, nor the conditions under which they are the same filter. Adds: - "Measurement error and the initial covariance": what each is and, more to the point, how they differ — H is noise on the observation and acts every period forever, P₁ is a prior on the state and decays. The three jobs H does (genuine measurement error, stochastic singularity, misspecification) are separated, along with the fourth, purely computational one for the particle filters. Includes the timing-convention warning (particle filters take Var(x₀), the Kalman filter Var(x₁)) and the interaction: H > 0 forces a strictly positive steady-state P, which is why the inversion filter's P ≡ 0 and measurement error are contradictory rather than merely unimplemented. - "When are they the same filter?": the equivalences as a table with their conditions. The sharp one is that the inversion filter *is* the Kalman filter started at P₁ = BB' — exact period by period given n_y ≥ n_ε and H = 0. - "More shocks than observables": spells out the implicit assumption the inversion filter makes there. Minimum norm is the conditional mean, so the score stays a proper density; what fails is the clamp P ≡ 0, which needs rank(CB) = n_ε. The filter then understates the innovation covariance, by an amount governed by how much of the unidentified subspace propagates into the next period's observables — measurable, and cheap, from a first-order Kalman recursion. - "The filter-free likelihood": the fourth option, which does not filter at all but treats the shocks as parameters. Notes why it has no initial covariance, why measurement error is mandatory there, and why its argument keeps the `_std` suffix (a matrix means per-period standard deviations, not a covariance). Verified by building the page: 4 tables, the admonition and both cross- references render, with no pipe collision in the table maths. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 122 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/docs/src/filters.md b/docs/src/filters.md index a5de22046..4080c3daa 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -22,6 +22,8 @@ Select a filter with the `filter` keyword: get_loglikelihood(model, data, parameters; filter = :kalman) ``` +Two inputs cut across all of them and are covered separately below: `measurement_error`, the noise on the observation, and `initial_covariance`, the prior on the state at the start of the sample. There is also a [filter-free likelihood](@ref "The filter-free likelihood") that does not integrate the shocks out at all, but treats them as parameters to be sampled. + ## Choosing a filter | filter | models | likelihood | differentiable | measurement error | smoothing | relative cost | @@ -89,6 +91,33 @@ In exchange it is exact for nonlinear models, deterministic, and differentiable, **References:** Fair & Taylor (1983); Cuba-Borda, Guerrieri, Iacoviello & Zhong (2019). +### More shocks than observables + +The inversion filter still runs when there are more shocks than observables: the per-period system ``y_t = CAx_{t-1} + Z\varepsilon_t`` (with ``Z = CB``) is then under-determined, and it returns the **minimum-norm** solution ``\varepsilon_t = Z^{+}v_t``, ``Z^{+} = Z'(ZZ')^{-1}``. Two things are worth being explicit about, because neither is visible from the output. + +First, the good news: minimum norm is not an arbitrary tie-break. For Gaussian shocks ``Z^{+}v = E[\varepsilon \mid v]`` is the conditional mean, and ``\|Z^{+}v\|^2 = v'(ZZ')^{-1}v``, so the score remains a proper Gaussian density ``\log N(v_t; 0, ZZ')`` — exactly the same expression as in the square case. + +Second, the **implicit assumption**. That expression is the Kalman contribution with the posterior state covariance *clamped to zero*. The filter propagates a single point ``\hat x_t`` and, at the next period, treats it as if it were known exactly. That is self-consistent only when the observation actually pins the state down, i.e. when + +```math +P_{t|t} = P - PC'(CPC')^{-1}CP = 0 \quad\Longleftrightarrow\quad \mathrm{rank}(CB) = n_\varepsilon, +``` + +which requires **at least as many observables as shocks**. With more shocks than observables the rank condition fails, ``P_{t|t} > 0`` necessarily, and the assumption is simply false. The consequence is that + +```math +F^{\text{kal}}_t = ZZ' + CA\,P_{t|t}\,A'C' \;\supsetneq\; ZZ' = F^{\text{inv}}, +``` + +so the inversion filter **understates the innovation covariance**: it treats innovations as more surprising than they are, because it is pretending to know a state it cannot know. Its likelihood is a certainty-equivalent approximation, not ``p(y_{1:T})``. + +How wrong it is depends on ``\|CA P_{t|t} A'C'\|`` relative to ``\|ZZ'\|`` — how much of the *unidentified* subspace propagates into the next period's observables — rather than on the shock/observable counts as such. That ratio is computable from a cheap first-order Kalman recursion even when the model is being filtered at third order, and is the right thing to look at before trusting an under-identified inversion likelihood. On the package's small RBC example with one observable and two shocks it is ``\approx 0.008``, which is why the two filters nearly agree there despite the state being genuinely unidentified; in a model whose unidentified directions propagate strongly the gap would be large. + +There is a second, separate approximation: the minimum-norm choice is made **greedily**, period by period, minimising ``\|\varepsilon_t\|`` given ``\hat x_{t-1}`` without regard for the fact that the null-space component moves ``x_t`` and hence the cost of matching later observations. The Kalman disturbance smoother solves the same minimum-norm problem *globally* over the whole path. The two coincide only when there is no null space to redistribute over, i.e. ``n_y = n_\varepsilon``. + +If the ratio above is large, the options are to rebalance the model so that ``n_y = n_\varepsilon`` (what most applied work does — Smets-Wouters has seven of each), or to use a particle filter, which represents the whole posterior instead of a point and handles the under-identified case natively. + + ## Particle filters When the model is nonlinear *and* there is measurement error, the filtering distribution is no longer Gaussian and no longer invertible. Particle filters represent it by a cloud of ``N`` weighted draws ("particles") and update the cloud each period. They are the general-purpose fallback: they work for any transition, any number of shocks, and any measurement error. @@ -110,7 +139,7 @@ Because the estimate is random, a repeated evaluation at the same parameters giv Without measurement error the observation equation is a deterministic function of the state. A particle would have to reproduce ``y_t`` *exactly* to get non-zero weight, which happens with probability zero — every weight collapses to zero and the filter dies. Measurement error smears the observation density and gives particles something to score against. -`measurement_error` is the covariance ``H`` of ``\eta_t``, never a standard deviation: a scalar is the common variance of every observable, a vector the per-observable variances, and a matrix the full covariance. `measurement_error = :auto` (the default) resolves to a variance of ``(0.1 s_i)^2`` per observable, where ``s_i`` is that observable's sample standard deviation, for the particle filters — and to *no* measurement error for the Kalman and inversion filters. For serious work set it explicitly or estimate it: the level of the likelihood depends on it, so likelihoods computed under different measurement errors are not comparable. +`measurement_error = :auto` (the default) therefore resolves, for the particle filters, to a variance of ``(0.1 s_i)^2`` per observable, ``s_i`` being that observable's sample standard deviation — and to *no* measurement error for the Kalman and inversion filters. For serious work set it explicitly or estimate it: the level of the likelihood depends on it, so likelihoods computed under different measurement errors are not comparable. See [Measurement error and the initial covariance](@ref) for what ``H`` is and the other jobs it does. ### Bootstrap (`:bootstrap_particle`) @@ -169,6 +198,62 @@ Resampling only happens when the effective sample size ``1/\sum_i W_i^2`` falls **References:** Kitagawa (1996); Douc & Cappé (2005). +## Measurement error and the initial covariance + +Two inputs are not part of the model's economics but change every likelihood, and they are easy to conflate because both are called "uncertainty". They are about different objects and act on different timescales. + +| | `measurement_error` ``H`` | `initial_covariance` ``P_1`` | +|---|---|---| +| uncertainty about | the **observation** ``y_t`` | the latent **state** ``x_1`` | +| acts | every period, forever | once, at ``t = 1`` | +| enters | ``F_t = CP_tC' + H`` | seeds the Riccati recursion | +| over time | **permanent** | **decays**, at the filter's own error-dynamics rate | +| encodes | distrust of the data; misspecification; a singularity fix | ignorance about where the economy started | +| changes the model? | yes — adds a noise term to the observation equation | no — it is a prior on initial conditions | + +### Measurement error + +``H`` is the covariance of ``\eta_t``, **never a standard deviation**: a scalar is the common variance of every observable, a vector the per-observable variances, and a matrix the full covariance. It does three quite different jobs, which are worth keeping separate: + +1. **Genuine measurement error** — hours from the establishment survey is not the model's ``n_t``; GDP gets revised for years. The literal reading, and the least common reason it is used. +2. **Stochastic singularity** — a model with ``n_\varepsilon`` shocks generates observables on an ``n_\varepsilon``-dimensional manifold. Observe more series than that and the model implies exact deterministic relationships among them; ``CP_tC'`` is rank-deficient and the likelihood is not small but *undefined*. ``H > 0`` is the standard fix, and the alternative — adding shocks — is a genuine modelling choice, not a technicality (see below). +3. **Misspecification you would rather quarantine** — giving a series the model cannot match a noise term so it does not dominate the likelihood. + +Mechanically it is the denominator of the Kalman gain ``K_t = P_tC'(CP_tC' + H)^{-1}``: large ``H`` means a small gain and a filter that trusts its own prediction; ``H \to 0`` means a filter that takes the data at face value. So ``H`` is a dial between *trust the model* and *trust the data*, and it is the same dial that decides whether a surprise in the data becomes an inferred structural shock or is written off as noise. + +The distinction between a shock and measurement error is worth stating plainly, because both add a dimension of randomness and both cure a singularity: **a shock is variation the model transmits; measurement error is variation the model refuses to transmit.** A technology shock moves consumption, investment and hours through the model's propagation; a measurement error in output moves output's *observation* and nothing else. + +For the particle filters there is a fourth, purely computational reason: without ``H > 0`` the observation density is a Dirac, no particle ever reproduces ``y_t`` exactly, every weight is zero and the filter dies. That has nothing to do with whether you believe in measurement error — it is why `:auto` picks a *small* data-driven value rather than an economically motivated one. + +### The initial covariance + +``P_1`` is the prior on the state at the start of the sample: where the economy was before the first observation. It seeds + +```math +P_{t+1} = A(P_t - K_tCP_t)A' + BB', +``` + +which contracts to a fixed point that does **not** depend on ``P_1``. So the choice eventually stops mattering — but the *rate* is the filter's own error dynamics, which can be slow. On Smets-Wouters (2007) the ergodic prior and ``P_1 = BB'`` still differ by nearly 500 log points over 184 observations. This is what `presample_periods` is for: discard the periods in which ``P_1`` is still being felt. + +The options are `:theoretical` (the ergodic covariance solving ``\Sigma = A\Sigma A' + BB'`` — right if the sample is a draw from the stationary distribution), `:diagonal` (``10I``, deliberately over-dispersed), or an explicit matrix. + +!!! warning "Timing convention differs between filters" + The particle filters' `initial_covariance` is ``\mathrm{Var}(x_0)`` — the cloud is drawn around the initial state and *then* propagated — whereas the Kalman filter's is ``P_1 = \mathrm{Var}(x_1)``, the first *predicted* state. They correspond as ``P_1 = A\,\mathrm{Var}(x_0)\,A' + BB'``. This is invisible at the `:theoretical` default, because the ergodic covariance is the fixed point of exactly that map and so is carried to itself — which is why passing `:theoretical` to both lines them up. It matters as soon as you supply a matrix: to reproduce a Kalman run with ``P_1 = BB'`` you must pass a **zero** matrix to the particle filter, not ``BB'``. + +### How the two interact + +They are not independent. With ``H > 0`` the gain shrinks, so ``P`` decays to its fixed point more slowly *and* that fixed point is strictly positive even when the state would otherwise be exactly identified: + +| ``H`` | ``\lVert P_\infty^{\text{post}} \rVert`` | +|---|---| +| ``0`` | ``0`` exactly | +| ``10^{-8}`` | ``5.4\times10^{-7}`` | +| ``10^{-6}`` | ``4.6\times10^{-5}`` | +| ``10^{-4}`` | ``7.3\times10^{-4}`` | + +Measurement error means you can never learn the state exactly. That is the real reason the inversion filter does not accept it: the inversion filter's ``P \equiv 0`` and ``H > 0`` are **contradictory assumptions**, not merely a missing feature. + + ## How the filters relate - **Particle → Kalman.** On a *linear* model with Gaussian shocks, the particle filters estimate exactly the quantity the Kalman filter computes in closed form. As ``N \to \infty`` the particle log-likelihood converges to the Kalman log-likelihood (from below, by the Jensen bias above). This is the sharpest correctness check available and is exactly what the package's tests do, on both a small RBC model and Smets-Wouters (2007). @@ -177,3 +262,38 @@ Resampling only happens when the effective sample size ``1/\sum_i W_i^2`` falls - **Inversion → Kalman.** The relationship is exact and worth stating precisely. Writing ``Z = CB``, the inversion filter's per-period score is ``\log N(v_t; 0, ZZ')`` with ``v_t = y_t - CA\hat x_{t-1}`` — in *both* the square case (``Z^{-1}``) and the under-determined case (minimum norm, ``Z^{+} = Z'(ZZ')^{-1}``), since ``\|Z^{+}v\|^2 = v'(ZZ')^{-1}v``. The minimum-norm choice is not an arbitrary tie-break: for Gaussian shocks ``Z^{+}v = E[\varepsilon \mid v]``, the conditional mean. That expression is exactly the Kalman contribution with the posterior state covariance **clamped to zero** (``P_{t|t-1} = BB'``), and correspondingly the gains coincide: the inversion filter's is ``BZ^{+}``, the Kalman's is ``P_tC'F_t^{-1}``, equal iff ``P_{t|t-1} = BB'``. So: *the inversion filter is the Kalman filter that assumes the state is known exactly.* Whether that is legitimate is precisely whether ``P_{t|t} = P - PC'(CPC')^{-1}CP`` really vanishes, which needs ``\mathrm{rank}(CB) = n_\varepsilon`` — **at least as many observables as shocks**. With *more observables than shocks* the system is stochastically singular and only the Kalman filter (with measurement error) is defined. With *more shocks than observables* the clamp is simply false: ``P_{t|t} > 0`` necessarily, so ``F_t^{\text{kal}} = ZZ' + CAP_{t|t}A'C' \supsetneq ZZ' = F^{\text{inv}}`` and the inversion filter understates the innovation covariance — it treats innovations as more surprising than they are, because it pretends to know a state it cannot know. The size of the discrepancy is governed not by the shock/observable counts as such but by how much of the *unidentified* subspace propagates into the next period's observables, ``\|CAP_{t|t}A'C'\|`` relative to ``\|ZZ'\|``; when the unidentified directions barely propagate the two nearly agree anyway. There is a second, related gap: the inversion filter minimises ``\|\varepsilon_t\|`` *greedily*, period by period, ignoring that the null-space component moves ``x_t`` and hence the cost of matching later observations, whereas the Kalman disturbance smoother solves the same minimum-norm problem globally over the whole path. Finally, even when ``n_y = n_\varepsilon`` the agreement is only asymptotic: the state-estimate error obeys ``\delta_t = (I - BZ^{-1}C)A\,\delta_{t-1}``, whose spectral radius is the invertibility (fundamentalness) condition, so a near-unit-root inverse system takes many periods to forget the initial condition. Add measurement error and the inversion filter is not defined at all. At higher order its per-period Newton solve has no Kalman counterpart, which is why it — not the Kalman filter — is the default for nonlinear algorithms. - **Correlated measurement error.** All filters that admit measurement error accept an arbitrary covariance: pass `measurement_error` a matrix instead of a vector of variances. The Kalman filter adds it to ``F_t`` directly; the particle filters factorise ``H`` once per missing-data pattern and score against the resulting triangular solve. The diagonal case is detected and takes a faster elementwise path, so there is no cost to the common case. A third option is to write the correlation into the model itself as measurement-error processes in the observation equations, which moves it into the state transition and makes ``H`` diagonal again — worth doing when the measurement errors are persistent rather than merely contemporaneously correlated. +### When are they the same filter? + +The differences above are not vague family resemblances — on a linear model the three filters coincide exactly, under stated conditions, and the conditions are all about ``H`` and ``P_1``. + +| pair | equivalent when | how exact | +|---|---|---| +| inversion ``\equiv`` Kalman | ``n_y \ge n_\varepsilon``, ``H = 0``, and the Kalman filter is started at ``P_1 = BB'`` | **exact, period by period** | +| inversion ``\approx`` Kalman | same but with the ergodic ``P_1`` | only asymptotically, at the rate of the inverse-system dynamics | +| particle ``\to`` Kalman | linear model, same ``H``, matching initial covariance, ``N \to \infty`` | up to Monte-Carlo error, from below (Jensen) | + +The first row is the sharp statement, and it is the one to remember: **the inversion filter *is* the Kalman filter that assumes the state is known exactly.** It fixes ``x_0`` at the steady state, so the only uncertainty about ``x_1`` is that period's shocks, ``\mathrm{Var}(x_1) = BB'``; given ``n_y \ge n_\varepsilon`` the update then drives the posterior covariance to exactly zero and it stays there. Hand the Kalman filter that same prior and the two agree to machine precision — verified in the test suite on a small RBC and on Smets-Wouters (2007) with seven shocks, seven observables and 184 periods. + +The second row is why the two filters normally *disagree* even on a square system: the default ergodic prior is a genuinely different starting point, and the error decays as ``\delta_t = (I - BZ^{-1}C)A\,\delta_{t-1}``. The spectral radius of that matrix is the **invertibility** (fundamentalness) condition — the "poor man's invertibility condition" of Fernández-Villaverde, Rubio-Ramírez, Sargent & Watson. If it exceeds one the inversion filter's state estimate never converges and its likelihood is wrong at any sample length; if it is close to one (0.98 in the RBC example) convergence is real but slow. + +The third row is the particle filters' correctness check, and is exactly what the package's tests do: on a linear model the particle log-likelihood must approach the Kalman value as ``N`` grows, approaching it *from below* because of the Jensen bias. + +## The filter-free likelihood + +There is a fourth option that is not a `filter` value at all, because it does not filter: instead of integrating the shocks out, it treats them as **parameters** and asks you to supply them. + +```julia +get_loglikelihood(model, data, parameters, shocks, measurement_error_std; + algorithm = :pruned_second_order) +``` + +Given a full path of structural shocks it forward-simulates the model, compares the implied observable path to the data under a Gaussian measurement-error model, and returns the *measurement* part of the joint log-likelihood. The priors on the shocks (typically standard normal) and on the measurement-error scale are yours to declare in the probabilistic-programming model — this function is the building block, not the whole posterior. + +Why bother, when a filter integrates the shocks out for you? Because the resulting object is **smooth and differentiable at every perturbation order**, with no resampling and no per-period nonlinear solve. That makes gradient-based samplers (NUTS/HMC) usable on genuinely nonlinear models, at the cost of a much larger parameter space — ``T \times n_\varepsilon`` extra latent variables. This is the approach of Childers, Fernández-Villaverde, Perla, Rackauckas & Wu (2025). + +Two things to note, both of which follow from there being no filtering distribution to track: + +- **There is no initial covariance.** `initial_state` is a fixed input, not a distribution; the sampler explores the shocks, not a state posterior. +- **Measurement error carries all the noise, and is mandatory.** Given the shocks, the model path is deterministic, so without measurement error the density is degenerate. This is the opposite extreme from the inversion filter, where measurement error must be *zero*. + +One naming wrinkle worth flagging: on this signature the argument is `measurement_error_std` and is a **standard deviation** (a matrix means per-period standard deviations, ``n_{obs} \times T``), whereas the filter-based `measurement_error` is a variance/covariance (a matrix means a full covariance, ``n_{obs} \times n_{obs}``). The names differ deliberately, because a matrix means different things in the two. From 145390147badde137b668e673bc80c57a22781f1 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Mon, 27 Jul 2026 10:00:22 +0200 Subject: [PATCH 21/24] Expose initial_covariance on the estimate functions; test state equivalence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `get_loglikelihood` accepted `initial_covariance` but the estimate entry points did not — they always used the ergodic covariance, with no way to override it. Adds the keyword to `get_shock_decomposition`, `get_estimated_shocks`, `get_estimated_variables`, `get_estimated_variable_standard_deviations` and the two estimate plots, threading it through `filter_data_with_model` into `filter_and_smooth`. It is forwarded only to the Kalman and particle filters; the inversion filter has no state covariance, and says so rather than silently ignoring a supplied matrix. The same pass also fixes two kwarg blocks that had been missed when the tempering controls were threaded through. With that in place the equivalence can be checked where it actually bites — on the paths, not on one scalar. With P₁ = BB' the Kalman gain is BB'C'(CBB'C')⁻¹ = B Z⁺, which *is* the inversion filter's state recursion, so the two must track the same states and shocks. On SW07 they agree to ~1e-10 across all variables and all 184 periods, against 0.99 relative deviation under the ergodic prior. One subtlety, now documented and tested: the states match the Kalman *smoothed* estimates, not the filtered ones. Seven observations do not pin all forty model variables contemporaneously, though the full sample does — checkable directly, since under P₁ = BB' the smoothed dispersion collapses to ~3.6e-5 while the filtered dispersion does not. Exact identification of the state is the inversion filter's assumption, so the smoothed estimates are what it reproduces. The shocks match under both. filters.md gains a "What you get by default" table and the state-equivalence section, plus a note that `initial_covariance` is expressed in different bases on the likelihood and estimate paths. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 50 + ext/StatsPlotsExt.jl | 23 +- src/filter/kalman.jl | 16 +- src/get_functions.jl | 46 +- test/Aguiar_Gopinath_2007.jl | 71 + test/Aguiar_Gopinath_2007.mod | 86 + test/Ascari_Sbordone_2014.jl | 89 + test/Ascari_Sbordone_2014.mod | 102 + test/Backus_Kehoe_Kydland_1992.jl | 165 ++ test/Backus_Kehoe_Kydland_1992.mod | 134 + test/Baxter_King_1993.jl | 89 + test/Baxter_King_1993.mod | 96 + test/Caldara_et_al_2012.jl | 53 + test/Caldara_et_al_2012.mod | 69 + test/FRBUS.jl | 3143 ++++++++++++++++++++++++ test/FRBUS.mod | 2107 ++++++++++++++++ test/GNSS_2010.jl | 297 +++ test/GNSS_2010.mod | 307 +++ test/Gali_2015_chapter_3_nonlinear.jl | 87 + test/Gali_2015_chapter_3_nonlinear.mod | 109 + test/Gali_Monacelli_2005_CITR.jl | 75 + test/Gali_Monacelli_2005_CITR.mod | 86 + test/Ghironi_Melitz_2005.jl | 117 + test/Ghironi_Melitz_2005.mod | 143 ++ test/Ireland_2004.jl | 51 + test/Ireland_2004.mod | 60 + test/JQ_2012_RBC.jl | 71 + test/JQ_2012_RBC.mod | 82 + test/NAWM_EAUS_2008.jl | 771 ++++++ test/NAWM_EAUS_2008.mod | 865 +++++++ test/QUEST3_2009.jl | 499 ++++ test/QUEST3_2009.mod | 494 ++++ test/SGU_2003_debt_premium.jl | 57 + test/SGU_2003_debt_premium.mod | 72 + test/test_particle_filter_sw07.jl | 41 + 35 files changed, 10619 insertions(+), 4 deletions(-) create mode 100644 test/Aguiar_Gopinath_2007.jl create mode 100644 test/Aguiar_Gopinath_2007.mod create mode 100644 test/Ascari_Sbordone_2014.jl create mode 100644 test/Ascari_Sbordone_2014.mod create mode 100644 test/Backus_Kehoe_Kydland_1992.jl create mode 100644 test/Backus_Kehoe_Kydland_1992.mod create mode 100644 test/Baxter_King_1993.jl create mode 100644 test/Baxter_King_1993.mod create mode 100644 test/Caldara_et_al_2012.jl create mode 100644 test/Caldara_et_al_2012.mod create mode 100644 test/FRBUS.jl create mode 100644 test/FRBUS.mod create mode 100644 test/GNSS_2010.jl create mode 100644 test/GNSS_2010.mod create mode 100644 test/Gali_2015_chapter_3_nonlinear.jl create mode 100644 test/Gali_2015_chapter_3_nonlinear.mod create mode 100644 test/Gali_Monacelli_2005_CITR.jl create mode 100644 test/Gali_Monacelli_2005_CITR.mod create mode 100644 test/Ghironi_Melitz_2005.jl create mode 100644 test/Ghironi_Melitz_2005.mod create mode 100644 test/Ireland_2004.jl create mode 100644 test/Ireland_2004.mod create mode 100644 test/JQ_2012_RBC.jl create mode 100644 test/JQ_2012_RBC.mod create mode 100644 test/NAWM_EAUS_2008.jl create mode 100644 test/NAWM_EAUS_2008.mod create mode 100644 test/QUEST3_2009.jl create mode 100644 test/QUEST3_2009.mod create mode 100644 test/SGU_2003_debt_premium.jl create mode 100644 test/SGU_2003_debt_premium.mod diff --git a/docs/src/filters.md b/docs/src/filters.md index 4080c3daa..ee01b3f2a 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -43,6 +43,30 @@ A short decision rule: By default the package picks `:kalman` for `:first_order` and `:inversion` for the nonlinear algorithms. +## What you get by default + +Every knob discussed on this page has a default, and the defaults are not neutral — they are what determines the number you get from `get_loglikelihood(model, data, parameters)` with no keywords. + +| setting | default | consequence | +|---|---|---| +| `filter` | `:kalman` at `:first_order`, `:inversion` at every higher order | nonlinear models are filtered *exactly given the shocks*, with no measurement error | +| `measurement_error` | `:auto` | **none** for Kalman and inversion; ``(0.1 s_i)^2`` per observable for the particle filters | +| `initial_covariance` | `:theoretical` | the ergodic covariance — *not* the inversion filter's implicit ``BB'``, which is why Kalman and inversion likelihoods differ by default | +| `smooth` | `true` for the Kalman filter, `false` otherwise | the particle filters **do** support smoothing but do not use it unless asked | +| `presample_periods` | `0` | the initial-condition transient is included in the likelihood | +| `warmup_iterations` | `0` | — | +| `on_failure_loglikelihood` | `-Inf`; `-1e6` for the particle filters | a stochastic failure rejects one proposal instead of killing a sampler's chain | +| `n_particles` | `10_000` | | +| `particle_resampling` | `:systematic`, threshold `0.5` | resample only when the effective sample size halves | +| `particle_initial_state_scaling` | `1.0` | the initial cloud has exactly the ergodic spread | +| tempering | ratio `2.0`, 1 MH step, ≤100 stages, scale `0.3` | only used by `:tempered_particle` | + +Three consequences are worth internalising, because they surprise people: + +1. **Kalman and inversion likelihoods are not comparable out of the box**, even on a first-order model with as many shocks as observables. They differ by the initial covariance alone (see below), and on Smets-Wouters that is worth hundreds of log points. Match `initial_covariance` before comparing. +2. **Likelihoods computed under different measurement error are not comparable at all** — ``H`` shifts the level of every period. Since `:auto` is data-driven, that includes two particle-filter runs on different samples. +3. **Switching perturbation order silently switches filter**, from Kalman to inversion, and with it the assumption about measurement error and the initial state. If you want a like-for-like comparison across orders, set `filter` explicitly. + ## The Kalman filter For a linear model with Gaussian shocks the filtering distribution stays Gaussian forever, so tracking it only requires tracking a mean and a covariance. The recursion alternates prediction and update: @@ -276,6 +300,32 @@ The first row is the sharp statement, and it is the one to remember: **the inver The second row is why the two filters normally *disagree* even on a square system: the default ergodic prior is a genuinely different starting point, and the error decays as ``\delta_t = (I - BZ^{-1}C)A\,\delta_{t-1}``. The spectral radius of that matrix is the **invertibility** (fundamentalness) condition — the "poor man's invertibility condition" of Fernández-Villaverde, Rubio-Ramírez, Sargent & Watson. If it exceeds one the inversion filter's state estimate never converges and its likelihood is wrong at any sample length; if it is close to one (0.98 in the RBC example) convergence is real but slow. +### Does the equivalence carry to the states? + +Yes, and it is a sharper check than the likelihood: a likelihood is one scalar, whereas the states and shocks pin the whole path. + +The reason is immediate once the gain is written out. With ``P_{t|t-1} = BB'`` the Kalman gain is + +```math +K_t = P_{t|t-1}C'F_t^{-1} = BB'C'(CBB'C')^{-1} = BZ'(ZZ')^{-1} = BZ^{+}, +``` + +so ``\hat x_{t|t} = A\hat x_{t-1|t-1} + BZ^{+}v_t`` — literally the inversion filter's recursion. The estimated **shocks** are then the same object as well, since the inversion filter's ``\hat\varepsilon_t = Z^{+}v_t`` is exactly the Kalman disturbance estimate. + +Measured on Smets-Wouters (2007), relative maximum deviation across all variables and all 184 periods: + +| comparison | deviation | +|---|---| +| inversion states vs Kalman **smoothed** states (``P_1 = BB'``) | ``7\times10^{-11}`` | +| inversion shocks vs Kalman smoothed shocks (``P_1 = BB'``) | ``9\times10^{-11}`` | +| inversion shocks vs Kalman **filtered** shocks (``P_1 = BB'``) | ``7\times10^{-11}`` | +| inversion states vs Kalman smoothed states (**ergodic** ``P_1``) | ``0.99`` | + +One wrinkle worth knowing. The states match the **smoothed** Kalman estimates, not the filtered ones. That is not a contradiction of "the inversion filter's filtered and smoothed estimates coincide" — it reflects what is being conditioned on. The estimates are reported for all model variables, and a single period's seven observations do not pin all forty of them contemporaneously; the *full sample* does, through the model's own restrictions. Directly checkable: under ``P_1 = BB'`` the Kalman **smoothed** dispersion collapses to ``\approx 0`` (max standard deviation ``3.6\times10^{-5}``) while the filtered dispersion does not. Since exact identification of the state is precisely the inversion filter's assumption, the smoothed estimates are the ones it reproduces. The *shocks* match under both, because a period's shocks are pinned by that period's observations alone. + +!!! note "`initial_covariance` is expressed in different bases" + On `get_loglikelihood` the matrix is over `union(past states, observables)`; on the estimate functions (`get_estimated_variables` and friends) it is over *all* model variables. Build ``BB'`` from the same rows you intend to filter over — this is the one place where passing the matrix from the wrong path silently fails with a dimension mismatch rather than a wrong answer. + The third row is the particle filters' correctness check, and is exactly what the package's tests do: on a linear model the particle log-likelihood must approach the Kalman value as ``N`` grows, approaching it *from below* because of the Jensen bias. ## The filter-free likelihood diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index 8b5aa69d1..282099a54 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -708,6 +708,7 @@ function plot_model_estimates(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR, n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, @@ -855,7 +856,16 @@ function plot_model_estimates(𝓂::ℳ, if filter ∈ MacroModelling.PARTICLE_FILTERS extra_kw = merge(extra_kw, (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, - particle_rng)) + particle_rng, tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale)) + end + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = MacroModelling.DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none, so only forward it where it means something. + if filter == :kalman || filter ∈ MacroModelling.PARTICLE_FILTERS + extra_kw = merge(extra_kw, (; initial_covariance)) end variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; extra_kw...) @@ -1379,6 +1389,7 @@ function plot_model_estimates!(𝓂::ℳ, steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = MacroModelling.DEFAULT_MEASUREMENT_ERROR, n_particles::Int = MacroModelling.DEFAULT_N_PARTICLES, particle_resampling::Symbol = MacroModelling.DEFAULT_PARTICLE_RESAMPLING, @@ -1517,6 +1528,16 @@ function plot_model_estimates!(𝓂::ℳ, tempering_target_ratio, tempering_mh_steps, tempering_max_stages, tempering_mh_scale) : NamedTuple() + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = MacroModelling.DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none (it fixes x₀ and clamps the covariance), so the + # argument is only forwarded where it means something. + if filter == :kalman || filter ∈ MacroModelling.PARTICLE_FILTERS + particle_kw = merge(particle_kw, (; initial_covariance)) + end + variables_to_plot, shocks_to_plot, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, smooth = smooth, opts = opts; particle_kw...) if pruning diff --git a/src/filter/kalman.jl b/src/filter/kalman.jl index 885f7afc5..b5073b619 100644 --- a/src/filter/kalman.jl +++ b/src/filter/kalman.jl @@ -464,13 +464,14 @@ end ::Val{:kalman}; # filter, warmup_iterations::Int = 0, opts::CalculationOptions = merge_calculation_options(), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, smooth::Bool = true) obs_axis = collect(axiskeys(data_in_deviations,1)) obs_symbols = obs_axis isa String_input ? obs_axis .|> Meta.parse .|> replace_indices : obs_axis - filtered_and_smoothed = filter_and_smooth(𝓂, data_in_deviations, obs_symbols; opts = opts) + filtered_and_smoothed = filter_and_smooth(𝓂, data_in_deviations, obs_symbols; opts = opts, initial_covariance = initial_covariance) variables = filtered_and_smoothed[smooth ? 1 : 5] standard_deviations = filtered_and_smoothed[smooth ? 2 : 6] @@ -485,6 +486,7 @@ end function filter_and_smooth(𝓂::ℳ, data_in_deviations::AbstractArray, observables::Vector{Symbol}; + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, opts::CalculationOptions = merge_calculation_options()) # Based on Durbin and Koopman (2012) # https://jrnold.github.io/ssmodels-in-stan/filtering-and-smoothing.html#smoothing @@ -526,7 +528,17 @@ function filter_and_smooth(𝓂::ℳ, 𝐁 = B * B' - P̄ = calculate_covariance(𝓂.parameter_values, 𝓂, opts = opts)[1] + # Prior on the state at the start of the sample. `:theoretical` is the ergodic + # covariance (the historical behaviour and the default); `:diagonal` starts + # diffuse; a matrix is used as given. Supplying B B' reproduces the inversion + # filter's implicit prior — see the Filters page. + P̄ = if initial_covariance isa AbstractMatrix + Matrix{Float64}(initial_covariance) + elseif initial_covariance == :diagonal + Matrix{Float64}(10.0 * ℒ.I(size(A, 1))) + else + calculate_covariance(𝓂.parameter_values, 𝓂, opts = opts)[1] + end n_obs = size(data_in_deviations,2) diff --git a/src/get_functions.jl b/src/get_functions.jl index aac92f0dc..6d4d3ee4e 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -298,6 +298,7 @@ And data, 4×2×40 Array{Float64, 3}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -354,7 +355,16 @@ And data, 4×2×40 Array{Float64, 3}: if filter ∈ PARTICLE_FILTERS extra_kw = merge(extra_kw, (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, - particle_rng)) + particle_rng, tempering_target_ratio, tempering_mh_steps, + tempering_max_stages, tempering_mh_scale)) + end + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none, so only forward it where it means something. + if filter == :kalman || filter ∈ PARTICLE_FILTERS + extra_kw = merge(extra_kw, (; initial_covariance)) end ensure_name_display_constants!(𝓂) axis1 = 𝓂.constants.post_complete_parameters.var_axis @@ -460,6 +470,7 @@ And data, 1×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -521,6 +532,16 @@ And data, 1×40 Matrix{Float64}: tempering_target_ratio, tempering_mh_steps, tempering_max_stages, tempering_mh_scale) : NamedTuple() + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none (it fixes x₀ and clamps the covariance), so the + # argument is only forwarded where it means something. + if filter == :kalman || filter ∈ PARTICLE_FILTERS + particle_kw = merge(particle_kw, (; initial_covariance)) + end + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, opts = opts, @@ -606,6 +627,7 @@ And data, 4×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -668,6 +690,16 @@ And data, 4×40 Matrix{Float64}: tempering_target_ratio, tempering_mh_steps, tempering_max_stages, tempering_mh_scale) : NamedTuple() + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none (it fixes x₀ and clamps the covariance), so the + # argument is only forwarded where it means something. + if filter == :kalman || filter ∈ PARTICLE_FILTERS + particle_kw = merge(particle_kw, (; initial_covariance)) + end + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), warmup_iterations = warmup_iterations, opts = opts, @@ -756,6 +788,7 @@ And data, 5×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -891,6 +924,7 @@ And data, 4×40 Matrix{Float64}: steady_state_function::SteadyStateFunctionType = missing, algorithm::Symbol = DEFAULT_ALGORITHM, filter::Symbol = DEFAULT_FILTER_SELECTOR(algorithm), + initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, measurement_error::Union{Symbol,Real,AbstractVector{<:Real},AbstractMatrix{<:Real}} = DEFAULT_MEASUREMENT_ERROR, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -958,6 +992,16 @@ And data, 4×40 Matrix{Float64}: tempering_target_ratio, tempering_mh_steps, tempering_max_stages, tempering_mh_scale) : NamedTuple() + if filter == :inversion && initial_covariance !== :theoretical + @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG + end + # The Kalman and particle filters take a prior on the initial state; the + # inversion filter has none (it fixes x₀ and clamps the covariance), so the + # argument is only forwarded where it means something. + if filter == :kalman || filter ∈ PARTICLE_FILTERS + particle_kw = merge(particle_kw, (; initial_covariance)) + end + variables, shocks, standard_deviations, decomposition = filter_data_with_model(𝓂, data_in_deviations, Val(algorithm), Val(filter), smooth = smooth, opts = opts; particle_kw...) diff --git a/test/Aguiar_Gopinath_2007.jl b/test/Aguiar_Gopinath_2007.jl new file mode 100644 index 000000000..3730094e9 --- /dev/null +++ b/test/Aguiar_Gopinath_2007.jl @@ -0,0 +1,71 @@ +using MacroModelling + +@model Aguiar_Gopinath_2007 begin + y[0] = (exp(g[0]) * l[0]) ^ alpha * exp(z[0]) * k[-1] ^ (1 - alpha) + + z[0] = rho_z * z[-1] + sigma__z * eps_z[x] + + g[0] = (1 - rho_g) * mu_g + rho_g * g[-1] + sigma__g * eps_g[x] + + u[0] = (c[0] ^ gamma * (1 - l[0]) ^ (1 - gamma)) ^ (1 - sigma) / (1 - sigma) + + uc[0] = gamma * u[0] * (1 - sigma) / c[0] + + ul[0] = u[0] * (1 - sigma) * ( - (1 - gamma)) / (1 - l[0]) + + c[0] + exp(g[0]) * k[0] = y[0] + k[-1] * (1 - delta) - k[-1] * phi / 2 * (exp(g[0]) * k[0] / k[-1] - exp(mu_g)) ^ 2 - b[-1] + exp(g[0]) * b[0] * q[0] + + 1 / q[0] = 1 + r_star + psi * (exp(b[0] - b_star) - 1) + + exp(g[0]) * uc[0] * (1 + phi * (exp(g[0]) * k[0] / k[-1] - exp(mu_g))) = beta * exp((1 - sigma) * g[0] * gamma) * uc[1] * (1 - delta + (1 - alpha) * y[1] / k[0] - phi / 2 * (k[1] * exp(g[1]) * ( - (2 * (k[1] * exp(g[1]) / k[0] - exp(mu_g)))) / k[0] + (k[1] * exp(g[1]) / k[0] - exp(mu_g)) ^ 2)) + + ul[0] + uc[0] * y[0] * alpha / l[0] = 0 + + uc[0] * exp(g[0]) * q[0] = beta * exp((1 - sigma) * g[0] * gamma) * uc[1] + + invest[0] = exp(g[0]) * k[0] + k[-1] * phi / 2 * (exp(g[0]) * k[0] / k[-1] - exp(mu_g)) ^ 2 - k[-1] * (1 - delta) + + c_y[0] = c[0] / y[0] + + i_y[0] = invest[0] / y[0] + + nx[0] = (b[-1] - exp(g[0]) * b[0] * q[0]) / y[0] + + delta_y[0] = g[-1] + log(y[0]) - log(y[-1]) + +end + + +@parameters Aguiar_Gopinath_2007 begin + gamma = 0.36 + + b_share = 0.1 + + psi = 0.001 + + alpha = 0.68 + + sigma = 2.0 + + delta = 0.05 + + phi = 4.0 + + rho_z = 0.95 + + rho_g = 0.01 + + sigma__z = 0.01 + + sigma__g = 0.0005 + + beta = 0.9803921568627451 + + mu_g = 0.006578315360122507 + + b_star = 0.0645176839232937 + + r_star = 0.029166381484582272 + +end + diff --git a/test/Aguiar_Gopinath_2007.mod b/test/Aguiar_Gopinath_2007.mod new file mode 100644 index 000000000..deab2ab64 --- /dev/null +++ b/test/Aguiar_Gopinath_2007.mod @@ -0,0 +1,86 @@ +var +b c c_y delta_y g i_y invest k l nx q u uc ul y z ; + +varexo +eps_g eps_z ; + +parameters +alpha b_star beta delta gamma mu_g phi psi r_star rho_g rho_z sigma sigma__g sigma__z ; + +% Parameter definitions: + gamma = 0.36; + b_share = 0.1; + psi = 0.001; + alpha = 0.68; + sigma = 2.0; + delta = 0.05; + phi = 4.0; + rho_z = 0.95; + rho_g = 0.01; + sigma__z = 0.01; + sigma__g = 0.0005; + beta = 0.9803921568627451; + mu_g = 0.006578315360122507; + b_star = 0.0645176839232937; + r_star = 0.029166381484582272; + +model; + y(0) = (exp(g(0)) * l(0)) ^ alpha * exp(z(0)) * k(-1) ^ (1 - alpha); + + z(0) = rho_z * z(-1) + sigma__z * eps_z; + + g(0) = (1 - rho_g) * mu_g + rho_g * g(-1) + sigma__g * eps_g; + + u(0) = (c(0) ^ gamma * (1 - l(0)) ^ (1 - gamma)) ^ (1 - sigma) / (1 - sigma); + + uc(0) = ((1 - sigma) * u(0) * gamma) / c(0); + + ul(0) = ((1 - sigma) * u(0) * -((1 - gamma))) / (1 - l(0)); + + c(0) + k(0) * exp(g(0)) = (((y(0) + (1 - delta) * k(-1)) - ((k(-1) * phi) / 2) * ((k(0) * exp(g(0))) / k(-1) - exp(mu_g)) ^ 2) - b(-1)) + b(0) * exp(g(0)) * q(0); + + 1 / q(0) = 1 + r_star + psi * (exp(b(0) - b_star) - 1); + + exp(g(0)) * uc(0) * (1 + phi * ((k(0) * exp(g(0))) / k(-1) - exp(mu_g))) = beta * exp(g(0) * gamma * (1 - sigma)) * uc(1) * (((1 - delta) + ((1 - alpha) * y(1)) / k(0)) - (phi / 2) * ((k(1) * exp(g(1)) * -(2 * ((k(1) * exp(g(1))) / k(0) - exp(mu_g)))) / k(0) + ((k(1) * exp(g(1))) / k(0) - exp(mu_g)) ^ 2)); + + ul(0) + (y(0) * alpha * uc(0)) / l(0) = 0; + + q(0) * exp(g(0)) * uc(0) = beta * exp(g(0) * gamma * (1 - sigma)) * uc(1); + + invest(0) = (((k(-1) * phi) / 2) * ((k(0) * exp(g(0))) / k(-1) - exp(mu_g)) ^ 2 + k(0) * exp(g(0))) - (1 - delta) * k(-1); + + c_y(0) = c(0) / y(0); + + i_y(0) = invest(0) / y(0); + + nx(0) = (b(-1) - b(0) * exp(g(0)) * q(0)) / y(0); + + delta_y(0) = (g(-1) + log(y(0))) - log(y(-1)); + +end; + +shocks; +var eps_g = 1; +var eps_z = 1; +end; + +initval; + b = 0.06451768392329392; + c = 0.4961560429642269; + c_y = 0.7690233325085202; + delta_y = 0.006578315360122507; + g = 0.006578315360122507; + i_y = 0.22878398204327882; + invest = 0.14760612640180762; + k = 2.607882091904729; + l = 0.33216869272353183; + nx = 0.0021926854482003343; + q = 0.9716601882753793; + u = -1.6664438374559876; + uc = 1.2091352911878372; + ul = -1.596996194025857; + y = 0.6451768392329369; + z = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Ascari_Sbordone_2014.jl b/test/Ascari_Sbordone_2014.jl new file mode 100644 index 000000000..ed87b2a21 --- /dev/null +++ b/test/Ascari_Sbordone_2014.jl @@ -0,0 +1,89 @@ +using MacroModelling + +@model Ascari_Sbordone_2014 begin + 1 / y[0] ^ sigma = beta * (1 + i[0]) / (pi[1] * y[1] ^ sigma) + + w[0] = y[0] ^ sigma * d_n * exp(zeta[0]) * N[0] ^ phi_par + + p_star[0] = ((1 - theta * pi[-1] ^ ((1 - epsilon) * var_rho) * pi[0] ^ (epsilon - 1)) / (1 - theta)) ^ (1 / (1 - epsilon)) + + p_star[0] ^ (1 + epsilon * alpha / (1 - alpha)) = epsilon / ((epsilon - 1) * (1 - alpha)) * psi[0] / phi[0] + + psi[0] = w[0] * exp(A[0]) ^ (( - 1) / (1 - alpha)) * y[0] ^ (1 / (1 - alpha) - sigma) + beta * theta * pi[0] ^ (epsilon * ( - var_rho) / (1 - alpha)) * pi[1] ^ (epsilon / (1 - alpha)) * psi[1] + + phi[0] = y[0] ^ (1 - sigma) + beta * theta * pi[0] ^ ((1 - epsilon) * var_rho) * pi[1] ^ (epsilon - 1) * phi[1] + + N[0] = s[0] * (y[0] / exp(A[0])) ^ (1 / (1 - alpha)) + + s[0] = (1 - theta) * p_star[0] ^ (( - epsilon) / (1 - alpha)) + theta * pi[-1] ^ (var_rho * ( - epsilon) / (1 - alpha)) * pi[0] ^ (epsilon / (1 - alpha)) * s[-1] + + (1 + i[0]) / (1 + i_bar) = ((1 + i[-1]) / (1 + i_bar)) ^ rho_i * ((pi[0] / Pi_bar) ^ phi_pi * (y[0] / Y_bar) ^ phi_y) ^ (1 - rho_i) * exp(v[0]) + + MC_real[0] = w[0] / (1 - alpha) * exp(A[0]) ^ (1 / (alpha - 1)) * y[0] ^ (alpha / (1 - alpha)) + + real_interest[0] = (1 + i[0]) / pi[1] + + Utility[0] = log(y[0]) - d_n * exp(zeta[0]) * N[0] ^ (1 + phi_par) / (1 + phi_par) + beta * Utility[1] + + v[0] = rho_v * v[-1] + sigma__v * e_v[x] + + A[0] = rho_a * A[-1] + sigma__a * e_a[x] + + zeta[0] = rho_zeta * zeta[-1] + sigma__zeta * e_zeta[x] + + A_tilde[0] = exp(A[0]) / s[0] + + Average_markup[0] = 1 / MC_real[0] + + Marginal_markup[0] = p_star[0] / MC_real[0] + + price_adjustment_gap[0] = 1 / p_star[0] + +end + + +@parameters Ascari_Sbordone_2014 begin + beta = 0.99 + + trend_inflation = 0.0 + + alpha = 0.0 + + theta = 0.75 + + epsilon = 10.0 + + sigma = 1.0 + + rho_v = 0.0 + + rho_a = 0.0 + + rho_zeta = 0.0 + + phi_par = 1.0 + + phi_pi = 2.0 + + phi_y = 0.125 + + rho_i = 0.8 + + var_rho = 0.0 + + sigma__zeta = 0.01 + + sigma__a = 0.01 + + sigma__v = 0.01 + + d_n = 8.09999999470471 + + Y_bar = 0.33333331837004737 + + Pi_bar = (1+trend_inflation/100)^0.25 + + i_bar = Pi_bar/beta-1 + +end + diff --git a/test/Ascari_Sbordone_2014.mod b/test/Ascari_Sbordone_2014.mod new file mode 100644 index 000000000..2039e932d --- /dev/null +++ b/test/Ascari_Sbordone_2014.mod @@ -0,0 +1,102 @@ +var +A A_tilde Average_markup MC_real Marginal_markup N Utility i p_star phi pi price_adjustment_gap psi real_interest s v w y zeta ; + +varexo +e_a e_v e_zeta ; + +parameters +Pi_bar Y_bar alpha beta d_n epsilon i_bar phi_par phi_pi phi_y rho_a rho_i rho_v rho_zeta sigma theta var_rho sigma__zeta sigma__v sigma__a ; + +% Parameter definitions: + beta = 0.99; + trend_inflation = 0.0; + alpha = 0.0; + theta = 0.75; + epsilon = 10.0; + sigma = 1.0; + rho_v = 0.0; + rho_a = 0.0; + rho_zeta = 0.0; + phi_par = 1.0; + phi_pi = 2.0; + phi_y = 0.125; + rho_i = 0.8; + var_rho = 0.0; + sigma__zeta = 0.01; + sigma__a = 0.01; + sigma__v = 0.01; + d_n = 8.09999999470471; + Y_bar = 0.33333331837004737; + Pi_bar = (1 + trend_inflation / 100) ^ (1 / 4); + i_bar = Pi_bar / beta - 1; + +model; + 1 / y(0) ^ sigma = (beta * (1 + i(0))) / (pi(1) * y(1) ^ sigma); + + w(0) = y(0) ^ sigma * d_n * exp(zeta(0)) * N(0) ^ phi_par; + + p_star(0) = ((1 - theta * pi(-1) ^ ((1 - epsilon) * var_rho) * pi(0) ^ (epsilon - 1)) / (1 - theta)) ^ (1 / (1 - epsilon)); + + p_star(0) ^ (1 + (epsilon * alpha) / (1 - alpha)) = ((epsilon / ((epsilon - 1) * (1 - alpha))) * psi(0)) / phi(0); + + psi(0) = w(0) * exp(A(0)) ^ (-1 / (1 - alpha)) * y(0) ^ (1 / (1 - alpha) - sigma) + beta * theta * pi(0) ^ ((epsilon * -var_rho) / (1 - alpha)) * pi(1) ^ (epsilon / (1 - alpha)) * psi(1); + + phi(0) = y(0) ^ (1 - sigma) + beta * theta * pi(0) ^ ((1 - epsilon) * var_rho) * pi(1) ^ (epsilon - 1) * phi(1); + + N(0) = s(0) * (y(0) / exp(A(0))) ^ (1 / (1 - alpha)); + + s(0) = (1 - theta) * p_star(0) ^ (-epsilon / (1 - alpha)) + theta * pi(-1) ^ ((var_rho * -epsilon) / (1 - alpha)) * pi(0) ^ (epsilon / (1 - alpha)) * s(-1); + + (1 + i(0)) / (1 + i_bar) = ((1 + i(-1)) / (1 + i_bar)) ^ rho_i * ((pi(0) / Pi_bar) ^ phi_pi * (y(0) / Y_bar) ^ phi_y) ^ (1 - rho_i) * exp(v(0)); + + MC_real(0) = ((w(0) * 1) / (1 - alpha)) * exp(A(0)) ^ (1 / (alpha - 1)) * y(0) ^ (alpha / (1 - alpha)); + + real_interest(0) = (1 + i(0)) / pi(1); + + Utility(0) = (log(y(0)) - (d_n * exp(zeta(0)) * N(0) ^ (1 + phi_par)) / (1 + phi_par)) + beta * Utility(1); + + v(0) = rho_v * v(-1) + sigma__v * e_v; + + A(0) = rho_a * A(-1) + sigma__a * e_a; + + zeta(0) = rho_zeta * zeta(-1) + sigma__zeta * e_zeta; + + A_tilde(0) = exp(A(0)) / s(0); + + Average_markup(0) = 1 / MC_real(0); + + Marginal_markup(0) = p_star(0) / MC_real(0); + + price_adjustment_gap(0) = 1 / p_star(0); + +end; + +shocks; +var e_a = 1; +var e_v = 1; +var e_zeta = 1; +end; + +initval; + A = 0.0; + A_tilde = 1.000000000000001; + Average_markup = 1.1111111118374883; + MC_real = 0.8999999994116344; + Marginal_markup = 1.111111093133382; + N = 0.3333333333333333; + Utility = -154.86122883739253; + i = 0.010101004433099076; + p_star = 0.9999999831663043; + phi = 3.883494580117997; + pi = 0.9999999943887681; + price_adjustment_gap = 1.000000016833696; + psi = 3.4951450632699883; + real_interest = 1.0101010101010102; + s = 0.9999999999999989; + v = 0.0; + w = 0.8999999994116344; + y = 0.3333333333333333; + zeta = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Backus_Kehoe_Kydland_1992.jl b/test/Backus_Kehoe_Kydland_1992.jl new file mode 100644 index 000000000..5fbf8ddb3 --- /dev/null +++ b/test/Backus_Kehoe_Kydland_1992.jl @@ -0,0 +1,165 @@ +using MacroModelling + +@model Backus_Kehoe_Kydland_1992 begin + Y__H__[0] = (sigma__H__ * Z__H__[-1] ^ (-nu__H__) + (N__H__[0] ^ (1 - theta__H__) * LAMBDA__H__[0] * AUX_ENDO_LAG_5_3[-1] ^ theta__H__) ^ (-nu__H__)) ^ (( - 1) / nu__H__) + + K__H__[0] = (1 - delta__H__) * K__H__[-1] + S__H__[0] + + X__H__[0] = S__H__[0] * phi__H__ + phi__H__ * S__H__[-1] + phi__H__ * AUX_ENDO_LAG_16_2[-1] + phi__H__ * AUX_ENDO_LAG_16_1[-1] + + A__H__[0] = N__H__[0] + (1 - eta__H__) * A__H__[-1] + + L__H__[0] = 1 - N__H__[0] * alpha__H__ - A__H__[-1] * eta__H__ * (1 - alpha__H__) + + U__H__[0] = (C__H__[0] ^ mu__H__ * L__H__[0] ^ (1 - mu__H__)) ^ gamma__H__ + + U__H__[0] * mu__H__ * psi__H__ / C__H__[0] = LGM[0] + + U__H__[0] * (1 - mu__H__) * psi__H__ / L__H__[0] * ( - alpha__H__) = Y__H__[0] ^ (1 + nu__H__) * (1 - theta__H__) * ( - LGM[0]) / N__H__[0] * (N__H__[0] ^ (1 - theta__H__) * LAMBDA__H__[0] * AUX_ENDO_LAG_5_3[-1] ^ theta__H__) ^ (-nu__H__) + + phi__H__ * LGM[0] + phi__H__ * beta__H__ * LGM[1] + phi__H__ * beta__H__ ^ 2 * AUX_ENDO_LEAD_107[1] + phi__H__ * beta__H__ ^ 3 * AUX_ENDO_LEAD_112[1] + (1 - delta__H__) * phi__H__ * LGM[1] * ( - beta__H__) + (1 - delta__H__) * phi__H__ * ( - (beta__H__ ^ 2)) * AUX_ENDO_LEAD_107[1] + (1 - delta__H__) * phi__H__ * ( - (beta__H__ ^ 3)) * AUX_ENDO_LEAD_112[1] + (1 - delta__H__) * phi__H__ * ( - (beta__H__ ^ 4)) * AUX_ENDO_LEAD_132[1] = AUX_ENDO_LEAD_151[1] + + LGM[0] = beta__H__ * LGM[1] * (1 + sigma__H__ * Z__H__[0] ^ (( - nu__H__) - 1) * Y__H__[1] ^ (1 + nu__H__)) + + NX__H__[0] = (Y__H__[0] - (Z__H__[0] + X__H__[0] + C__H__[0] - Z__H__[-1])) / Y__H__[0] + + Y__F__[0] = (sigma__F__ * Z__F__[-1] ^ (-nu__F__) + (N__F__[0] ^ (1 - theta__F__) * LAMBDA__F__[0] * AUX_ENDO_LAG_4_3[-1] ^ theta__F__) ^ (-nu__F__)) ^ (( - 1) / nu__F__) + + K__F__[0] = (1 - delta__F__) * K__F__[-1] + S__F__[0] + + X__F__[0] = S__F__[0] * phi__F__ + phi__F__ * S__F__[-1] + phi__F__ * AUX_ENDO_LAG_15_2[-1] + phi__F__ * AUX_ENDO_LAG_15_1[-1] + + A__F__[0] = N__F__[0] + (1 - eta__F__) * A__F__[-1] + + L__F__[0] = 1 - N__F__[0] * alpha__F__ - A__F__[-1] * eta__F__ * (1 - alpha__F__) + + U__F__[0] = (C__F__[0] ^ mu__F__ * L__F__[0] ^ (1 - mu__F__)) ^ gamma__F__ + + U__F__[0] * mu__F__ * psi__F__ / C__F__[0] = LGM[0] + + U__F__[0] * (1 - mu__F__) * psi__F__ / L__F__[0] * ( - alpha__F__) = Y__F__[0] ^ (1 + nu__F__) * ( - LGM[0]) * (1 - theta__F__) / N__F__[0] * (N__F__[0] ^ (1 - theta__F__) * LAMBDA__F__[0] * AUX_ENDO_LAG_4_3[-1] ^ theta__F__) ^ (-nu__F__) + + LGM[0] * phi__F__ + phi__F__ * LGM[1] * beta__F__ + phi__F__ * beta__F__ ^ 2 * AUX_ENDO_LEAD_107[1] + phi__F__ * beta__F__ ^ 3 * AUX_ENDO_LEAD_112[1] + (1 - delta__F__) * phi__F__ * LGM[1] * ( - beta__F__) + (1 - delta__F__) * phi__F__ * ( - (beta__F__ ^ 2)) * AUX_ENDO_LEAD_107[1] + (1 - delta__F__) * phi__F__ * ( - (beta__F__ ^ 3)) * AUX_ENDO_LEAD_112[1] + (1 - delta__F__) * phi__F__ * ( - (beta__F__ ^ 4)) * AUX_ENDO_LEAD_132[1] = AUX_ENDO_LEAD_302[1] + + LGM[0] = LGM[1] * beta__F__ * (1 + sigma__F__ * Z__F__[0] ^ (( - nu__F__) - 1) * Y__F__[1] ^ (1 + nu__F__)) + + NX__F__[0] = (Y__F__[0] - (Z__F__[0] + X__F__[0] + C__F__[0] - Z__F__[-1])) / Y__F__[0] + + LAMBDA__H__[0] - 1 = rho__H____H__ * (LAMBDA__H__[-1] - 1) + rho__H____F__ * (LAMBDA__F__[-1] - 1) + Z_E__H__ * E__H__[x] + + LAMBDA__F__[0] - 1 = (LAMBDA__F__[-1] - 1) * rho__F____F__ + (LAMBDA__H__[-1] - 1) * rho__F____H__ + Z_E__F__ * E__F__[x] + + Z__H__[0] + X__H__[0] + C__H__[0] - Z__H__[-1] + Z__F__[0] + X__F__[0] + C__F__[0] - Z__F__[-1] = Y__H__[0] + Y__F__[0] + + dLGM[0] = LGM[1] / LGM[0] + + dLGM_ann[0] = dLGM[0] * dLGM[-1] * AUX_ENDO_LAG_25_1[-1] * AUX_ENDO_LAG_25_2[-1] + + AUX_ENDO_LEAD_107[0] = LGM[1] + + AUX_ENDO_LEAD_112[0] = AUX_ENDO_LEAD_107[1] + + AUX_ENDO_LEAD_132[0] = AUX_ENDO_LEAD_112[1] + + AUX_ENDO_LEAD_416[0] = Y__H__[1] ^ (1 + nu__H__) * theta__H__ * LGM[1] * beta__H__ ^ 4 / AUX_ENDO_LAG_5_2[-1] * (N__H__[1] ^ (1 - theta__H__) * LAMBDA__H__[1] * AUX_ENDO_LAG_5_2[-1] ^ theta__H__) ^ (-nu__H__) + + AUX_ENDO_LEAD_433[0] = AUX_ENDO_LEAD_416[1] + + AUX_ENDO_LEAD_151[0] = AUX_ENDO_LEAD_433[1] + + AUX_ENDO_LEAD_487[0] = Y__F__[1] ^ (1 + nu__F__) * theta__F__ * LGM[1] * beta__F__ ^ 4 / AUX_ENDO_LAG_4_2[-1] * (N__F__[1] ^ (1 - theta__F__) * LAMBDA__F__[1] * AUX_ENDO_LAG_4_2[-1] ^ theta__F__) ^ (-nu__F__) + + AUX_ENDO_LEAD_504[0] = AUX_ENDO_LEAD_487[1] + + AUX_ENDO_LEAD_302[0] = AUX_ENDO_LEAD_504[1] + + AUX_ENDO_LAG_5_1[0] = K__H__[-1] + + AUX_ENDO_LAG_5_2[0] = AUX_ENDO_LAG_5_1[-1] + + AUX_ENDO_LAG_5_3[0] = AUX_ENDO_LAG_5_2[-1] + + AUX_ENDO_LAG_16_1[0] = S__H__[-1] + + AUX_ENDO_LAG_16_2[0] = AUX_ENDO_LAG_16_1[-1] + + AUX_ENDO_LAG_4_1[0] = K__F__[-1] + + AUX_ENDO_LAG_4_2[0] = AUX_ENDO_LAG_4_1[-1] + + AUX_ENDO_LAG_4_3[0] = AUX_ENDO_LAG_4_2[-1] + + AUX_ENDO_LAG_15_1[0] = S__F__[-1] + + AUX_ENDO_LAG_15_2[0] = AUX_ENDO_LAG_15_1[-1] + + AUX_ENDO_LAG_25_1[0] = dLGM[-1] + + AUX_ENDO_LAG_25_2[0] = AUX_ENDO_LAG_25_1[-1] + +end + + +@parameters Backus_Kehoe_Kydland_1992 begin + K_ss = 11.0148 + + F_H_ratio = 1.0 + + mu__F__ = 0.34 + + mu__H__ = 0.34 + + gamma__F__ = (-1.0) + + gamma__H__ = (-1.0) + + alpha__F__ = 1.0 + + alpha__H__ = 1.0 + + eta__F__ = 0.5 + + eta__H__ = 0.5 + + theta__F__ = 0.36 + + theta__H__ = 0.36 + + nu__F__ = 3.0 + + nu__H__ = 3.0 + + sigma__F__ = 0.01 + + sigma__H__ = 0.01 + + delta__F__ = 0.025 + + delta__H__ = 0.025 + + psi__F__ = 0.5 + + psi__H__ = 0.5 + + Z_E__F__ = 0.00852 + + Z_E__H__ = 0.00852 + + rho__H____H__ = 0.906 + + rho__H____F__ = 0.088 + + phi__F__ = 0.25 + + phi__H__ = 0.25 + + beta__F__ = 0.9899998184488822 + + beta__H__ = 0.989999818448882 + + rho__F____F__ = rho__H____H__ + + rho__F____H__ = rho__H____F__ + +end + diff --git a/test/Backus_Kehoe_Kydland_1992.mod b/test/Backus_Kehoe_Kydland_1992.mod new file mode 100644 index 000000000..9d50fe69e --- /dev/null +++ b/test/Backus_Kehoe_Kydland_1992.mod @@ -0,0 +1,134 @@ +var +A__F__ A__H__ C__F__ C__H__ K__F__ K__H__ LAMBDA__F__ LAMBDA__H__ LGM L__F__ L__H__ NX__F__ NX__H__ N__F__ N__H__ S__F__ S__H__ U__F__ U__H__ X__F__ X__H__ Y__F__ Y__H__ Z__F__ Z__H__ dLGM dLGM_ann ; + +varexo +E__F__ E__H__ ; + +parameters +Z_E__F__ Z_E__H__ alpha__F__ alpha__H__ beta__F__ beta__H__ delta__F__ delta__H__ eta__F__ eta__H__ gamma__F__ gamma__H__ mu__F__ mu__H__ nu__F__ nu__H__ phi__F__ phi__H__ psi__F__ psi__H__ rho__F____F__ rho__F____H__ rho__H____F__ rho__H____H__ sigma__F__ sigma__H__ theta__F__ theta__H__ ; + +% Parameter definitions: + K_ss = 11.0148; + F_H_ratio = 1.0; + mu__F__ = 0.34; + mu__H__ = 0.34; + gamma__F__ = -1.0; + gamma__H__ = -1.0; + alpha__F__ = 1.0; + alpha__H__ = 1.0; + eta__F__ = 0.5; + eta__H__ = 0.5; + theta__F__ = 0.36; + theta__H__ = 0.36; + nu__F__ = 3.0; + nu__H__ = 3.0; + sigma__F__ = 0.01; + sigma__H__ = 0.01; + delta__F__ = 0.025; + delta__H__ = 0.025; + psi__F__ = 0.5; + psi__H__ = 0.5; + Z_E__F__ = 0.00852; + Z_E__H__ = 0.00852; + rho__H____H__ = 0.906; + rho__H____F__ = 0.088; + phi__F__ = 0.25; + phi__H__ = 0.25; + beta__F__ = 0.9899998184488822; + beta__H__ = 0.989999818448882; + rho__F____F__ = rho__H____H__; + rho__F____H__ = rho__H____F__; + +model; + Y__H__(0) = ((LAMBDA__H__(0) * K__H__(-4) ^ theta__H__ * N__H__(0) ^ (1 - theta__H__)) ^ -nu__H__ + sigma__H__ * Z__H__(-1) ^ -nu__H__) ^ (-1 / nu__H__); + + K__H__(0) = (1 - delta__H__) * K__H__(-1) + S__H__(0); + + X__H__(0) = phi__H__ * S__H__(-3) + phi__H__ * S__H__(-2) + phi__H__ * S__H__(-1) + phi__H__ * S__H__(0); + + A__H__(0) = (1 - eta__H__) * A__H__(-1) + N__H__(0); + + L__H__(0) = (1 - alpha__H__ * N__H__(0)) - (1 - alpha__H__) * eta__H__ * A__H__(-1); + + U__H__(0) = (C__H__(0) ^ mu__H__ * L__H__(0) ^ (1 - mu__H__)) ^ gamma__H__; + + ((psi__H__ * mu__H__) / C__H__(0)) * U__H__(0) = LGM(0); + + ((psi__H__ * (1 - mu__H__)) / L__H__(0)) * U__H__(0) * -alpha__H__ = ((-(LGM(0)) * (1 - theta__H__)) / N__H__(0)) * (LAMBDA__H__(0) * K__H__(-4) ^ theta__H__ * N__H__(0) ^ (1 - theta__H__)) ^ -nu__H__ * Y__H__(0) ^ (1 + nu__H__); + + (beta__H__ ^ 0 * LGM(0) * phi__H__ + beta__H__ ^ 1 * LGM(1) * phi__H__ + beta__H__ ^ 2 * LGM(2) * phi__H__ + beta__H__ ^ 3 * LGM(3) * phi__H__) + (-(beta__H__ ^ 1) * LGM(1) * phi__H__ * (1 - delta__H__) + -(beta__H__ ^ 2) * LGM(2) * phi__H__ * (1 - delta__H__) + -(beta__H__ ^ 3) * LGM(3) * phi__H__ * (1 - delta__H__) + -(beta__H__ ^ 4) * LGM(4) * phi__H__ * (1 - delta__H__)) = ((beta__H__ ^ 4 * LGM(4) * theta__H__) / K__H__(0)) * (LAMBDA__H__(4) * K__H__(0) ^ theta__H__ * N__H__(4) ^ (1 - theta__H__)) ^ -nu__H__ * Y__H__(4) ^ (1 + nu__H__); + + LGM(0) = beta__H__ * LGM(1) * (1 + sigma__H__ * Z__H__(0) ^ (-nu__H__ - 1) * Y__H__(1) ^ (1 + nu__H__)); + + NX__H__(0) = (Y__H__(0) - ((C__H__(0) + X__H__(0) + Z__H__(0)) - Z__H__(-1))) / Y__H__(0); + + Y__F__(0) = ((LAMBDA__F__(0) * K__F__(-4) ^ theta__F__ * N__F__(0) ^ (1 - theta__F__)) ^ -nu__F__ + sigma__F__ * Z__F__(-1) ^ -nu__F__) ^ (-1 / nu__F__); + + K__F__(0) = (1 - delta__F__) * K__F__(-1) + S__F__(0); + + X__F__(0) = phi__F__ * S__F__(-3) + phi__F__ * S__F__(-2) + phi__F__ * S__F__(-1) + phi__F__ * S__F__(0); + + A__F__(0) = (1 - eta__F__) * A__F__(-1) + N__F__(0); + + L__F__(0) = (1 - alpha__F__ * N__F__(0)) - (1 - alpha__F__) * eta__F__ * A__F__(-1); + + U__F__(0) = (C__F__(0) ^ mu__F__ * L__F__(0) ^ (1 - mu__F__)) ^ gamma__F__; + + ((psi__F__ * mu__F__) / C__F__(0)) * U__F__(0) = LGM(0); + + ((psi__F__ * (1 - mu__F__)) / L__F__(0)) * U__F__(0) * -alpha__F__ = ((-(LGM(0)) * (1 - theta__F__)) / N__F__(0)) * (LAMBDA__F__(0) * K__F__(-4) ^ theta__F__ * N__F__(0) ^ (1 - theta__F__)) ^ -nu__F__ * Y__F__(0) ^ (1 + nu__F__); + + (beta__F__ ^ 0 * LGM(0) * phi__F__ + beta__F__ ^ 1 * LGM(1) * phi__F__ + beta__F__ ^ 2 * LGM(2) * phi__F__ + beta__F__ ^ 3 * LGM(3) * phi__F__) + (-(beta__F__ ^ 1) * LGM(1) * phi__F__ * (1 - delta__F__) + -(beta__F__ ^ 2) * LGM(2) * phi__F__ * (1 - delta__F__) + -(beta__F__ ^ 3) * LGM(3) * phi__F__ * (1 - delta__F__) + -(beta__F__ ^ 4) * LGM(4) * phi__F__ * (1 - delta__F__)) = ((beta__F__ ^ 4 * LGM(4) * theta__F__) / K__F__(0)) * (LAMBDA__F__(4) * K__F__(0) ^ theta__F__ * N__F__(4) ^ (1 - theta__F__)) ^ -nu__F__ * Y__F__(4) ^ (1 + nu__F__); + + LGM(0) = beta__F__ * LGM(1) * (1 + sigma__F__ * Z__F__(0) ^ (-nu__F__ - 1) * Y__F__(1) ^ (1 + nu__F__)); + + NX__F__(0) = (Y__F__(0) - ((C__F__(0) + X__F__(0) + Z__F__(0)) - Z__F__(-1))) / Y__F__(0); + + LAMBDA__H__(0) - 1 = rho__H____H__ * (LAMBDA__H__(-1) - 1) + rho__H____F__ * (LAMBDA__F__(-1) - 1) + Z_E__H__ * E__H__; + + LAMBDA__F__(0) - 1 = rho__F____F__ * (LAMBDA__F__(-1) - 1) + rho__F____H__ * (LAMBDA__H__(-1) - 1) + Z_E__F__ * E__F__; + + ((C__H__(0) + X__H__(0) + Z__H__(0)) - Z__H__(-1)) + ((C__F__(0) + X__F__(0) + Z__F__(0)) - Z__F__(-1)) = Y__H__(0) + Y__F__(0); + + dLGM(0) = LGM(1) / LGM(0); + + dLGM_ann(0) = dLGM(-3) * dLGM(-2) * dLGM(-1) * dLGM(0); + +end; + +shocks; +var E__F__ = 1; +var E__H__ = 1; +end; + +initval; + A__F__ = 0.6064361690984907; + A__H__ = 0.6064361690984906; + C__F__ = 0.8260902429883573; + C__H__ = 0.8260902429883576; + K__F__ = 11.0148; + K__H__ = 11.0148; + LAMBDA__F__ = 0.9999999999999988; + LAMBDA__H__ = 0.9999999999999989; + LGM = 0.2787328444414798; + L__F__ = 0.6967819154507546; + L__H__ = 0.6967819154507549; + NX__F__ = 2.0159112082212333e-16; + NX__H__ = -1.0079556041106167e-16; + N__F__ = 0.30321808454924537; + N__H__ = 0.30321808454924526; + S__F__ = 0.27537; + S__H__ = 0.27537; + U__F__ = 1.3544616658441064; + U__H__ = 1.3544616658441064; + X__F__ = 0.27537; + X__H__ = 0.27537; + Y__F__ = 1.1014602429883575; + Y__H__ = 1.1014602429883575; + Z__F__ = 1.0986911684853575; + Z__H__ = 1.0986911684853493; + dLGM = 1.0; + dLGM_ann = 1.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Baxter_King_1993.jl b/test/Baxter_King_1993.jl new file mode 100644 index 000000000..33821f52d --- /dev/null +++ b/test/Baxter_King_1993.jl @@ -0,0 +1,89 @@ +using MacroModelling + +@model Baxter_King_1993 begin + uc[0] = c[0] ^ (-1) + + ul[0] = theta__l * l[0] ^ (-1) + + y[0] = A * k[-1] ^ theta__k * n[0] ^ theta__n + + fk[0] = n[0] ^ theta__n * A * theta__k * k[-1] ^ (theta__k - 1) + + fn[0] = k[-1] ^ theta__k * A * theta__n * n[0] ^ (theta__n - 1) + + gamma__x * k[0] = k[-1] * (1 - delta__k) + iv[0] + + l[0] + n[0] = 1 + + c[0] + iv[0] = y[0] * (1 - tau) + tr[0] + check_walras[0] + + c[0] + iv[0] + gb[0] = y[0] + + y[0] * tau = tr[0] + gb[0] + + uc[0] = lambda[0] + + ul[0] = fn[0] * (1 - tau) * lambda[0] + + beta * lambda[1] * (1 + q[1] - delta__k) = gamma__x * lambda[0] + + q[0] = fk[0] * (1 - tau) + + gb[0] = GB_BAR + e_gb[x] + + 1 + r[0] = gamma__x * lambda[0] / (beta * lambda[1]) + + w[0] = fn[0] + +end + + +@parameters Baxter_King_1993 begin + A = 1.0 + + gamma__x = 1.016 + + theta__k = 0.42 + + delta__k = 0.1 + + N = 0.2 + + R = 0.065 + + sG = 0.2 + + tau_BAR = 0.2 + + tau = 0.2 + + theta__n = 1-theta__k + + L = 1 - N + + beta = gamma__x/(1+R) + + Q = (gamma__x / beta - 1) + delta__k + + FK = Q / (1 - tau_BAR) + + K = (FK / (theta__k * A * N ^ theta__n)) ^ (1 / (theta__k - 1)) + + FN = theta__n * A * K ^ theta__k * N ^ (theta__n - 1) + + IV = ((gamma__x - 1) + delta__k) * K + + Y = A * N ^ (1 - theta__k) * K ^ theta__k + + GB_BAR = sG*Y + + C = (Y - IV) - GB_BAR + + UC = C ^ -1 + + UL = UC * (1 - tau_BAR) * FN + + theta__l = UL*L + +end + diff --git a/test/Baxter_King_1993.mod b/test/Baxter_King_1993.mod new file mode 100644 index 000000000..1e353562e --- /dev/null +++ b/test/Baxter_King_1993.mod @@ -0,0 +1,96 @@ +var +c check_walras fk fn gb iv k l n q r tr uc ul w y lambda ; + +varexo +e_gb ; + +parameters +A GB_BAR beta gamma__x delta__k theta__k theta__l theta__n tau ; + +% Parameter definitions: + A = 1.0; + gamma__x = 1.016; + theta__k = 0.42; + delta__k = 0.1; + N = 0.2; + R = 0.065; + sG = 0.2; + tau_BAR = 0.2; + tau = 0.2; + theta__n = 1 - theta__k; + L = 1 - N; + beta = gamma__x / (1 + R); + Q = (gamma__x / beta - 1) + delta__k; + FK = Q / (1 - tau_BAR); + K = (FK / (theta__k * A * N ^ theta__n)) ^ (1 / (theta__k - 1)); + FN = theta__n * A * K ^ theta__k * N ^ (theta__n - 1); + IV = ((gamma__x - 1) + delta__k) * K; + Y = A * N ^ (1 - theta__k) * K ^ theta__k; + GB_BAR = sG * Y; + C = (Y - IV) - GB_BAR; + UC = C ^ -1; + UL = UC * (1 - tau_BAR) * FN; + theta__l = UL * L; + +model; + uc(0) = c(0) ^ -1; + + ul(0) = theta__l * l(0) ^ -1; + + y(0) = A * k(-1) ^ theta__k * n(0) ^ theta__n; + + fk(0) = theta__k * A * k(-1) ^ (theta__k - 1) * n(0) ^ theta__n; + + fn(0) = theta__n * A * k(-1) ^ theta__k * n(0) ^ (theta__n - 1); + + gamma__x * k(0) = (1 - delta__k) * k(-1) + iv(0); + + l(0) + n(0) = 1; + + c(0) + iv(0) = (1 - tau) * y(0) + tr(0) + check_walras(0); + + c(0) + iv(0) + gb(0) = y(0); + + tau * y(0) = gb(0) + tr(0); + + uc(0) = lambda(0); + + ul(0) = lambda(0) * (1 - tau) * fn(0); + + beta * lambda(1) * ((q(1) + 1) - delta__k) = gamma__x * lambda(0); + + q(0) = (1 - tau) * fk(0); + + gb(0) = GB_BAR + e_gb; + + 1 + r(0) = (gamma__x * lambda(0)) / (lambda(1) * beta); + + w(0) = fn(0); + +end; + +shocks; +var e_gb = 1; +end; + +initval; + c = 0.1887100038517644; + check_walras = 5.551115123125783e-17; + fk = 0.2062499999999999; + fn = 0.9706929055197505; + gb = 0.0669443383117069; + iv = 0.07906734939506331; + k = 0.6816150809919252; + l = 0.8; + n = 0.19999999999999996; + q = 0.16499999999999992; + r = 0.06499999999999999; + tr = 1.3877787807814457e-17; + uc = 5.299136132631953; + ul = 4.1150670794633655; + w = 0.9706929055197505; + y = 0.33472169155853454; + lambda = 5.299136132631953; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Caldara_et_al_2012.jl b/test/Caldara_et_al_2012.jl new file mode 100644 index 000000000..844bb78af --- /dev/null +++ b/test/Caldara_et_al_2012.jl @@ -0,0 +1,53 @@ +using MacroModelling + +@model Caldara_et_al_2012 begin + V[0] = ((1 - beta) * (c[0] ^ nu * (1 - l[0]) ^ (1 - nu)) ^ (1 - 1 / psi) + beta * V[1] ^ (1 - 1 / psi)) ^ (1 / (1 - 1 / psi)) + + exp(s[0]) = V[1] ^ (1 - gamma) + + 1 = beta * c[0] * (1 + zeta * exp(z[1]) * k[0] ^ (zeta - 1) * l[1] ^ (1 - zeta) - delta) * (((1 - l[1]) / (1 - l[0])) ^ (1 - nu) * (c[1] / c[0]) ^ nu) ^ (1 - 1 / psi) / c[1] + + R_k[0] = zeta * exp(z[1]) * k[0] ^ (zeta - 1) * l[1] ^ (1 - zeta) - delta + + SDF_plus__1[0] = (((1 - l[1]) / (1 - l[0])) ^ (1 - nu) * (c[1] / c[0]) ^ nu) ^ (1 - 1 / psi) * beta * c[0] / c[1] + + 1 + R_f[0] = 1 / SDF_plus__1[0] + + c[0] * (1 - nu) / nu / (1 - l[0]) = (1 - zeta) * exp(z[0]) * k[-1] ^ zeta * l[0] ^ (-zeta) + + c[0] + i[0] = exp(z[0]) * k[-1] ^ zeta * l[0] ^ (1 - zeta) + + k[0] = i[0] + k[-1] * (1 - delta) + + z[0] = lambda * z[-1] + sigma[0] * epsilon__z[x] + + y[0] = exp(z[0]) * k[-1] ^ zeta * l[0] ^ (1 - zeta) + + log(sigma[0]) = (1 - rho) * log(sigma_bar) + rho * log(sigma[-1]) + eta * omega[x] + +end + + +@parameters Caldara_et_al_2012 begin + beta = 0.991 + + zeta = 0.3 + + delta = 0.0196 + + lambda = 0.95 + + psi = 0.5 + + gamma = 40.0 + + sigma_bar = 0.021 + + eta = 0.1 + + rho = 0.9 + + nu = 0.36218431417051217 + +end + diff --git a/test/Caldara_et_al_2012.mod b/test/Caldara_et_al_2012.mod new file mode 100644 index 000000000..73ddcaf25 --- /dev/null +++ b/test/Caldara_et_al_2012.mod @@ -0,0 +1,69 @@ +var +R_k R_f SDF_plus__1 V c i k l s y z sigma ; + +varexo +omega epsilon__z ; + +parameters +beta gamma delta zeta eta lambda nu rho sigma_bar psi ; + +% Parameter definitions: + beta = 0.991; + zeta = 0.3; + delta = 0.0196; + lambda = 0.95; + psi = 0.5; + gamma = 40.0; + sigma_bar = 0.021; + eta = 0.1; + rho = 0.9; + nu = 0.36218431417051217; + +model; + V(0) = ((1 - beta) * (c(0) ^ nu * (1 - l(0)) ^ (1 - nu)) ^ (1 - 1 / psi) + beta * V(1) ^ (1 - 1 / psi)) ^ (1 / (1 - 1 / psi)); + + exp(s(0)) = V(1) ^ (1 - gamma); + + 1 = (((1 + zeta * exp(z(1)) * k(0) ^ (zeta - 1) * l(1) ^ (1 - zeta)) - delta) * c(0) * beta * (((1 - l(1)) / (1 - l(0))) ^ (1 - nu) * (c(1) / c(0)) ^ nu) ^ (1 - 1 / psi)) / c(1); + + R_k(0) = zeta * exp(z(1)) * k(0) ^ (zeta - 1) * l(1) ^ (1 - zeta) - delta; + + SDF_plus__1(0) = (c(0) * beta * (((1 - l(1)) / (1 - l(0))) ^ (1 - nu) * (c(1) / c(0)) ^ nu) ^ (1 - 1 / psi)) / c(1); + + 1 + R_f(0) = 1 / SDF_plus__1(0); + + (((1 - nu) / nu) * c(0)) / (1 - l(0)) = (1 - zeta) * exp(z(0)) * k(-1) ^ zeta * l(0) ^ -zeta; + + c(0) + i(0) = exp(z(0)) * k(-1) ^ zeta * l(0) ^ (1 - zeta); + + k(0) = i(0) + k(-1) * (1 - delta); + + z(0) = lambda * z(-1) + sigma(0) * epsilon__z; + + y(0) = exp(z(0)) * k(-1) ^ zeta * l(0) ^ (1 - zeta); + + log(sigma(0)) = (1 - rho) * log(sigma_bar) + rho * log(sigma(-1)) + eta * omega; + +end; + +shocks; +var omega = 1; +var epsilon__z = 1; +end; + +initval; + R_k = 0.009081735620585375; + R_f = 0.009081735620585276; + SDF_plus__1 = 0.991; + V = 0.687138657856569; + c = 0.7247305637488348; + i = 0.18688997126148366; + k = 9.53520261538182; + l = 0.3333333333333333; + s = 14.633547871166774; + y = 0.9116205350103185; + z = 0.0; + sigma = 0.021; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/FRBUS.jl b/test/FRBUS.jl new file mode 100644 index 000000000..72c27c3a8 --- /dev/null +++ b/test/FRBUS.jl @@ -0,0 +1,3143 @@ +using MacroModelling + +@model FRBUS begin + delrff[0] = rff[0] - rff[-1] + + dpadj[0] = dpadj[-1] + dpgap[-1] + + dpgap[0] = y_dpgap_1 * pipxnc[0] + y_dpgap_2 * phr_l[0] - pxp_l[0] + y_dpgap_3 * phr_l[-1] + pxp_l[-1] + y_dpgap_4 * pbfir_l[0] + y_dpgap_5 * pbfir_l[-1] + y_dpgap_6 * pegfr_l[0] + y_dpgap_7 * pegfr_l[-1] + y_dpgap_8 * pegsr_l[0] + y_dpgap_9 * pegsr_l[-1] + y_dpgap_10 * pxr_l[0] + y_dpgap_11 * pxr_l[-1] + + ebfi_l[0] = y_ebfi_l_8 * hgpbfir[-1] + y_ebfi_l_6 * xb_l[-1] + y_ebfi_l_5 * zebfi[0] + y_ebfi_l_1 * ebfi_l[-1] + ebfi_l_aerr[x] + y_ebfi_l_2 * qebfi_l[-1] + y_ebfi_l_3 * AUX_ENDO_LAG_4_1[-1] + y_ebfi_l_4 * AUX_ENDO_LAG_4_2[-1] + y_ebfi_l_7 * AUX_ENDO_LAG_205_1[-1] + + ebfin_l[0] = ebfi_l[0] + pxp_l[0] + pbfir_l[0] + + ecd_l[0] = y_ecd_l_4 * zgapc2[0] + zecd[0] + y_ecd_l_1 * ecd_l[-1] + ecd_l_aerr[x] + y_ecd_l_2 * qecd_l[-1] + y_ecd_l_3 * AUX_ENDO_LAG_6_1[-1] + + ech_l[0] = y_ech_l_3 * ech_l_aerr[x] + kh_l[-1] + ech_l[-1] * y_ech_l_1 + y_ech_l_2 * AUX_ENDO_LAG_86_1[-1] + y_ech_l_4 * AUX_ENDO_LAG_7_1[-1] + y_ech_l_5 * AUX_ENDO_LAG_86_2[-1] + + ecnia_l[0] = ecnia_l[-1] + eco_l[0] * y_ecnia_l_1 + eco_l[-1] * y_ecnia_l_2 + ecd_l[0] * y_ecnia_l_3 + ecd_l[-1] * y_ecnia_l_4 + ech_l[0] * y_ecnia_l_5 + ech_l[-1] * y_ecnia_l_6 + + ecnian_l[0] = ecnia_l[0] + pcnia_l[0] + + eco_l[0] = y_eco_l_8 * yht_l[-1] + y_eco_l_7 * yhl_l[-1] + y_eco_l_6 * yht_l[0] + y_eco_l_5 * yhl_l[0] + y_eco_l_4 * zeco[0] + eco_l[-1] * y_eco_l_1 + eco_l_aerr[x] + y_eco_l_2 * qeco_l[-1] + y_eco_l_3 * AUX_ENDO_LAG_10_1[-1] + + egfe_l[0] = fiscal_egfe * fiscal[0] + y_egfe_l_7 * xgap2[-1] + y_egfe_l_6 * xgap2[0] + y_egfe_l_5 * egfet_l[0] + y_egfe_l_1 * egfe_l[-1] + egfe_l_aerr[x] + y_egfe_l_2 * egfet_l[-1] + y_egfe_l_3 * AUX_ENDO_LAG_11_1[-1] + y_egfe_l_4 * AUX_ENDO_LAG_11_2[-1] + + egfen_l[0] = pegfr_l[0] + pxp_l[0] + egfe_l[0] + + egfet_l[0] = egfet_l[-1] * y_egfet_l_1 + pegfr_l[-1] * y_egfet_l_2 + pxp_l[-1] * y_egfet_l_3 + y_egfet_l_4 * xgdptn_l[-1] + y_egfet_l_5 * hggdpt[0] + y_egfet_l_6 * hggdpt[-1] + y_egfet_l_7 * AUX_ENDO_LAG_68_1[-1] + y_egfet_l_8 * AUX_ENDO_LAG_68_2[-1] + + egfl_l[0] = fiscal[0] * fiscal_egfl + xgap2[-1] * y_egfl_l_7 + xgap2[0] * y_egfl_l_6 + y_egfl_l_5 * egflt_l[0] + y_egfl_l_1 * egfl_l[-1] + egfl_l_aerr[x] + y_egfl_l_2 * egflt_l[-1] + y_egfl_l_3 * AUX_ENDO_LAG_14_1[-1] + y_egfl_l_4 * AUX_ENDO_LAG_14_2[-1] + + egfln_l[0] = egfl_l[0] + pgfl_l[0] + + egflt_l[0] = egflt_l[-1] * y_egflt_l_1 + y_egflt_l_2 * pgfl_l[-1] + xgdptn_l[-1] * y_egflt_l_3 + hggdpt[0] * y_egflt_l_4 + hggdpt[-1] * y_egflt_l_5 + y_egflt_l_6 * AUX_ENDO_LAG_68_1[-1] + y_egflt_l_7 * AUX_ENDO_LAG_68_2[-1] + + egse_l[0] = xgap2[-1] * y_egse_l_7 + xgap2[0] * y_egse_l_6 + y_egse_l_5 * egset_l[0] + y_egse_l_1 * egse_l[-1] + egse_l_aerr[x] + y_egse_l_2 * egset_l[-1] + y_egse_l_3 * AUX_ENDO_LAG_17_1[-1] + y_egse_l_4 * AUX_ENDO_LAG_17_2[-1] + + egsen_l[0] = pegsr_l[0] + pxp_l[0] + egse_l[0] + + egset_l[0] = egset_l[-1] * y_egset_l_1 + pegsr_l[-1] * y_egset_l_2 + pxp_l[-1] * y_egset_l_3 + xgdptn_l[-1] * y_egset_l_4 + hggdpt[0] * y_egset_l_5 + hggdpt[-1] * y_egset_l_6 + y_egset_l_7 * AUX_ENDO_LAG_68_1[-1] + y_egset_l_8 * AUX_ENDO_LAG_68_2[-1] + + egsl_l[0] = xgap2[-1] * y_egsl_l_7 + xgap2[0] * y_egsl_l_6 + y_egsl_l_5 * egslt_l[0] + y_egsl_l_1 * egsl_l[-1] + egsl_l_aerr[x] + y_egsl_l_2 * egslt_l[-1] + y_egsl_l_3 * AUX_ENDO_LAG_20_1[-1] + y_egsl_l_4 * AUX_ENDO_LAG_20_2[-1] + + egsln_l[0] = egsl_l[0] + pgsl_l[0] + + egslt_l[0] = egslt_l[-1] * y_egslt_l_1 + y_egslt_l_2 * pgsl_l[-1] + xgdptn_l[-1] * y_egslt_l_3 + hggdpt[0] * y_egslt_l_4 + hggdpt[-1] * y_egslt_l_5 + y_egslt_l_6 * AUX_ENDO_LAG_68_1[-1] + y_egslt_l_7 * AUX_ENDO_LAG_68_2[-1] + + eh_l[0] = y_eh_l_7 * d83[x] + y_eh_l_5 * rme[-1] + zeh[0] + y_eh_l_1 * eh_l[-1] + eh_l_aerr[x] + y_eh_l_2 * qeh_l[-1] + y_eh_l_3 * AUX_ENDO_LAG_23_1[-1] + y_eh_l_4 * AUX_ENDO_LAG_23_2[-1] + y_eh_l_6 * AUX_ENDO_LAG_175_1[-1] + + ehn_l[0] = pxp_l[0] + phr_l[0] + eh_l[0] + + emn_l[0] = emon_l[0] * y_emn_l_2 + empn_l[0] * y_emn_l_3 + + emo_l[0] = y_emo_l_3 * AUX_EXO_LAG_367_0[-1] + y_emo_l_2 * pmo_l[-1] + emo_ltilde[0] + emo_l[-1] * y_emo_l_1 + y_emo_l_4 * xgdpn_l[-1] + xgap2[0] * y_emo_l_5 + xgap2[-1] * y_emo_l_6 + y_emo_l_7 * AUX_ENDO_LAG_213_1[-1] + y_emo_l_8 * ddockm[x] + y_emo_l_9 * AUX_EXO_LAG_282_0[-1] + + emo_ltilde[0] = (1 - rho_emo_l) * emo_lbar + rho_emo_l * emo_ltilde[-1] + emo_l_aerr[x] + + emon_l[0] = emo_l[0] + pmo_l[0] + + emp_l[0] = xgdp_l[0] + emp_l_aerr[x] + y_emp_l_1 * emptrt[x] + y_emp_l_2 * pmp_l[0] + y_emp_l_3 * pxb_l[0] + y_emp_l_4 * pmp_l[-1] + y_emp_l_5 * pxb_l[-1] + xgap2[-1] * y_emp_l_6 + + empn_l[0] = emp_l[0] + pmp_l[0] + + ex_l[0] = y_ex_l_10 * ddockx[x] + y_ex_l_1 * ex_l[-1] + ex_l_aerr[x] + pxr_l[-1] * y_ex_l_2 + pxp_l[-1] * y_ex_l_3 + y_ex_l_4 * fpx_l[-1] + y_ex_l_5 * fgdp_l[-1] + y_ex_l_6 * fpc_l[-1] + y_ex_l_7 * fxgap[0] + y_ex_l_8 * fxgap[-1] + y_ex_l_9 * AUX_ENDO_LAG_53_1[-1] + + exn_l[0] = pxr_l[0] + pxp_l[0] + ex_l[0] + + fcbn_l[0] = exn_l[0] * y_fcbn_l_2 + emn_l[0] * y_fcbn_l_3 + y_fcbn_l_4 * fynicn_l[0] + y_fcbn_l_5 * fyniln_l[0] + y_fcbn_l_6 * ufcbr[x] + pxb_l[0] * y_fcbn_l_7 + y_fcbn_l_8 * xbt_l[0] + + fgdp_l[0] = fgdpt_l[0] + fxgap[0] * y_fgdp_l_2 + + fgdpt_l[0] = y_fgdpt_l_1 * fgdpt_l[-1] + y_fgdpt_l_2 * xgdpt_l[-1] + hggdpt[0] * y_fgdpt_l_3 + hggdpt[-1] * y_fgdpt_l_4 + y_fgdpt_l_5 * AUX_ENDO_LAG_68_1[-1] + y_fgdpt_l_6 * AUX_ENDO_LAG_68_2[-1] + + fnicn_l[0] = y_fnicn_l_1 * fnicn_l[-1] + y_fnicn_l_2 * xgdptn_l[0] + y_fnicn_l_4 * fpc_l[0] + fpc_l[-1] * y_fnicn_l_5 + y_fnicn_l_6 * fpx_l[0] + fpx_l[-1] * y_fnicn_l_7 + y_fnicn_l_8 * rfnict[x] + + fniln_l[0] = y_fniln_l_1 * fniln_l[-1] + rfnict[x] * y_fniln_l_3 + xgdptn_l[0] * y_fniln_l_4 + fcbn_l[0] * y_fniln_l_5 + y_fniln_l_6 * pgdp_l[0] + y_fniln_l_7 * pgdp_l[-1] + fpx_l[0] * y_fniln_l_8 + fpx_l[-1] * y_fniln_l_9 + y_fniln_l_10 * fnirn_l[0] + + fnirn_l[0] = y_fnirn_l_2 * ufnir[x] + xgdpn_l[0] + + fpc_l[0] = fpc_l[-1] + y_fpc_l_2 * fpic[0] + + fpi10[0] = fxgap[-1] * y_fpi10_6 + y_fpi10_5 * fpitrg[x] + y_fpi10_1 * fpi10[-1] + y_fpi10_2 * AUX_ENDO_LAG_42_1[-1] + y_fpi10_3 * AUX_ENDO_LAG_42_2[-1] + y_fpi10_4 * AUX_ENDO_LAG_42_3[-1] + + fpi10t[0] = y_fpi10t_1 * fpi10t[-1] + fpi10[0] * y_fpi10t_2 + + fpic[0] = fpi10[0] * y_fpic_1 + y_fpic_2 * fpic[-1] + + fpx_l[0] = fpc_l[0] + fpxr_l[0] - pcpi_l[0] + + fpxr_l[0] = fpxrr_l[0] + y_fpxr_l_1 * rg10[0] + y_fpxr_l_2 * zpi10f[0] + y_fpxr_l_3 * frl10[0] + fpi10t[0] * y_fpxr_l_4 + fnicn_l[0] * y_fpxr_l_5 + fniln_l[0] * y_fpxr_l_6 + xgdpn_l[0] * y_fpxr_l_7 + + fpxrr_l[0] = fpxrr_ltilde[0] + y_fpxrr_l_1 * fpxrr_l[-1] + y_fpxrr_l_4 * fpxrrt[x] + y_fpxrr_l_3 * AUX_ENDO_LAG_47_1[-1] + y_fpxrr_l_2 * AUX_EXO_LAG_303_0[-1] + + fpxrr_ltilde[0] = (1 - rho_fpxrr_l) * fpxrr_lbar + rho_fpxrr_l * fpxrr_ltilde[-1] + fpxrr_l_aerr[x] + + frl10[0] = fxgap[-1] * y_frl10_6 + fxgap[0] * y_frl10_5 + y_frl10_4 * frs10[0] + y_frl10_1 * frl10[-1] + y_frl10_2 * frs10[-1] + y_frl10_3 * AUX_ENDO_LAG_49_1[-1] + + frs10[0] = rfrs10[x] + fxgap[0] * y_frs10_8 + fpitrg[x] * y_frs10_7 + y_frs10_1 * dfmprr[x] + y_frs10_2 * frstar[-1] + fpi10[0] * y_frs10_3 + fpi10[-1] * y_frs10_4 + y_frs10_5 * AUX_ENDO_LAG_42_1[-1] + y_frs10_6 * AUX_ENDO_LAG_42_2[-1] + + frstar[0] = frstar[-1] * y_frstar_1 + frs10[0] * y_frstar_2 + fpi10[0] * y_frstar_3 + fpi10[-1] * y_frstar_4 + y_frstar_5 * AUX_ENDO_LAG_42_1[-1] + y_frstar_6 * AUX_ENDO_LAG_42_2[-1] + + ftcin_l[0] = y_ftcin_l_2 * uftcin[x] + ynicpn_l[0] + + fxgap[0] = xgap2[-1] * y_fxgap_13 + frstar[0] * y_fxgap_12 + fpi10[-1] * y_fxgap_4 + frs10[-1] * y_fxgap_3 + fxgap_aerr[x] + fxgap[-1] * y_fxgap_1 + y_fxgap_2 * AUX_ENDO_LAG_53_1[-1] + y_fxgap_5 * AUX_ENDO_LAG_42_1[-1] + y_fxgap_6 * AUX_ENDO_LAG_42_2[-1] + y_fxgap_7 * AUX_ENDO_LAG_42_3[-1] + y_fxgap_8 * AUX_ENDO_LAG_50_1[-1] + y_fxgap_9 * AUX_ENDO_LAG_42_4[-1] + y_fxgap_10 * AUX_ENDO_LAG_50_2[-1] + y_fxgap_11 * AUX_ENDO_LAG_42_5[-1] + + fynicn_l[0] = fnicn_l[-1] + y_fynicn_l_2 * rfynic[0] + + fyniln_l[0] = fniln_l[-1] + y_fyniln_l_2 * rfynil[0] + + gfdbtnp_l[0] = ugfdbtp_l[0] + y_gfdbtnp_l_2 * gfdbtnp_l[-1] + y_gfdbtnp_l_3 * gfexpn_l[0] + y_gfdbtnp_l_4 * gfrecn_l[0] + + gfdbtn_l[0] = gfdbtnp_l[0] + ugfdbt_l[x] + + ugfdbtp_l[0] = (1 - rho_ugfdbtp_l) * ugfdbtp_lbar + rho_ugfdbtp_l * ugfdbtp_l[-1] + ugfdbtp_lerr[x] + + ugfsrp[0] = y_ugfsrp_1 * ugfsrp[-1] + + uleg_l[0] = uleg_l[-1] + y_uleg_l_1 * leg_l[-1] + y_uleg_l_2 * lep_l[-1] + y_uleg_l_3 * adjlegrt[x] + + gfexpn_l[0] = egfln_l[0] * y_gfexpn_l_2 + egfen_l[0] * y_gfexpn_l_3 + y_gfexpn_l_4 * gtn_l[0] + y_gfexpn_l_5 * gfintn_l[0] + + gfintn_l[0] = y_gfintn_l_2 * rgfint[0] + gfdbtn_l[-1] + + gfrecn_l[0] = y_gfrecn_l_2 * tpn_l[0] + y_gfrecn_l_3 * tcin_l[0] + ugfsrp[0] * y_gfrecn_l_4 + xgdpn_l[0] * y_gfrecn_l_5 + + gtn_l[0] = pgdp_l[0] + gtr_l[0] + + gtr_l[0] = y_gtr_l_2 * gtrd[0] + y_gtr_l_3 * gtrt[x] + xgdpt_l[0] + + gtrd[0] = 0.0014 * (fiscalav[0] - y_gtrd_6 * fiscalav[-1]) + y_gtrd_6 * gtrd[-1] + gtrd_aerr[x] + xgap2[0] * y_gtrd_1 + xgap2[-1] * y_gtrd_2 + y_gtrd_3 * AUX_ENDO_LAG_213_1[-1] + y_gtrd_4 * AUX_ENDO_LAG_213_2[-1] + y_gtrd_5 * AUX_ENDO_LAG_213_3[-1] + y_gtrd_7 * AUX_ENDO_LAG_213_4[-1] + + hgemp[0] = y_hgemp_1 * hgemp[-1] + emp_l[0] * y_hgemp_2 + emp_l[-1] * y_hgemp_3 + + hggdp[0] = xgdp_l[0] * y_hggdp_1 + y_hggdp_2 * xgdp_l[-1] + + hggdpt[0] = hxbt[0] + huxb[0] + + hgpbfir[0] = hgpbfir[-1] * y_hgpbfir_1 + pbfir_l[0] * y_hgpbfir_2 + pxp_l[0] * y_hgpbfir_3 + pxb_l[0] * y_hgpbfir_4 + pbfir_l[-1] * y_hgpbfir_5 + pxp_l[-1] * y_hgpbfir_6 + pxb_l[-1] * y_hgpbfir_7 + + hgpkir[0] = y_hgpkir_1 * hgpkir[-1] + y_hgpkir_2 * pkir[x] + y_hgpkir_3 * AUX_EXO_LAG_337_0[-1] + + hgynid[0] = ynicpn_l[0] * y_hgynid_1 + tcin_l[0] * y_hgynid_2 + pxb_l[0] * y_hgynid_3 + y_hgynid_4 * ynicpn_l[-1] + y_hgynid_5 * tcin_l[-1] + pxb_l[-1] * y_hgynid_6 + + hks[0] = y_hks_1 * kbfi_l[0] + y_hks_2 * kbfi_l[-1] + y_hks_3 * ki_l[0] + y_hks_4 * ki_l[-1] + hksr[x] + + hlept[0] = y_hlept_1 * hqlfpr[0] + y_hlept_2 * n16_l[x] + y_hlept_3 * AUX_EXO_LAG_325_0[-1] + + hlprdt[0] = hxbt[0] - hlept[0] - hqlww[0] + + hmfpt[0] = hmfpt_aerr[x] + y_hmfpt_1 * hmfpt[-1] + + hqlfpr[0] = hqlfpr_aerr[x] + y_hqlfpr_1 * hqlfpr[-1] + + hqlww[0] = hqlww_aerr[x] + y_hqlww_1 * hqlww[-1] + + huqpct[0] = y_huqpct_1 * huqpct[-1] + + huxb[0] = y_huxb_1 * dglprd[x] + y_huxb_2 * huxb[-1] + + hxbt[0] = hmfpt[0] + hks[0] * y_hxbt_5 + hlept[0] * y_hxbt_1 + hqlww[0] * y_hxbt_2 + y_hxbt_3 * lqualt_l[x] + y_hxbt_4 * AUX_EXO_LAG_321_0[-1] + + jccan_l[0] = xgdpn_l[0] + y_jccan_l_2 * jccan_l[-1] + xgdpn_l[-1] * y_jccan_l_3 + y_jccan_l_4 * pkbfir[-1] + kbfi_l[-1] * y_jccan_l_5 + y_jccan_l_6 * jrbfi[x] + pxp_l[-1] * y_jccan_l_7 + + jkcd_l[0] = y_jkcd_l_2 * jrcd[x] + kcd_l[-1] + + kbfi_l[0] = pbfir_l[0] * y_kbfi_l_2 + y_kbfi_l_3 * pkbfir[0] + ebfi_l[0] * y_kbfi_l_4 + jrbfi[x] * y_kbfi_l_5 + kbfi_l[-1] * y_kbfi_l_6 + + kcd_l[0] = ecd_l[0] * y_kcd_l_2 + jrcd[x] * y_kcd_l_3 + kcd_l[-1] * y_kcd_l_4 + + kh_l[0] = eh_l[0] * y_kh_l_2 + y_kh_l_3 * jrh[x] + kh_l[-1] * y_kh_l_4 + + ki_l[0] = ki_l[-1] * y_ki_l_1 + ki_l_aerr[x] + y_ki_l_2 * qkir_l[0] + y_ki_l_3 * xfs_l[-1] + y_ki_l_4 * AUX_ENDO_LAG_87_1[-1] + y_ki_l_5 * AUX_ENDO_LAG_210_1[-1] + y_ki_l_6 * AUX_ENDO_LAG_210_2[-1] + + ks_l[0] = ks_l[-1] + hks[0] * y_ks_l_1 + + leg_l[0] = uleg_l[0] + egfl_l[0] * y_leg_l_1 + egsl_l[0] * y_leg_l_2 - lprdt_l[0] + + leh_l[0] = y_leh_l_2 * lep_l[0] + leg_l[0] * y_leh_l_3 + y_leh_l_4 * leo_l[0] + + leo_l[0] = xgap2[-1] * y_leo_l_5 + y_leo_l_4 * qlf_l[-1] + leo_l_aerr[x] + y_leo_l_1 * qleor[x] + qlf_l[0] + y_leo_l_2 * leo_l[-1] + y_leo_l_3 * AUX_EXO_LAG_343_0[-1] + + lep_l[0] = lhp_l[0] - lww_l[0] + + leppot_l[0] = qlf_l[0] + y_leppot_l_2 * lurnat[0] + qleor[x] * y_leppot_l_3 + adjlegrt[x] * y_leppot_l_4 + + lf_l[0] = n16_l[x] + y_lf_l_2 * lfpr[0] + + lfpr[0] = hqlfpr[0] + y_lfpr_1 * lfpr[-1] + lfpr_aerr[x] + y_lfpr_2 * qlfpr[-1] + y_lfpr_3 * lur[-1] + y_lfpr_4 * lurnat[-1] + + lhp_l[0] = y_lhp_l_7 * hlprdt[-1] + y_lhp_l_6 * xbo_l[-1] + y_lhp_l_5 * xbo_l[0] + y_lhp_l_4 * zlhp[0] + y_lhp_l_1 * lhp_l[-1] + lhp_l_aerr[x] + y_lhp_l_2 * qlhp_l[-1] + y_lhp_l_3 * AUX_ENDO_LAG_96_1[-1] + y_lhp_l_8 * AUX_ENDO_LAG_207_1[-1] + y_lhp_l_9 * AUX_ENDO_LAG_74_1[-1] + + lprdt_l[0] = xbt_l[0] - leppot_l[0] - qlww_l[0] + + lur[0] = leh_l[0] * y_lur_1 + lf_l[0] * y_lur_2 + + lurnat[0] = lurnat_aerr[x] + lurnat[-1] * y_lurnat_1 + + lww_l[0] = y_lww_l_1 * lww_l[-1] + hqlww[0] * y_lww_l_2 + lww_l_aerr[x] + y_lww_l_3 * qlww_l[-1] + lhp_l[0] * y_lww_l_4 + lhp_l[-1] * y_lww_l_5 + hlept[0] * y_lww_l_6 + + mfpt_l[0] = mfpt_l_aerr[x] + mfpt_l[-1] + hmfpt[0] * y_mfpt_l_1 + + pbfir_l[0] = pbfir_l[-1] + dpadj[0] + pxp_l[-1] + pbfir_l_aerr[x] + pipxnc[0] * y_pbfir_l_1 - pxp_l[0] + + pcdr_l[0] = y_pcdr_l_1 * pcdr_l[-1] + y_pcdr_l_2 * AUX_ENDO_LAG_103_1[-1] + + pcer_l[0] = pcer_l[-1] + pcer_l_aerr[x] + pmp_l[0] * y_pcer_l_1 + y_pcer_l_2 * pcxfe_l[0] + pmp_l[-1] * y_pcer_l_3 + y_pcer_l_4 * pcxfe_l[-1] + + pcfr_l[0] = pcfr_l_aerr[x] + y_pcfr_l_1 * pcfr_l[-1] + y_pcfr_l_6 * pcfrt[x] + y_pcfr_l_5 * AUX_ENDO_LAG_105_3[-1] + y_pcfr_l_4 * AUX_ENDO_LAG_105_2[-1] + y_pcfr_l_3 * AUX_ENDO_LAG_105_1[-1] + y_pcfr_l_2 * AUX_EXO_LAG_329_0[-1] + + pchr_l[0] = y_pchr_l_1 * pchr_l[-1] + y_pchr_l_2 * AUX_ENDO_LAG_106_1[-1] + + pcnia_l[0] = pcnia_l[-1] + y_pcnia_l_1 * picnia[0] + + pcor_l[0] = pcor_l[-1] + pcdr_l[0] * y_pcor_l_1 + pcdr_l[-1] * y_pcor_l_2 + pchr_l[0] * y_pcor_l_3 + pchr_l[-1] * y_pcor_l_4 + + pcpi_l[0] = pcnia_l[0] + y_pcpi_l_2 * upcpi[x] + + pcpix_l[0] = pcxfe_l[0] + y_pcpix_l_2 * upcpix[x] + + pcxfe_l[0] = pcxfe_l[-1] + y_pcxfe_l_1 * picxfe[0] + + pegfr_l[0] = pegfr_l[-1] + dpadj[0] + pxp_l[-1] + pegfr_l_aerr[x] + pipxnc[0] * y_pegfr_l_1 - pxp_l[0] + + pegsr_l[0] = pegsr_l[-1] + dpadj[0] + pxp_l[-1] + pegsr_l_aerr[x] + pipxnc[0] * y_pegsr_l_1 - pxp_l[0] + + pgdp_l[0] = xgdpn_l[0] - xgdp_l[0] + + pgfl_l[0] = y_pgfl_l_1 * upgfl[x] + pl_l[0] - lprdt_l[0] + + pgsl_l[0] = pl_l[0] + y_pgsl_l_1 * upgsl[x] - lprdt_l[0] + + phouse_l[0] = pcnia_l[-1] * y_phouse_l_4 + pchr_l[-1] * y_phouse_l_3 + y_phouse_l_1 * phouse_l[-1] + phouse_l_aerr[x] + y_phouse_l_2 * AUX_ENDO_LAG_117_1[-1] + + phr_l[0] = phr_l[-1] + dpadj[0] + pxp_l[-1] + phr_l_aerr[x] + pipxnc[0] * y_phr_l_1 - pxp_l[0] + + pic4[0] = pcnia_l[0] * y_pic4_1 + y_pic4_2 * AUX_ENDO_LAG_107_3[-1] + + picnia[0] = picxfe[0] + pcer_l[0] * y_picnia_1 + pcer_l[-1] * y_picnia_2 + pcfr_l[0] * y_picnia_3 + pcfr_l[-1] * y_picnia_4 + + picx4[0] = pcxfe_l[0] * y_picx4_1 + y_picx4_2 * AUX_ENDO_LAG_111_3[-1] + + picxfe[0] = picxfe_aerr[x] + y_picxfe_1 * picxfe[-1] + y_picxfe_2 * zpicxfe[0] + y_picxfe_3 * ptr[-1] + y_picxfe_4 * qpcnia_l[-1] + pcnia_l[-1] * y_picxfe_5 + + pieci[0] = y_pieci_12 * pl_l[-1] + y_pieci_11 * qpl_l[-1] + lurnat[-1] * y_pieci_10 + lur[-1] * y_pieci_9 + huqpct[-1] * y_pieci_8 + hlprdt[-1] * y_pieci_7 + ptr[-1] * y_pieci_6 + y_pieci_5 * zpieci[0] + pieci_aerr[x] + y_pieci_1 * pieci[-1] + y_pieci_2 * AUX_ENDO_LAG_123_1[-1] + y_pieci_3 * AUX_ENDO_LAG_123_2[-1] + y_pieci_4 * AUX_ENDO_LAG_123_3[-1] + + pigdp[0] = pgdp_l[0] * y_pigdp_1 + pgdp_l[-1] * y_pigdp_2 + + pipl[0] = pieci[0] + + pipxnc[0] = picnia[0] + y_pipxnc_11 * pxnc_l[-1] + y_pipxnc_10 * qpxnc_l[-1] + y_pipxnc_9 * fpxr_l[-1] + fpxr_l[0] * y_pipxnc_8 + huqpct[0] * y_pipxnc_1 + y_pipxnc_2 * pipxnc[-1] + y_pipxnc_3 * picnia[-1] + huqpct[-1] * y_pipxnc_4 + y_pipxnc_5 * AUX_ENDO_LAG_126_1[-1] + y_pipxnc_6 * AUX_ENDO_LAG_120_1[-1] + y_pipxnc_7 * AUX_ENDO_LAG_78_1[-1] + + pkbfir[0] = y_pkbfir_1 * upkbfir[x] + pbfir_l[0] * y_pkbfir_2 + + pl_l[0] = pl_l[-1] + pipl[0] * y_pl_l_1 + + pmo_l[0] = pmo_l[-1] * y_pmo_l_1 + pmo_ltilde[0] + y_pmo_l_2 * qpmo_l + fpc_l[-1] * y_pmo_l_3 + fpx_l[-1] * y_pmo_l_4 + pxb_l[-1] * y_pmo_l_5 + fpc_l[0] * y_pmo_l_6 + fpx_l[0] * y_pmo_l_7 + pxb_l[0] * y_pmo_l_8 + + pmo_ltilde[0] = (1 - rho_pmo_l) * pmo_lbar + rho_pmo_l * pmo_ltilde[-1] + pmo_l_aerr[x] + + pmp_l[0] = y_pmp_l_2 * upmp[x] + poil_l[0] + + poil_l[0] = pxb_l[0] + poilr_l[0] + + poilr_l[0] = poilr_l_aerr[x] + y_poilr_l_1 * poilr_l[-1] + y_poilr_l_4 * poilrt[x] + y_poilr_l_3 * AUX_ENDO_LAG_133_1[-1] + y_poilr_l_2 * AUX_EXO_LAG_340_0[-1] + + ptr[0] = ptr[-1] * y_ptr_1 + picxfe[-1] * y_ptr_2 + y_ptr_3 * AUX_EXO_LAG_336_0[-1] + + pxb_l[0] = pgdp_l[0] + y_pxb_l_2 * upxb[x] + + pxnc_l[0] = pxnc_l[-1] + pipxnc[0] * y_pxnc_l_1 + + pxp_l[0] = pxp_l[-1] + pcnia_l[0] * y_pxp_l_1 + pcnia_l[-1] * y_pxp_l_2 + pxnc_l[0] * y_pxp_l_3 + pxnc_l[-1] * y_pxp_l_4 + + pxr_l[0] = pxr_l[-1] + dpadj[0] + pxp_l[-1] + pxr_l_aerr[x] + pipxnc[0] * y_pxr_l_1 - pxp_l[0] + + qebfi_l[0] = xb_l[0] + y_qebfi_l_2 * vbfi[0] + hxbt[0] * y_qebfi_l_3 + hgpbfir[0] * y_qebfi_l_4 + jrbfi[x] * y_qebfi_l_5 + + qec_l[0] = y_qec_l_1 * zyh_l[0] + y_qec_l_2 * zyht_l[0] + y_qec_l_3 * zyhp_l[0] + y_qec_l_4 * wpo_l[0] + y_qec_l_5 * wps_l[0] + + qecd_l[0] = qec_l[0] + y_qecd_l_13 * rccd[0] + pcdr_l[0] * y_qecd_l_12 + y_qecd_l_11 * hgpcdr[x] + jrcd[x] * y_qecd_l_2 + hggdpt[0] * y_qecd_l_3 + hggdpt[-1] * y_qecd_l_4 + y_qecd_l_5 * AUX_ENDO_LAG_68_1[-1] + y_qecd_l_6 * AUX_ENDO_LAG_68_2[-1] + y_qecd_l_7 * AUX_ENDO_LAG_68_3[-1] + y_qecd_l_8 * AUX_ENDO_LAG_68_4[-1] + y_qecd_l_9 * AUX_ENDO_LAG_68_5[-1] + y_qecd_l_10 * AUX_ENDO_LAG_68_6[-1] + + qeco_l[0] = qec_l[0] - pcor_l[0] + + qeh_l[0] = qec_l[0] + pcnia_l[0] + y_qeh_l_19 * rcch[0] + jrh[x] * y_qeh_l_2 + hggdpt[0] * y_qeh_l_3 + hggdpt[-1] * y_qeh_l_4 + y_qeh_l_5 * AUX_ENDO_LAG_68_1[-1] + y_qeh_l_6 * AUX_ENDO_LAG_68_2[-1] + y_qeh_l_7 * AUX_ENDO_LAG_68_3[-1] + y_qeh_l_8 * AUX_ENDO_LAG_68_4[-1] + y_qeh_l_9 * AUX_ENDO_LAG_68_5[-1] + y_qeh_l_10 * AUX_ENDO_LAG_68_6[-1] + y_qeh_l_11 * AUX_ENDO_LAG_68_7[-1] + y_qeh_l_12 * AUX_ENDO_LAG_68_8[-1] + y_qeh_l_13 * AUX_ENDO_LAG_68_9[-1] + y_qeh_l_14 * AUX_ENDO_LAG_68_10[-1] + y_qeh_l_15 * AUX_ENDO_LAG_68_11[-1] + y_qeh_l_16 * AUX_ENDO_LAG_68_12[-1] + y_qeh_l_17 * AUX_ENDO_LAG_68_13[-1] + y_qeh_l_18 * AUX_ENDO_LAG_68_14[-1] - phr_l[0] - pxp_l[0] + + qkir_l[0] = dglprd[x] * y_qkir_l_1 + rho_qkir_l * qkir_l[-1] + + qlf_l[0] = n16_l[x] + y_qlf_l_2 * qlfpr[0] + + qlfpr[0] = hqlfpr[0] + qlfpr[-1] + + qlhp_l[0] = xbo_l[0] - lprdt_l[0] + + qlww_l[0] = qlww_l[-1] + hqlww[-1] * y_qlww_l_1 + + qpcnia_l[0] = qpxp_l[0] + uqpct_l[0] + + qpl_l[0] = pxb_l[0] + pl_l[0] - qpxb_l[0] + + qpxb_l[0] = pl_l[0] + pwstar_l[x] - lprdt_l[0] + + qpxnc_l[0] = pxnc_l[0] + qpxp_l[0] * y_qpxnc_l_1 + pxp_l[0] * y_qpxnc_l_2 + qpcnia_l[0] * y_qpxnc_l_3 + pcnia_l[0] * y_qpxnc_l_4 + + qpxp_l[0] = pxp_l[0] + qpxb_l[0] * y_qpxp_l_1 + pxb_l[0] * y_qpxp_l_2 + + qynidn_l[0] = y_qynidn_l_1 * d79a[x] + ynicpn_l[0] * y_qynidn_l_2 + tcin_l[0] * y_qynidn_l_3 + + rbbb[0] = rg10[0] + rbbbp[0] + + rbbbp[0] = rbbbp_aerr[x] + y_rbbbp_1 * zgap10[0] + y_rbbbp_2 * rbbbp[-1] + y_rbbbp_3 * zgap10[-1] + + rbfi[0] = y_rbfi_1 * trfcim[x] + y_rbfi_2 * rg5[0] + rbbb[0] * y_rbfi_3 + rg10[0] * y_rbfi_4 + y_rbfi_5 * zpib5[0] + y_rbfi_6 * req[0] + + rcar[0] = rcar_aerr[x] + d79a[x] * y_rcar_1 + y_rcar_2 * t47[x] + y_rcar_3 * rcar[-1] + rg5[0] * y_rcar_4 + y_rcar_5 * rg5[-1] + + rccd[0] = rcar[0] + jrcd[x] * y_rccd_1 - zpi5[0] + + rcch[0] = jrh[x] * y_rcch_1 + y_rcch_2 * trfpm[x] + y_rcch_3 * rme[0] + y_rcch_4 * trspp[x] - zpi10[0] + + rcgain[0] = picx4[0] + rcgain_aerr[x] + xgap2[0] * y_rcgain_1 + y_rcgain_2 * rcgain[-1] + y_rcgain_3 * picx4[-1] + + req[0] = rg30[0] - zpic30[0] + reqp[0] + + reqp[0] = reqp_aerr[x] + rbbbp[0] * y_reqp_1 + y_reqp_2 * reqp[-1] + rbbbp[-1] * y_reqp_3 + + rfynic[0] = rfynil[0] * y_rfynic_4 + y_rfynic_1 * rfynic[-1] + rfynic_aerr[x] + y_rfynic_2 * rfynil[-1] + y_rfynic_3 * AUX_ENDO_LAG_165_1[-1] + + rfynil[0] = reqp[0] * y_rfynil_8 + y_rfynil_7 * rtb[0] + rg10[0] * y_rfynil_6 + rfynil[-1] * y_rfynil_1 + rfynil_aerr[x] + y_rfynil_2 * rg10[-1] + y_rfynil_3 * rtb[-1] + reqp[-1] * y_rfynil_4 + y_rfynil_5 * AUX_ENDO_LAG_166_1[-1] + + rg10[0] = zrff10[0] + rg10p[0] + + rg10p[0] = rg10p_aerr[x] + zgap10[0] * y_rg10p_1 + y_rg10p_2 * d8095[x] + y_rg10p_3 * rg10p[-1] + zgap10[-1] * y_rg10p_4 + y_rg10p_5 * AUX_EXO_LAG_279_0[-1] + + rg30[0] = zrff30[0] + rg30p[0] + + rg30p[0] = rg30p_aerr[x] + y_rg30p_1 * zgap30[0] + d8095[x] * y_rg30p_2 + y_rg30p_3 * rg30p[-1] + y_rg30p_4 * zgap30[-1] + y_rg30p_5 * AUX_EXO_LAG_279_0[-1] + + rg5[0] = zrff5[0] + rg5p[0] + + rg5p[0] = rg5p_aerr[x] + y_rg5p_1 * zgap05[0] + y_rg5p_2 * rg5p[-1] + y_rg5p_3 * zgap05[-1] + + rgfint[0] = gfdbtn_l[-1] * y_rgfint_4 + rgfint_aerr[x] + y_rgfint_1 * rgfint[-1] + y_rgfint_2 * rgw[-1] + y_rgfint_3 * AUX_ENDO_LAG_56_1[-1] + + rgw[0] = rtb[0] * y_rgw_1 + rg5[0] * y_rgw_2 + rg10[0] * y_rgw_3 + rg30[0] * y_rgw_4 + + rme[0] = rme[-1] * y_rme_1 + rme_aerr[x] + rg10[0] * y_rme_2 + rg10[-1] * y_rme_3 + y_rme_4 * d87[x] + + rrff[0] = rff[0] + picxfe[0] * y_rrff_1 + picxfe[-1] * y_rrff_2 + y_rrff_3 * AUX_ENDO_LAG_122_1[-1] + y_rrff_4 * AUX_ENDO_LAG_122_2[-1] + + rrtr[0] = y_rrtr_1 * rrtr[-1] + rrff[0] * y_rrtr_2 + + rspnia[0] = y_rspnia_1 * yhsn_l[0] + y_rspnia_2 * ydn_l[0] + + rtb[0] = rff[-1] * y_rtb_4 + rff[0] * y_rtb_3 + rtb[-1] * y_rtb_1 + y_rtb_2 * AUX_ENDO_LAG_179_1[-1] + + rtbfi_l[0] = pxp_l[0] + rbfi[0] * y_rtbfi_l_2 + jrbfi[x] * y_rtbfi_l_3 + hgpbfir[0] * y_rtbfi_l_4 + y_rtbfi_l_5 * tritc[x] + trfcim[x] * y_rtbfi_l_6 + y_rtbfi_l_7 * tapddp[x] + y_rtbfi_l_8 * tdpv[x] + pkbfir[0] * y_rtbfi_l_9 - pxb_l[0] + + rtinv[0] = pxb_l[0] * y_rtinv_7 + rbfi[0] * y_rtinv_1 + hgpkir[0] * y_rtinv_2 + pxp_l[0] * y_rtinv_3 + pkir[x] * y_rtinv_4 + pxp_l[-1] * y_rtinv_5 + y_rtinv_6 * AUX_EXO_LAG_337_0[-1] + + rtr[0] = ptr[0] + rrtr[0] + + tcin_l[0] = ynicpn_l[0] + y_tcin_l_2 * trci[0] + + tpn_l[0] = y_tpn_l_2 * trp[0] + y_tpn_l_3 * ypn_l[0] + gtn_l[0] * y_tpn_l_4 + + trci[0] = xgap2[-1] * y_trci_4 + trci_aerr[x] + trcit[x] + xgap2[0] * y_trci_1 + y_trci_2 * trci[-1] + y_trci_3 * AUX_EXO_LAG_361_0[-1] + + trp[0] = xgap2[0] * y_trp_5 + trp_a[0] + trpt[0] + y_trp_1 * trp[-1] + y_trp_2 * trpt[-1] + y_trp_3 * AUX_ENDO_LAG_187_1[-1] + y_trp_4 * AUX_ENDO_LAG_189_1[-1] + + trp_a[0] = (1 - rho_trp_a) * trp_abar + rho_trp_a * trp_a[-1] + trp_aerr[x] + + trpt[0] = trpts[0] + + trptd[0] = y_trptd_3 * AUX_EXO_LAG_305_0[-1] + xgdpn_l[-1] * y_trptd_2 + gfdbtnp_l[-1] * y_trptd_1 + trpt[-1] + y_trptd_4 * AUX_ENDO_LAG_56_1[-1] + y_trptd_5 * AUX_ENDO_LAG_218_1[-1] + y_trptd_6 * AUX_EXO_LAG_305_1[-1] + + trpts[0] = trpt[-1] + xgap2[-1] * y_trpts_5 + y_trpts_1 * gfrecn_l[-1] + y_trpts_2 * gfexpn_l[-1] + xgdpn_l[-1] * y_trpts_3 + y_trpts_4 * gfsrt[-1] + + gfsrt[0] = gfsrt[-1] * rho_gfsrt + gfsrt_err[x] + + tryh[0] = tpn_l[0] * y_tryh_1 + y_tryh_2 * yhln_l[0] + y_tryh_3 * yhptn_l[0] + + uqpct_l[0] = huqpct[0] + uqpct_l[-1] + + uxbt_l[0] = uxbt_l[-1] + huxb[0] * y_uxbt_l_1 + + uynicpnr[0] = y_uynicpnr_1 * uynicpnr[-1] + + vbfi[0] = y_vbfi_1 * uvbfi[x] + pkbfir[0] * y_vbfi_2 + pbfir_l[0] * y_vbfi_3 + rtbfi_l[0] * y_vbfi_4 + + wpo_l[0] = wpon_l[0] - pcnia_l[0] + + wpon_l[0] = y_wpon_l_2 * wpon_l[-1] + rcgain[0] * y_wpon_l_3 + phouse_l[0] * y_wpon_l_4 + phouse_l[-1] * y_wpon_l_5 + ydn_l[0] * y_wpon_l_6 + ecnian_l[0] * y_wpon_l_7 + y_wpon_l_8 * yhibn_l[0] + pcdr_l[0] * y_wpon_l_9 + pcnia_l[0] * y_wpon_l_10 + ecd_l[0] * y_wpon_l_11 + jkcd_l[0] * y_wpon_l_12 + + wps_l[0] = wpsn_l[0] - pcnia_l[0] + + wpsn_l[0] = ynicpn_l[0] * y_wpsn_l_1 + tcin_l[0] * y_wpsn_l_2 + req[0] * y_wpsn_l_3 + y_wpsn_l_4 * zdivgr[0] + + xb_l[0] = y_xb_l_2 * xbn_l[0] + pxb_l[0] * y_xb_l_3 + + xbn_l[0] = pxb_l[0] * y_xbn_l_2 + xbo_l[0] * y_xbn_l_3 + xgdpn_l[0] * y_xbn_l_4 + y_xbn_l_5 * xgdo_l[0] + pgdp_l[0] * y_xbn_l_6 + + xbo_l[0] = xbt_l[0] + xgap2[0] * y_xbo_l_1 + + xbt_l[0] = mfpt_l[0] + leppot_l[0] * y_xbt_l_1 + qlww_l[0] * y_xbt_l_2 + lqualt_l[x] * y_xbt_l_3 + ks_l[0] * y_xbt_l_4 + xbtr_l[0] + + xbtr_l[0] = y_xbtr_l_1 * xbtr_l[-1] + + xfs_l[0] = xfs_l[-1] + ecnia_l[0] * y_xfs_l_1 + ecnia_l[-1] * y_xfs_l_2 + eh_l[0] * y_xfs_l_3 + eh_l[-1] * y_xfs_l_4 + ebfi_l[0] * y_xfs_l_5 + ebfi_l[-1] * y_xfs_l_6 + egfe_l[0] * y_xfs_l_7 + egfe_l[-1] * y_xfs_l_8 + egfl_l[0] * y_xfs_l_9 + egfl_l[-1] * y_xfs_l_10 + egse_l[0] * y_xfs_l_11 + egse_l[-1] * y_xfs_l_12 + egsl_l[0] * y_xfs_l_13 + egsl_l[-1] * y_xfs_l_14 + ex_l[0] * y_xfs_l_15 + ex_l[-1] * y_xfs_l_16 + emo_l[0] * y_xfs_l_17 + emo_l[-1] * y_xfs_l_18 + emp_l[0] * y_xfs_l_19 + emp_l[-1] * y_xfs_l_20 + + xfsn_l[0] = xgdpn_l[0] * y_xfsn_l_2 + pkir[x] * y_xfsn_l_3 + pxp_l[0] * y_xfsn_l_4 + ki_l[0] * y_xfsn_l_5 + ki_l[-1] * y_xfsn_l_6 + + xgap[0] = xbo_l[0] * y_xgap_1 + xbt_l[0] * y_xgap_2 + + xgap2[0] = xgdo_l[0] * y_xgap2_1 + xgdpt_l[0] * y_xgap2_2 + + xgdi_l[0] = xgdo_l[0] + mei_l + + xgdin_l[0] = pgdp_l[0] + xgdi_l[0] + + xgdo_l[0] = xgdp_l[0] - mep_l + + xgdp_l[0] = xgdp_l[-1] + xfs_l[0] * y_xgdp_l_1 + xfs_l[-1] * y_xgdp_l_2 + ki_l[0] * y_xgdp_l_3 + ki_l[-1] * y_xgdp_l_4 + y_xgdp_l_5 * AUX_ENDO_LAG_87_1[-1] + + xgdpn_l[0] = y_xgdpn_l_2 * xpn_l[0] + egfln_l[0] * y_xgdpn_l_3 + egsln_l[0] * y_xgdpn_l_4 + emn_l[0] * y_xgdpn_l_5 + pkir[x] * y_xgdpn_l_6 + pxp_l[0] * y_xgdpn_l_7 + ki_l[0] * y_xgdpn_l_8 + ki_l[-1] * y_xgdpn_l_9 + + xgdpt_l[0] = xbt_l[0] + uxbt_l[0] + + xgdptn_l[0] = pgdp_l[0] + xgdpt_l[0] + + xp_l[0] = xp_l[-1] + ecnia_l[0] * y_xp_l_1 + ecnia_l[-1] * y_xp_l_2 + eh_l[0] * y_xp_l_3 + eh_l[-1] * y_xp_l_4 + ebfi_l[0] * y_xp_l_5 + ebfi_l[-1] * y_xp_l_6 + egfe_l[0] * y_xp_l_7 + egfe_l[-1] * y_xp_l_8 + egse_l[0] * y_xp_l_9 + egse_l[-1] * y_xp_l_10 + ex_l[0] * y_xp_l_11 + ex_l[-1] * y_xp_l_12 + + xpn_l[0] = pxp_l[0] + xp_l[0] + + ydn_l[0] = y_ydn_l_2 * uyd[x] + ypn_l[0] * y_ydn_l_3 + tpn_l[0] * y_ydn_l_4 + + yh_l[0] = yhl_l[0] * y_yh_l_2 + yht_l[0] * y_yh_l_3 + y_yh_l_4 * yhp_l[0] + + yhgap[0] = y_yhgap_1 * yhshr_l[0] + y_yhgap_2 * zyhst_l[0] + + yhibn_l[0] = xgdpn_l[0] + y_yhibn_l_2 * uyhibn[x] + + yhl_l[0] = yhln_l[0] + tryh[0] * y_yhl_l_2 - pcnia_l[0] + + yhln_l[0] = y_yhln_l_2 * uyhln[x] + yniln_l[0] + + yhp_l[0] = tryh[0] * y_yhp_l_2 + yhptn_l[0] * y_yhp_l_3 + y_yhp_l_4 * yhpntn_l[0] - pcnia_l[0] + + yhpcd_l[0] = kcd_l[-1] + + yhpgap[0] = y_yhpgap_1 * yhpshr_l[0] + y_yhpgap_2 * zyhpst_l[0] + + yhpntn_l[0] = pcnia_l[0] * y_yhpntn_l_2 + pcdr_l[0] * y_yhpntn_l_3 + yhpcd_l[0] * y_yhpntn_l_4 + yhibn_l[0] * y_yhpntn_l_5 + ynicpn_l[0] * y_yhpntn_l_6 + tcin_l[0] * y_yhpntn_l_7 + y_yhpntn_l_8 * ynidn_l[0] + zpi10[0] * y_yhpntn_l_9 + gfdbtn_l[0] * y_yhpntn_l_10 + + yhpshr_l[0] = yhp_l[0] - yh_l[0] + + yhptn_l[0] = y_yhptn_l_2 * uyhptn[x] + y_yhptn_l_3 * ynirn_l[0] + gfintn_l[0] * y_yhptn_l_4 + ynidn_l[0] * y_yhptn_l_5 + yhibn_l[0] * y_yhptn_l_6 + + yhshr_l[0] = yh_l[0] * y_yhshr_l_2 + xgdp_l[0] * y_yhshr_l_3 + + yhsn_l[0] = yhln_l[0] * y_yhsn_l_2 + y_yhsn_l_3 * yhtn_l[0] + yhptn_l[0] * y_yhsn_l_4 + tpn_l[0] * y_yhsn_l_5 + ecnian_l[0] * y_yhsn_l_6 + yhibn_l[0] * y_yhsn_l_7 + y_yhsn_l_8 * uyhsn[x] + xgdptn_l[0] * y_yhsn_l_9 + + yht_l[0] = yhtn_l[0] - pcnia_l[0] + + yhtgap[0] = y_yhtgap_1 * yhtshr_l[0] + y_yhtgap_2 * zyhtst_l[0] + + yhtn_l[0] = gtn_l[0] + y_yhtn_l_2 * uyhtn[x] + + yhtshr_l[0] = yht_l[0] - yh_l[0] + + ykbfin_l[0] = pxb_l[0] + rtbfi_l[0] + kbfi_l[0] * y_ykbfin_l_2 + kbfi_l[-1] * y_ykbfin_l_3 + + ykin_l[0] = pxb_l[0] + rtinv[0] * y_ykin_l_2 + ki_l[0] * y_ykin_l_3 + ki_l[-1] * y_ykin_l_4 + + ynicpn_l[0] = y_ynicpn_l_2 * ynin_l[0] + yniln_l[0] * y_ynicpn_l_3 + ynirn_l[0] * y_ynicpn_l_4 + uynicpnr[0] * y_ynicpn_l_5 + xgdpn_l[0] * y_ynicpn_l_6 + + ynidn_l[0] = y_ynidn_l_3 * AUX_EXO_LAG_390_0[-1] + y_ynidn_l_2 * ynidn_l[-1] + y_ynidn_l_1 * ymsdn[x] + pxb_l[0] + pxb_l[-1] * y_ynidn_l_4 + ynidn_l_aerr[x] + y_ynidn_l_5 * qynidn_l[-1] + y_ynidn_l_6 * AUX_ENDO_LAG_244_1[-1] + zynid[0] + y_ynidn_l_8 * AUX_ENDO_LAG_135_1[-1] + y_ynidn_l_7 * AUX_EXO_LAG_390_1[-1] + + yniln_l[0] = y_yniln_l_2 * uyl[x] + pl_l[0] * y_yniln_l_3 + lhp_l[0] * y_yniln_l_4 + pgfl_l[0] * y_yniln_l_5 + egfl_l[0] * y_yniln_l_6 + pgsl_l[0] * y_yniln_l_7 + egsl_l[0] * y_yniln_l_8 + + ynin_l[0] = y_ynin_l_2 * uyni[x] + xgdin_l[0] * y_ynin_l_3 + fynicn_l[0] * y_ynin_l_4 + fyniln_l[0] * y_ynin_l_5 + jccan_l[0] * y_ynin_l_6 + + ynirn_l[0] = xgdpn_l[0] + y_ynirn_l_1 * ynirn_l_aerr[x] + y_ynirn_l_2 * ynirn_l[-1] + xgdpn_l[-1] * y_ynirn_l_3 + rbbb[0] * y_ynirn_l_4 + y_ynirn_l_5 * rbbb[-1] + + ypn_l[0] = y_ypn_l_2 * uyp[x] + yhln_l[0] * y_ypn_l_3 + yhtn_l[0] * y_ypn_l_4 + yhptn_l[0] * y_ypn_l_5 + + zdivgr[0] = y_zdivgr_1 * hgynid[1] + y_zdivgr_2 * zdivgr[1] + + zebfi[0] = hgpbfir[-1] * y_zebfi_21 + y_zebfi_20 * hxbt[-1] + qebfi_l[-1] * y_zebfi_15 + y_zebfi_11 * xgap[-1] + ptr[-1] * y_zebfi_10 + y_zebfi_9 * rtr[-1] + rff[-1] * y_zebfi_5 + picnia[-1] * y_zebfi_1 + y_zebfi_2 * AUX_ENDO_LAG_120_1[-1] + y_zebfi_3 * AUX_ENDO_LAG_120_2[-1] + y_zebfi_4 * AUX_ENDO_LAG_120_3[-1] + y_zebfi_6 * AUX_ENDO_LAG_164_1[-1] + y_zebfi_7 * AUX_ENDO_LAG_164_2[-1] + y_zebfi_8 * AUX_ENDO_LAG_164_3[-1] + y_zebfi_12 * AUX_ENDO_LAG_212_1[-1] + y_zebfi_13 * AUX_ENDO_LAG_212_2[-1] + y_zebfi_14 * AUX_ENDO_LAG_212_3[-1] + y_zebfi_16 * AUX_ENDO_LAG_139_1[-1] + y_zebfi_17 * AUX_ENDO_LAG_139_2[-1] + y_zebfi_18 * AUX_ENDO_LAG_139_3[-1] + y_zebfi_19 * AUX_ENDO_LAG_139_4[-1] + + zecd[0] = picnia[-1] * y_zecd_1 + rff[-1] * y_zecd_5 + xgap2[-1] * y_zecd_9 + ptr[-1] * y_zecd_13 + rtr[-1] * y_zecd_14 + y_zecd_15 * yhgap[-1] + y_zecd_19 * yhtgap[-1] + y_zecd_23 * yhpgap[-1] + hggdpt[-1] * y_zecd_27 + qecd_l[-1] * y_zecd_29 + y_zecd_33 * AUX_ENDO_LAG_141_4[-1] + y_zecd_32 * AUX_ENDO_LAG_141_3[-1] + y_zecd_31 * AUX_ENDO_LAG_141_2[-1] + y_zecd_30 * AUX_ENDO_LAG_141_1[-1] + y_zecd_2 * AUX_ENDO_LAG_120_1[-1] + y_zecd_3 * AUX_ENDO_LAG_120_2[-1] + y_zecd_4 * AUX_ENDO_LAG_120_3[-1] + y_zecd_6 * AUX_ENDO_LAG_164_1[-1] + y_zecd_7 * AUX_ENDO_LAG_164_2[-1] + y_zecd_8 * AUX_ENDO_LAG_164_3[-1] + y_zecd_10 * AUX_ENDO_LAG_213_1[-1] + y_zecd_11 * AUX_ENDO_LAG_213_2[-1] + y_zecd_12 * AUX_ENDO_LAG_213_3[-1] + y_zecd_16 * AUX_ENDO_LAG_225_1[-1] + y_zecd_17 * AUX_ENDO_LAG_225_2[-1] + y_zecd_18 * AUX_ENDO_LAG_225_3[-1] + y_zecd_20 * AUX_ENDO_LAG_238_1[-1] + y_zecd_21 * AUX_ENDO_LAG_238_2[-1] + y_zecd_22 * AUX_ENDO_LAG_238_3[-1] + y_zecd_24 * AUX_ENDO_LAG_231_1[-1] + y_zecd_25 * AUX_ENDO_LAG_231_2[-1] + y_zecd_26 * AUX_ENDO_LAG_231_3[-1] + y_zecd_28 * AUX_EXO_LAG_309_0[-1] + + zeco[0] = qeco_l[-1] * y_zeco_28 + hggdpt[-1] * y_zeco_27 + yhpgap[-1] * y_zeco_23 + yhtgap[-1] * y_zeco_19 + yhgap[-1] * y_zeco_15 + rtr[-1] * y_zeco_14 + ptr[-1] * y_zeco_13 + xgap2[-1] * y_zeco_9 + rff[-1] * y_zeco_5 + picnia[-1] * y_zeco_1 + y_zeco_2 * AUX_ENDO_LAG_120_1[-1] + y_zeco_3 * AUX_ENDO_LAG_120_2[-1] + y_zeco_4 * AUX_ENDO_LAG_120_3[-1] + y_zeco_6 * AUX_ENDO_LAG_164_1[-1] + y_zeco_7 * AUX_ENDO_LAG_164_2[-1] + y_zeco_8 * AUX_ENDO_LAG_164_3[-1] + y_zeco_10 * AUX_ENDO_LAG_213_1[-1] + y_zeco_11 * AUX_ENDO_LAG_213_2[-1] + y_zeco_12 * AUX_ENDO_LAG_213_3[-1] + y_zeco_16 * AUX_ENDO_LAG_225_1[-1] + y_zeco_17 * AUX_ENDO_LAG_225_2[-1] + y_zeco_18 * AUX_ENDO_LAG_225_3[-1] + y_zeco_20 * AUX_ENDO_LAG_238_1[-1] + y_zeco_21 * AUX_ENDO_LAG_238_2[-1] + y_zeco_22 * AUX_ENDO_LAG_238_3[-1] + y_zeco_24 * AUX_ENDO_LAG_231_1[-1] + y_zeco_25 * AUX_ENDO_LAG_231_2[-1] + y_zeco_26 * AUX_ENDO_LAG_231_3[-1] + y_zeco_29 * AUX_ENDO_LAG_142_1[-1] + y_zeco_30 * AUX_ENDO_LAG_142_2[-1] + y_zeco_31 * AUX_ENDO_LAG_142_3[-1] + y_zeco_32 * AUX_ENDO_LAG_142_4[-1] + + zeh[0] = qeh_l[-1] * y_zeh_28 + hggdpt[-1] * y_zeh_27 + yhpgap[-1] * y_zeh_23 + yhtgap[-1] * y_zeh_19 + yhgap[-1] * y_zeh_15 + rtr[-1] * y_zeh_14 + ptr[-1] * y_zeh_13 + xgap2[-1] * y_zeh_9 + rff[-1] * y_zeh_5 + picnia[-1] * y_zeh_1 + y_zeh_2 * AUX_ENDO_LAG_120_1[-1] + y_zeh_3 * AUX_ENDO_LAG_120_2[-1] + y_zeh_4 * AUX_ENDO_LAG_120_3[-1] + y_zeh_6 * AUX_ENDO_LAG_164_1[-1] + y_zeh_7 * AUX_ENDO_LAG_164_2[-1] + y_zeh_8 * AUX_ENDO_LAG_164_3[-1] + y_zeh_10 * AUX_ENDO_LAG_213_1[-1] + y_zeh_11 * AUX_ENDO_LAG_213_2[-1] + y_zeh_12 * AUX_ENDO_LAG_213_3[-1] + y_zeh_16 * AUX_ENDO_LAG_225_1[-1] + y_zeh_17 * AUX_ENDO_LAG_225_2[-1] + y_zeh_18 * AUX_ENDO_LAG_225_3[-1] + y_zeh_20 * AUX_ENDO_LAG_238_1[-1] + y_zeh_21 * AUX_ENDO_LAG_238_2[-1] + y_zeh_22 * AUX_ENDO_LAG_238_3[-1] + y_zeh_24 * AUX_ENDO_LAG_231_1[-1] + y_zeh_25 * AUX_ENDO_LAG_231_2[-1] + y_zeh_26 * AUX_ENDO_LAG_231_3[-1] + y_zeh_29 * AUX_ENDO_LAG_143_1[-1] + y_zeh_30 * AUX_ENDO_LAG_143_2[-1] + y_zeh_31 * AUX_ENDO_LAG_143_3[-1] + y_zeh_32 * AUX_ENDO_LAG_143_4[-1] + + zgap05[0] = xgap[0] * y_zgap05_1 + y_zgap05_2 * zgap05[1] + + zgap10[0] = xgap[0] * y_zgap10_1 + y_zgap10_2 * zgap10[1] + + zgap30[0] = xgap[0] * y_zgap30_1 + y_zgap30_2 * zgap30[1] + + zgapc2[0] = rtr[-1] * y_zgapc2_14 + ptr[-1] * y_zgapc2_13 + xgap2[-1] * y_zgapc2_9 + rff[-1] * y_zgapc2_5 + picnia[-1] * y_zgapc2_1 + y_zgapc2_2 * AUX_ENDO_LAG_120_1[-1] + y_zgapc2_3 * AUX_ENDO_LAG_120_2[-1] + y_zgapc2_4 * AUX_ENDO_LAG_120_3[-1] + y_zgapc2_6 * AUX_ENDO_LAG_164_1[-1] + y_zgapc2_7 * AUX_ENDO_LAG_164_2[-1] + y_zgapc2_8 * AUX_ENDO_LAG_164_3[-1] + y_zgapc2_10 * AUX_ENDO_LAG_213_1[-1] + y_zgapc2_11 * AUX_ENDO_LAG_213_2[-1] + y_zgapc2_12 * AUX_ENDO_LAG_213_3[-1] + + zlhp[0] = hqlww[-1] * y_zlhp_20 + y_zlhp_19 * hlept[-1] + y_zlhp_17 * lprdt_l[-1] + xbo_l[-1] * y_zlhp_15 + xgap[-1] * y_zlhp_11 + ptr[-1] * y_zlhp_10 + rtr[-1] * y_zlhp_9 + rff[-1] * y_zlhp_5 + picnia[-1] * y_zlhp_1 + y_zlhp_2 * AUX_ENDO_LAG_120_1[-1] + y_zlhp_3 * AUX_ENDO_LAG_120_2[-1] + y_zlhp_4 * AUX_ENDO_LAG_120_3[-1] + y_zlhp_6 * AUX_ENDO_LAG_164_1[-1] + y_zlhp_7 * AUX_ENDO_LAG_164_2[-1] + y_zlhp_8 * AUX_ENDO_LAG_164_3[-1] + y_zlhp_12 * AUX_ENDO_LAG_212_1[-1] + y_zlhp_13 * AUX_ENDO_LAG_212_2[-1] + y_zlhp_14 * AUX_ENDO_LAG_212_3[-1] + y_zlhp_16 * AUX_ENDO_LAG_207_1[-1] + y_zlhp_18 * AUX_ENDO_LAG_97_1[-1] + + zpi10[0] = picnia[0] * y_zpi10_1 + y_zpi10_2 * zpi10[1] + + zpi10f[0] = picnia[0] * y_zpi10f_1 + y_zpi10f_2 * zpi10f[1] + + zpi5[0] = xgap[-1] * y_zpi5_11 + ptr[-1] * y_zpi5_10 + rtr[-1] * y_zpi5_9 + rff[-1] * y_zpi5_5 + picnia[-1] * y_zpi5_1 + y_zpi5_2 * AUX_ENDO_LAG_120_1[-1] + y_zpi5_3 * AUX_ENDO_LAG_120_2[-1] + y_zpi5_4 * AUX_ENDO_LAG_120_3[-1] + y_zpi5_6 * AUX_ENDO_LAG_164_1[-1] + y_zpi5_7 * AUX_ENDO_LAG_164_2[-1] + y_zpi5_8 * AUX_ENDO_LAG_164_3[-1] + y_zpi5_12 * AUX_ENDO_LAG_212_1[-1] + y_zpi5_13 * AUX_ENDO_LAG_212_2[-1] + y_zpi5_14 * AUX_ENDO_LAG_212_3[-1] + + zpib5[0] = pxb_l[0] * y_zpib5_1 + pxb_l[-1] * y_zpib5_2 + y_zpib5_3 * zpib5[1] + + zpic30[0] = picnia[0] * y_zpic30_1 + y_zpic30_2 * zpic30[1] + + zpic58[0] = AUX_ENDO_LEAD_3940[1] + + zpicxfe[0] = lurnat[-1] * y_zpicxfe_26 + lur[-1] * y_zpicxfe_25 + huqpct[-1] * y_zpicxfe_24 + hlprdt[-1] * y_zpicxfe_23 + pl_l[-1] * y_zpicxfe_22 + qpl_l[-1] * y_zpicxfe_21 + pcnia_l[-1] * y_zpicxfe_20 + qpcnia_l[-1] * y_zpicxfe_19 + ptr[-1] * y_zpicxfe_18 + rtr[-1] * y_zpicxfe_17 + xgap2[-1] * y_zpicxfe_13 + rff[-1] * y_zpicxfe_9 + pieci[-1] * y_zpicxfe_5 + picxfe[-1] * y_zpicxfe_1 + y_zpicxfe_2 * AUX_ENDO_LAG_122_1[-1] + y_zpicxfe_3 * AUX_ENDO_LAG_122_2[-1] + y_zpicxfe_4 * AUX_ENDO_LAG_122_3[-1] + y_zpicxfe_6 * AUX_ENDO_LAG_123_1[-1] + y_zpicxfe_7 * AUX_ENDO_LAG_123_2[-1] + y_zpicxfe_8 * AUX_ENDO_LAG_123_3[-1] + y_zpicxfe_10 * AUX_ENDO_LAG_164_1[-1] + y_zpicxfe_11 * AUX_ENDO_LAG_164_2[-1] + y_zpicxfe_12 * AUX_ENDO_LAG_164_3[-1] + y_zpicxfe_14 * AUX_ENDO_LAG_213_1[-1] + y_zpicxfe_15 * AUX_ENDO_LAG_213_2[-1] + y_zpicxfe_16 * AUX_ENDO_LAG_213_3[-1] + y_zpicxfe_27 * AUX_ENDO_LAG_98_1[-1] + y_zpicxfe_28 * AUX_ENDO_LAG_99_1[-1] + + zpieci[0] = lurnat[-1] * y_zpieci_26 + lur[-1] * y_zpieci_25 + huqpct[-1] * y_zpieci_24 + hlprdt[-1] * y_zpieci_23 + pl_l[-1] * y_zpieci_22 + qpl_l[-1] * y_zpieci_21 + pcnia_l[-1] * y_zpieci_20 + qpcnia_l[-1] * y_zpieci_19 + ptr[-1] * y_zpieci_18 + rtr[-1] * y_zpieci_17 + xgap2[-1] * y_zpieci_13 + rff[-1] * y_zpieci_9 + pieci[-1] * y_zpieci_5 + picxfe[-1] * y_zpieci_1 + y_zpieci_2 * AUX_ENDO_LAG_122_1[-1] + y_zpieci_3 * AUX_ENDO_LAG_122_2[-1] + y_zpieci_4 * AUX_ENDO_LAG_122_3[-1] + y_zpieci_6 * AUX_ENDO_LAG_123_1[-1] + y_zpieci_7 * AUX_ENDO_LAG_123_2[-1] + y_zpieci_8 * AUX_ENDO_LAG_123_3[-1] + y_zpieci_10 * AUX_ENDO_LAG_164_1[-1] + y_zpieci_11 * AUX_ENDO_LAG_164_2[-1] + y_zpieci_12 * AUX_ENDO_LAG_164_3[-1] + y_zpieci_14 * AUX_ENDO_LAG_213_1[-1] + y_zpieci_15 * AUX_ENDO_LAG_213_2[-1] + y_zpieci_16 * AUX_ENDO_LAG_213_3[-1] + y_zpieci_27 * AUX_ENDO_LAG_98_1[-1] + y_zpieci_28 * AUX_ENDO_LAG_99_1[-1] + + zrff10[0] = rff[0] * y_zrff10_1 + y_zrff10_2 * zrff10[1] + + zrff30[0] = rff[0] * y_zrff30_1 + y_zrff30_2 * zrff30[1] + + zrff5[0] = rff[0] * y_zrff5_1 + y_zrff5_2 * zrff5[1] + + zyh_l[0] = xgdpt_l[0] + zyhst_l[0] + yhgap[-1] * y_zyh_l_16 + yhgap[0] * y_zyh_l_15 + rtr[0] * y_zyh_l_14 + ptr[0] * y_zyh_l_13 + xgap2[-1] * y_zyh_l_10 + xgap2[0] * y_zyh_l_9 + rff[-1] * y_zyh_l_6 + rff[0] * y_zyh_l_5 + picnia[0] * y_zyh_l_1 + picnia[-1] * y_zyh_l_2 + y_zyh_l_3 * AUX_ENDO_LAG_120_1[-1] + y_zyh_l_4 * AUX_ENDO_LAG_120_2[-1] + y_zyh_l_7 * AUX_ENDO_LAG_164_1[-1] + y_zyh_l_8 * AUX_ENDO_LAG_164_2[-1] + y_zyh_l_11 * AUX_ENDO_LAG_213_1[-1] + y_zyh_l_12 * AUX_ENDO_LAG_213_2[-1] + y_zyh_l_17 * AUX_ENDO_LAG_225_1[-1] + y_zyh_l_18 * AUX_ENDO_LAG_225_2[-1] + + zyhp_l[0] = zyhpst_l[0] + xgdpt_l[0] + zyhst_l[0] + yhpgap[-1] * y_zyhp_l_20 + yhpgap[0] * y_zyhp_l_19 + yhgap[-1] * y_zyhp_l_16 + yhgap[0] * y_zyhp_l_15 + rtr[0] * y_zyhp_l_14 + ptr[0] * y_zyhp_l_13 + xgap2[-1] * y_zyhp_l_10 + xgap2[0] * y_zyhp_l_9 + rff[-1] * y_zyhp_l_6 + rff[0] * y_zyhp_l_5 + picnia[0] * y_zyhp_l_1 + picnia[-1] * y_zyhp_l_2 + y_zyhp_l_3 * AUX_ENDO_LAG_120_1[-1] + y_zyhp_l_4 * AUX_ENDO_LAG_120_2[-1] + y_zyhp_l_7 * AUX_ENDO_LAG_164_1[-1] + y_zyhp_l_8 * AUX_ENDO_LAG_164_2[-1] + y_zyhp_l_11 * AUX_ENDO_LAG_213_1[-1] + y_zyhp_l_12 * AUX_ENDO_LAG_213_2[-1] + y_zyhp_l_17 * AUX_ENDO_LAG_225_1[-1] + y_zyhp_l_18 * AUX_ENDO_LAG_225_2[-1] + y_zyhp_l_21 * AUX_ENDO_LAG_231_1[-1] + y_zyhp_l_22 * AUX_ENDO_LAG_231_2[-1] + + zyhpst_l[0] = zyhpst_l[-1] + yhpgap[-1] * y_zyhpst_l_1 + + zyhst_l[0] = zyhst_l[-1] + yhgap[-1] * y_zyhst_l_1 + + zyht_l[0] = zyhtst_l[0] + xgdpt_l[0] + zyhst_l[0] + yhtgap[-1] * y_zyht_l_20 + yhtgap[0] * y_zyht_l_19 + yhgap[-1] * y_zyht_l_16 + yhgap[0] * y_zyht_l_15 + rtr[0] * y_zyht_l_14 + ptr[0] * y_zyht_l_13 + xgap2[-1] * y_zyht_l_10 + xgap2[0] * y_zyht_l_9 + rff[-1] * y_zyht_l_6 + rff[0] * y_zyht_l_5 + picnia[0] * y_zyht_l_1 + picnia[-1] * y_zyht_l_2 + y_zyht_l_3 * AUX_ENDO_LAG_120_1[-1] + y_zyht_l_4 * AUX_ENDO_LAG_120_2[-1] + y_zyht_l_7 * AUX_ENDO_LAG_164_1[-1] + y_zyht_l_8 * AUX_ENDO_LAG_164_2[-1] + y_zyht_l_11 * AUX_ENDO_LAG_213_1[-1] + y_zyht_l_12 * AUX_ENDO_LAG_213_2[-1] + y_zyht_l_17 * AUX_ENDO_LAG_225_1[-1] + y_zyht_l_18 * AUX_ENDO_LAG_225_2[-1] + y_zyht_l_21 * AUX_ENDO_LAG_238_1[-1] + y_zyht_l_22 * AUX_ENDO_LAG_238_2[-1] + + zyhtst_l[0] = zyhtst_l[-1] + yhtgap[-1] * y_zyhtst_l_1 + + zynid[0] = hggdpt[-1] * y_zynid_25 + pxb_l[-1] * y_zynid_16 + qynidn_l[-1] * y_zynid_15 + xgap[-1] * y_zynid_11 + ptr[-1] * y_zynid_10 + rtr[-1] * y_zynid_9 + rff[-1] * y_zynid_5 + picnia[-1] * y_zynid_1 + y_zynid_2 * AUX_ENDO_LAG_120_1[-1] + y_zynid_3 * AUX_ENDO_LAG_120_2[-1] + y_zynid_4 * AUX_ENDO_LAG_120_3[-1] + y_zynid_6 * AUX_ENDO_LAG_164_1[-1] + y_zynid_7 * AUX_ENDO_LAG_164_2[-1] + y_zynid_8 * AUX_ENDO_LAG_164_3[-1] + y_zynid_12 * AUX_ENDO_LAG_212_1[-1] + y_zynid_13 * AUX_ENDO_LAG_212_2[-1] + y_zynid_14 * AUX_ENDO_LAG_212_3[-1] + y_zynid_17 * AUX_ENDO_LAG_154_1[-1] + y_zynid_18 * AUX_ENDO_LAG_135_1[-1] + y_zynid_19 * AUX_ENDO_LAG_154_2[-1] + y_zynid_20 * AUX_ENDO_LAG_135_2[-1] + y_zynid_21 * AUX_ENDO_LAG_154_3[-1] + y_zynid_22 * AUX_ENDO_LAG_135_3[-1] + y_zynid_23 * AUX_ENDO_LAG_154_4[-1] + y_zynid_24 * AUX_ENDO_LAG_135_4[-1] + + ugap[0] = lur[0] - lurnat[0] + + rff[0] = rule[0] + eradd[x] + + rule[0] = rff[-1] * 0.85 + rstar * 0.15 + picx4[0] * 0.225 - 0.075 * pitarg[x] + xgap2[0] * 0.15 + + fiscal[0] = (1 - rho_fiscal) * fbar_iscal + rho_fiscal * fiscal[-1] + fiscal_aerr[x] + + fiscalav[0] = fiscal[0] * av + fiscalav[-1] * rho_fiscalav + + gov_exp_share[0] = egfe_l[0] * y_xfs_l_7 * 100 + + income_tax_share_of_gdp[0] = tryh[0] * 100 * (y_yh_l_2 * (( - y_yhl_l_2) - 1) + y_yh_l_4 * (( - y_yhp_l_2) - 1)) + + debt_to_gdp[0] = 100 * y_gfrecn_l_5 * y_gfrecn_l_4 * y_gfdbtnp_l_4 * ( - gfdbtnp_l[0]) + + AUX_ENDO_LEAD_4485[0] = pic4[1] + + AUX_ENDO_LEAD_4489[0] = AUX_ENDO_LEAD_4485[1] + + AUX_ENDO_LEAD_4493[0] = AUX_ENDO_LEAD_4489[1] + + AUX_ENDO_LEAD_4497[0] = AUX_ENDO_LEAD_4493[1] + + AUX_ENDO_LEAD_4501[0] = AUX_ENDO_LEAD_4497[1] + + AUX_ENDO_LEAD_4505[0] = AUX_ENDO_LEAD_4501[1] + + AUX_ENDO_LEAD_3940[0] = AUX_ENDO_LEAD_4505[1] + + AUX_ENDO_LAG_4_1[0] = ebfi_l[-1] + + AUX_ENDO_LAG_4_2[0] = AUX_ENDO_LAG_4_1[-1] + + AUX_ENDO_LAG_205_1[0] = xb_l[-1] + + AUX_ENDO_LAG_6_1[0] = ecd_l[-1] + + AUX_ENDO_LAG_86_1[0] = kh_l[-1] + + AUX_ENDO_LAG_7_1[0] = ech_l[-1] + + AUX_ENDO_LAG_86_2[0] = AUX_ENDO_LAG_86_1[-1] + + AUX_ENDO_LAG_10_1[0] = eco_l[-1] + + AUX_ENDO_LAG_11_1[0] = egfe_l[-1] + + AUX_ENDO_LAG_11_2[0] = AUX_ENDO_LAG_11_1[-1] + + AUX_ENDO_LAG_68_1[0] = hggdpt[-1] + + AUX_ENDO_LAG_68_2[0] = AUX_ENDO_LAG_68_1[-1] + + AUX_ENDO_LAG_14_1[0] = egfl_l[-1] + + AUX_ENDO_LAG_14_2[0] = AUX_ENDO_LAG_14_1[-1] + + AUX_ENDO_LAG_17_1[0] = egse_l[-1] + + AUX_ENDO_LAG_17_2[0] = AUX_ENDO_LAG_17_1[-1] + + AUX_ENDO_LAG_20_1[0] = egsl_l[-1] + + AUX_ENDO_LAG_20_2[0] = AUX_ENDO_LAG_20_1[-1] + + AUX_ENDO_LAG_23_1[0] = eh_l[-1] + + AUX_ENDO_LAG_23_2[0] = AUX_ENDO_LAG_23_1[-1] + + AUX_ENDO_LAG_175_1[0] = rme[-1] + + AUX_ENDO_LAG_213_1[0] = xgap2[-1] + + AUX_ENDO_LAG_53_1[0] = fxgap[-1] + + AUX_ENDO_LAG_42_1[0] = fpi10[-1] + + AUX_ENDO_LAG_42_2[0] = AUX_ENDO_LAG_42_1[-1] + + AUX_ENDO_LAG_42_3[0] = AUX_ENDO_LAG_42_2[-1] + + AUX_ENDO_LAG_47_1[0] = fpxrr_l[-1] + + AUX_ENDO_LAG_49_1[0] = frl10[-1] + + AUX_ENDO_LAG_50_1[0] = frs10[-1] + + AUX_ENDO_LAG_42_4[0] = AUX_ENDO_LAG_42_3[-1] + + AUX_ENDO_LAG_50_2[0] = AUX_ENDO_LAG_50_1[-1] + + AUX_ENDO_LAG_42_5[0] = AUX_ENDO_LAG_42_4[-1] + + AUX_ENDO_LAG_213_2[0] = AUX_ENDO_LAG_213_1[-1] + + AUX_ENDO_LAG_213_3[0] = AUX_ENDO_LAG_213_2[-1] + + AUX_ENDO_LAG_213_4[0] = AUX_ENDO_LAG_213_3[-1] + + AUX_ENDO_LAG_87_1[0] = ki_l[-1] + + AUX_ENDO_LAG_210_1[0] = xfs_l[-1] + + AUX_ENDO_LAG_210_2[0] = AUX_ENDO_LAG_210_1[-1] + + AUX_ENDO_LAG_96_1[0] = lhp_l[-1] + + AUX_ENDO_LAG_207_1[0] = xbo_l[-1] + + AUX_ENDO_LAG_74_1[0] = hlprdt[-1] + + AUX_ENDO_LAG_103_1[0] = pcdr_l[-1] + + AUX_ENDO_LAG_105_1[0] = pcfr_l[-1] + + AUX_ENDO_LAG_105_2[0] = AUX_ENDO_LAG_105_1[-1] + + AUX_ENDO_LAG_105_3[0] = AUX_ENDO_LAG_105_2[-1] + + AUX_ENDO_LAG_106_1[0] = pchr_l[-1] + + AUX_ENDO_LAG_117_1[0] = phouse_l[-1] + + AUX_ENDO_LAG_107_1[0] = pcnia_l[-1] + + AUX_ENDO_LAG_107_2[0] = AUX_ENDO_LAG_107_1[-1] + + AUX_ENDO_LAG_107_3[0] = AUX_ENDO_LAG_107_2[-1] + + AUX_ENDO_LAG_111_1[0] = pcxfe_l[-1] + + AUX_ENDO_LAG_111_2[0] = AUX_ENDO_LAG_111_1[-1] + + AUX_ENDO_LAG_111_3[0] = AUX_ENDO_LAG_111_2[-1] + + AUX_ENDO_LAG_123_1[0] = pieci[-1] + + AUX_ENDO_LAG_123_2[0] = AUX_ENDO_LAG_123_1[-1] + + AUX_ENDO_LAG_123_3[0] = AUX_ENDO_LAG_123_2[-1] + + AUX_ENDO_LAG_126_1[0] = pipxnc[-1] + + AUX_ENDO_LAG_120_1[0] = picnia[-1] + + AUX_ENDO_LAG_78_1[0] = huqpct[-1] + + AUX_ENDO_LAG_133_1[0] = poilr_l[-1] + + AUX_ENDO_LAG_68_3[0] = AUX_ENDO_LAG_68_2[-1] + + AUX_ENDO_LAG_68_4[0] = AUX_ENDO_LAG_68_3[-1] + + AUX_ENDO_LAG_68_5[0] = AUX_ENDO_LAG_68_4[-1] + + AUX_ENDO_LAG_68_6[0] = AUX_ENDO_LAG_68_5[-1] + + AUX_ENDO_LAG_68_7[0] = AUX_ENDO_LAG_68_6[-1] + + AUX_ENDO_LAG_68_8[0] = AUX_ENDO_LAG_68_7[-1] + + AUX_ENDO_LAG_68_9[0] = AUX_ENDO_LAG_68_8[-1] + + AUX_ENDO_LAG_68_10[0] = AUX_ENDO_LAG_68_9[-1] + + AUX_ENDO_LAG_68_11[0] = AUX_ENDO_LAG_68_10[-1] + + AUX_ENDO_LAG_68_12[0] = AUX_ENDO_LAG_68_11[-1] + + AUX_ENDO_LAG_68_13[0] = AUX_ENDO_LAG_68_12[-1] + + AUX_ENDO_LAG_68_14[0] = AUX_ENDO_LAG_68_13[-1] + + AUX_ENDO_LAG_165_1[0] = rfynic[-1] + + AUX_ENDO_LAG_166_1[0] = rfynil[-1] + + AUX_ENDO_LAG_56_1[0] = gfdbtn_l[-1] + + AUX_ENDO_LAG_122_1[0] = picxfe[-1] + + AUX_ENDO_LAG_122_2[0] = AUX_ENDO_LAG_122_1[-1] + + AUX_ENDO_LAG_179_1[0] = rtb[-1] + + AUX_ENDO_LAG_187_1[0] = trp[-1] + + AUX_ENDO_LAG_189_1[0] = trpt[-1] + + AUX_ENDO_LAG_218_1[0] = xgdpn_l[-1] + + AUX_ENDO_LAG_135_1[0] = pxb_l[-1] + + AUX_ENDO_LAG_244_1[0] = ynidn_l[-1] + + AUX_ENDO_LAG_120_2[0] = AUX_ENDO_LAG_120_1[-1] + + AUX_ENDO_LAG_120_3[0] = AUX_ENDO_LAG_120_2[-1] + + AUX_ENDO_LAG_164_1[0] = rff[-1] + + AUX_ENDO_LAG_164_2[0] = AUX_ENDO_LAG_164_1[-1] + + AUX_ENDO_LAG_164_3[0] = AUX_ENDO_LAG_164_2[-1] + + AUX_ENDO_LAG_212_1[0] = xgap[-1] + + AUX_ENDO_LAG_212_2[0] = AUX_ENDO_LAG_212_1[-1] + + AUX_ENDO_LAG_212_3[0] = AUX_ENDO_LAG_212_2[-1] + + AUX_ENDO_LAG_139_1[0] = qebfi_l[-1] + + AUX_ENDO_LAG_139_2[0] = AUX_ENDO_LAG_139_1[-1] + + AUX_ENDO_LAG_139_3[0] = AUX_ENDO_LAG_139_2[-1] + + AUX_ENDO_LAG_139_4[0] = AUX_ENDO_LAG_139_3[-1] + + AUX_ENDO_LAG_141_1[0] = qecd_l[-1] + + AUX_ENDO_LAG_141_2[0] = AUX_ENDO_LAG_141_1[-1] + + AUX_ENDO_LAG_141_3[0] = AUX_ENDO_LAG_141_2[-1] + + AUX_ENDO_LAG_141_4[0] = AUX_ENDO_LAG_141_3[-1] + + AUX_ENDO_LAG_225_1[0] = yhgap[-1] + + AUX_ENDO_LAG_225_2[0] = AUX_ENDO_LAG_225_1[-1] + + AUX_ENDO_LAG_225_3[0] = AUX_ENDO_LAG_225_2[-1] + + AUX_ENDO_LAG_238_1[0] = yhtgap[-1] + + AUX_ENDO_LAG_238_2[0] = AUX_ENDO_LAG_238_1[-1] + + AUX_ENDO_LAG_238_3[0] = AUX_ENDO_LAG_238_2[-1] + + AUX_ENDO_LAG_231_1[0] = yhpgap[-1] + + AUX_ENDO_LAG_231_2[0] = AUX_ENDO_LAG_231_1[-1] + + AUX_ENDO_LAG_231_3[0] = AUX_ENDO_LAG_231_2[-1] + + AUX_ENDO_LAG_142_1[0] = qeco_l[-1] + + AUX_ENDO_LAG_142_2[0] = AUX_ENDO_LAG_142_1[-1] + + AUX_ENDO_LAG_142_3[0] = AUX_ENDO_LAG_142_2[-1] + + AUX_ENDO_LAG_142_4[0] = AUX_ENDO_LAG_142_3[-1] + + AUX_ENDO_LAG_143_1[0] = qeh_l[-1] + + AUX_ENDO_LAG_143_2[0] = AUX_ENDO_LAG_143_1[-1] + + AUX_ENDO_LAG_143_3[0] = AUX_ENDO_LAG_143_2[-1] + + AUX_ENDO_LAG_143_4[0] = AUX_ENDO_LAG_143_3[-1] + + AUX_ENDO_LAG_97_1[0] = lprdt_l[-1] + + AUX_ENDO_LAG_122_3[0] = AUX_ENDO_LAG_122_2[-1] + + AUX_ENDO_LAG_98_1[0] = lur[-1] + + AUX_ENDO_LAG_99_1[0] = lurnat[-1] + + AUX_ENDO_LAG_154_1[0] = qynidn_l[-1] + + AUX_ENDO_LAG_154_2[0] = AUX_ENDO_LAG_154_1[-1] + + AUX_ENDO_LAG_135_2[0] = AUX_ENDO_LAG_135_1[-1] + + AUX_ENDO_LAG_154_3[0] = AUX_ENDO_LAG_154_2[-1] + + AUX_ENDO_LAG_135_3[0] = AUX_ENDO_LAG_135_2[-1] + + AUX_ENDO_LAG_154_4[0] = AUX_ENDO_LAG_154_3[-1] + + AUX_ENDO_LAG_135_4[0] = AUX_ENDO_LAG_135_3[-1] + + AUX_EXO_LAG_367_0[0] = uemot[x] + + AUX_EXO_LAG_282_0[0] = ddockm[x] + + AUX_EXO_LAG_303_0[0] = fpxrrt[x] + + AUX_EXO_LAG_337_0[0] = pkir[x] + + AUX_EXO_LAG_325_0[0] = n16_l[x] + + AUX_EXO_LAG_321_0[0] = lqualt_l[x] + + AUX_EXO_LAG_343_0[0] = qleor[x] + + AUX_EXO_LAG_329_0[0] = pcfrt[x] + + AUX_EXO_LAG_340_0[0] = poilrt[x] + + AUX_EXO_LAG_336_0[0] = pitarg[x] + + AUX_EXO_LAG_279_0[0] = d8095[x] + + AUX_EXO_LAG_361_0[0] = trcit[x] + + AUX_EXO_LAG_305_0[0] = gfdrt[x] + + AUX_EXO_LAG_305_1[0] = AUX_EXO_LAG_305_0[-1] + + AUX_EXO_LAG_390_0[0] = ymsdn[x] + + AUX_EXO_LAG_390_1[0] = AUX_EXO_LAG_390_0[-1] + + AUX_EXO_LAG_309_0[0] = hgpcdr[x] + +end + + +@parameters FRBUS begin + mep_l = 0.0 + + mei_l = 0.0 + + qpmo_l = 0.0 + + rstar = 0.0 + + rho_qkir_l = 0.8 + + y_dpgap_1 = 0.0025 + + y_dpgap_2 = (-0.103649883938) + + y_dpgap_3 = 0.103649883938 + + y_dpgap_4 = (-0.341041547027) + + y_dpgap_5 = 0.341041547027 + + y_dpgap_6 = (-0.121366054939) + + y_dpgap_7 = 0.121366054939 + + y_dpgap_8 = (-0.104958882473) + + y_dpgap_9 = 0.104958882473 + + y_dpgap_10 = (-0.328983631622) + + y_dpgap_11 = 0.328983631622 + + y_ebfi_l_1 = 1.27660626172 + + y_ebfi_l_2 = 0.0453619253429 + + y_ebfi_l_3 = (-0.135655771316) + + y_ebfi_l_4 = (-0.18631241575) + + y_ebfi_l_5 = 0.616485384319 + + y_ebfi_l_6 = 0.383514615681 + + y_ebfi_l_7 = (-0.383514615681) + + y_ebfi_l_8 = (-0.000958786539202) + + y_ecd_l_1 = 0.78385727975 + + y_ecd_l_2 = 0.156149940356 + + y_ecd_l_3 = 0.0599927798938 + + y_ecd_l_4 = 0.0296796460069 + + y_ech_l_1 = 1.71348425234 + + y_ech_l_2 = (-1.71348425234) + + y_ech_l_3 = 9.76051187168 + + y_ech_l_4 = (-0.718706571642) + + y_ech_l_5 = 0.718706571642 + + y_ecnia_l_1 = 0.735 + + y_ecnia_l_2 = (-0.735) + + y_ecnia_l_3 = 0.1055 + + y_ecnia_l_4 = (-0.1055) + + y_ecnia_l_5 = 0.1595 + + y_ecnia_l_6 = (-0.1595) + + y_eco_l_1 = 1.17546755467 + + y_eco_l_2 = 0.109703169694 + + y_eco_l_3 = (-0.285170724366) + + y_eco_l_4 = 0.692476259501 + + y_eco_l_5 = 0.229572174835 + + y_eco_l_6 = 0.0779515656641 + + y_eco_l_7 = (-0.229612885136) + + y_eco_l_8 = (-0.0779108553636) + + y_egfe_l_1 = 0.726276173623 + + y_egfe_l_2 = (-1.38339974044) + + y_egfe_l_3 = 0.0497143719338 + + y_egfe_l_4 = 0.103593759929 + + y_egfe_l_5 = 1.50381543495 + + y_egfe_l_6 = (-0.000983552448045) + + y_egfe_l_7 = 0.000725681212301 + + y_egfet_l_1 = 0.9 + + y_egfet_l_2 = (-0.1) + + y_egfet_l_3 = (-0.1) + + y_egfet_l_4 = 0.1 + + y_egfet_l_5 = 0.000625 + + y_egfet_l_6 = 0.000625 + + y_egfet_l_7 = 0.000625 + + y_egfet_l_8 = 0.000625 + + y_egfl_l_1 = 1.16197632264 + + y_egfl_l_2 = (-1.12731388567) + + y_egfl_l_3 = (-0.302868541805) + + y_egfl_l_4 = 0.0613337937414 + + y_egfl_l_5 = 1.2068723111 + + y_egfl_l_6 = (-0.00250725401078) + + y_egfl_l_7 = 0.00235067489642 + + y_egflt_l_1 = 0.9 + + y_egflt_l_2 = (-0.1) + + y_egflt_l_3 = 0.1 + + y_egflt_l_4 = 0.000625 + + y_egflt_l_5 = 0.000625 + + y_egflt_l_6 = 0.000625 + + y_egflt_l_7 = 0.000625 + + y_egse_l_1 = 1.00049378528 + + y_egse_l_2 = (-0.797614647892) + + y_egse_l_3 = (-0.128950321813) + + y_egse_l_4 = (-0.00262964990773) + + y_egse_l_5 = 0.928700834331 + + y_egse_l_6 = 0.00158066587876 + + y_egse_l_7 = (-0.000853766092194) + + y_egset_l_1 = 0.9 + + y_egset_l_2 = (-0.1) + + y_egset_l_3 = (-0.1) + + y_egset_l_4 = 0.1 + + y_egset_l_5 = 0.000625 + + y_egset_l_6 = 0.000625 + + y_egset_l_7 = 0.000625 + + y_egset_l_8 = 0.000625 + + y_egsl_l_1 = 1.04483163655 + + y_egsl_l_2 = (-0.633546297018) + + y_egsl_l_3 = (-0.134688612832) + + y_egsl_l_4 = (-0.0215581541096) + + y_egsl_l_5 = 0.744961427412 + + y_egsl_l_6 = (-0.00143256549309) + + y_egsl_l_7 = 0.00176517379444 + + y_egslt_l_1 = 0.9 + + y_egslt_l_2 = (-0.1) + + y_egslt_l_3 = 0.1 + + y_egslt_l_4 = 0.000625 + + y_egslt_l_5 = 0.000625 + + y_egslt_l_6 = 0.000625 + + y_egslt_l_7 = 0.000625 + + y_eh_l_1 = 1.3576278254 + + y_eh_l_2 = 0.0130993143616 + + y_eh_l_3 = (-0.164666195693) + + y_eh_l_4 = (-0.206060944067) + + y_eh_l_5 = (-0.0282729007489) + + y_eh_l_6 = 0.0282729007489 + + y_eh_l_7 = (-0.000786966438108) + + y_emn_l_2 = 0.928554219554 + + y_emn_l_3 = 0.0714457804463 + + y_emo_l_1 = 0.819289500318 + + y_emo_l_2 = (-0.180710499682) + + y_emo_l_3 = 1.31018224516 + + y_emo_l_4 = 0.180710499682 + + y_emo_l_5 = 0.0135818692772 + + y_emo_l_6 = 0.00278890259237 + + y_emo_l_7 = (-0.0163707718696) + + y_emo_l_8 = 0.723524924437 + + y_emo_l_9 = (-0.404694213855) + + y_emp_l_1 = 40.1856146542 + + y_emp_l_2 = 0.048026 + + y_emp_l_3 = (-0.048026) + + y_emp_l_4 = (-0.048026) + + y_emp_l_5 = 0.048026 + + y_emp_l_6 = 0.022115 + + y_ex_l_1 = 0.892272127137 + + y_ex_l_2 = (-0.107727872863) + + y_ex_l_3 = (-0.107727872863) + + y_ex_l_4 = (-0.107727872863) + + y_ex_l_5 = 0.107727872863 + + y_ex_l_6 = 0.107727872863 + + y_ex_l_7 = 0.0148164224533 + + y_ex_l_8 = (-0.0045419370785) + + y_ex_l_9 = (-0.0102744853748) + + y_ex_l_10 = 1.01585705046 + + y_fcbn_l_2 = (-5.55068537239) + + y_fcbn_l_3 = 6.86052021077 + + y_fcbn_l_4 = (-2.52909715822) + + y_fcbn_l_5 = 1.9133340876 + + y_fcbn_l_6 = (-35.2463013113) + + y_fcbn_l_7 = 0.305928232246 + + y_fcbn_l_8 = 0.305928232246 + + y_fgdp_l_2 = 0.01 + + y_fgdpt_l_1 = 0.9 + + y_fgdpt_l_2 = 0.1 + + y_fgdpt_l_3 = 0.000625 + + y_fgdpt_l_4 = 0.000625 + + y_fgdpt_l_5 = 0.000625 + + y_fgdpt_l_6 = 0.000625 + + y_fnicn_l_1 = 0.993277528339 + + y_fnicn_l_2 = 0.00672247166135 + + y_fnicn_l_4 = 0.537028034851 + + y_fnicn_l_5 = (-0.537028034851) + + y_fnicn_l_6 = (-0.66631256176) + + y_fnicn_l_7 = 0.66631256176 + + y_fnicn_l_8 = 0.892965336399 + + y_fniln_l_1 = 0.982046754178 + + y_fniln_l_3 = 0.692942512139 + + y_fniln_l_4 = 0.0100008124223 + + y_fniln_l_5 = 0.00373870870246 + + y_fniln_l_6 = 0.315405113519 + + y_fniln_l_7 = (-0.315405113519) + + y_fniln_l_8 = (-0.0591384587847) + + y_fniln_l_9 = 0.0591384587847 + + y_fniln_l_10 = 0.00421372469752 + + y_fnirn_l_2 = (-169.102652771) + + y_fpc_l_2 = 0.0025 + + y_fpi10_1 = 0.156993726433 + + y_fpi10_2 = 0.156993726433 + + y_fpi10_3 = 0.156993726433 + + y_fpi10_4 = 0.156993726433 + + y_fpi10_5 = 0.372025094268 + + y_fpi10_6 = 0.32214582784 + + y_fpi10t_1 = 0.95 + + y_fpi10t_2 = 0.05 + + y_fpic_1 = 0.678829880162 + + y_fpic_2 = 0.321170119838 + + y_fpxr_l_1 = 0.048 + + y_fpxr_l_2 = (-0.048) + + y_fpxr_l_3 = (-0.048) + + y_fpxr_l_4 = 0.048 + + y_fpxr_l_5 = 0.563832456119 + + y_fpxr_l_6 = (-0.726654492224) + + y_fpxr_l_7 = 0.162822036105 + + y_fpxrr_l_1 = 1.18364909386 + + y_fpxrr_l_2 = (-0.00291888934318) + + y_fpxrr_l_3 = (-0.211089676177) + + y_fpxrr_l_4 = 0.00302407543125 + + y_frl10_1 = 0.988458285734 + + y_frl10_2 = (-0.29200997295) + + y_frl10_3 = (-0.0655047670227) + + y_frl10_4 = 0.369056454239 + + y_frl10_5 = 0.12455118125 + + y_frl10_6 = (-0.12455118125) + + y_frs10_1 = 4.78434763861 + + y_frs10_2 = 0.0 + + y_frs10_3 = 0.25 + + y_frs10_4 = 0.25 + + y_frs10_5 = 0.25 + + y_frs10_6 = 0.25 + + y_frs10_7 = 0.0 + + y_frs10_8 = 0.0 + + y_frstar_1 = 0.95 + + y_frstar_2 = 0.05 + + y_frstar_3 = (-0.0125) + + y_frstar_4 = (-0.0125) + + y_frstar_5 = (-0.0125) + + y_frstar_6 = (-0.0125) + + y_ftcin_l_2 = 190.397828213 + + y_fxgap_1 = 1.29072367633 + + y_fxgap_2 = (-0.468009114875) + + y_fxgap_3 = (-0.0166666666667) + + y_fxgap_4 = 0.00416666666667 + + y_fxgap_5 = 0.00833333333333 + + y_fxgap_6 = 0.0125 + + y_fxgap_7 = 0.0125 + + y_fxgap_8 = (-0.0166666666667) + + y_fxgap_9 = 0.00833333333333 + + y_fxgap_10 = (-0.0166666666667) + + y_fxgap_11 = 0.00416666666667 + + y_fxgap_12 = 0.05 + + y_fxgap_13 = 0.0373455901902 + + y_fynicn_l_2 = 0.203972136271 + + y_fyniln_l_2 = 0.344642504397 + + y_gfdbtnp_l_2 = 0.984645217482 + + y_gfdbtnp_l_3 = 0.0737924242446 + + y_gfdbtnp_l_4 = (-0.0584376417269) + + y_ugfsrp_1 = 0.947688 + + y_uleg_l_1 = (-0.0162972181781) + + y_uleg_l_2 = 0.0162972181781 + + y_uleg_l_3 = 0.1 + + y_gfexpn_l_2 = 0.0964148144871 + + y_gfexpn_l_3 = 0.19363872408 + + y_gfexpn_l_4 = 0.600944699108 + + y_gfexpn_l_5 = 0.109001762325 + + y_gfintn_l_2 = 34.038852147 + + y_gfrecn_l_2 = 0.5764571204 + + y_gfrecn_l_3 = 0.0743675317358 + + y_gfrecn_l_4 = 5.57251231588 + + y_gfrecn_l_5 = 0.349175347864 + + y_gtr_l_2 = 7.39501037898 + + y_gtr_l_3 = 7.39501037898 + + y_gtrd_1 = (-0.000176387604876) + + y_gtrd_2 = (-0.000206546235356) + + y_gtrd_3 = (-4.93246174231e-5) + + y_gtrd_4 = (-4.93246174231e-5) + + y_gtrd_5 = (-4.93246174231e-5) + + y_gtrd_6 = 0.862481931486 + + y_gtrd_7 = 0.000309352740077 + + y_hgemp_1 = 0.9 + + y_hgemp_2 = 40.0 + + y_hgemp_3 = (-40.0) + + y_hggdp_1 = 400.0 + + y_hggdp_2 = (-400.0) + + y_hgpbfir_1 = 0.975 + + y_hgpbfir_2 = 10.0 + + y_hgpbfir_3 = 10.0 + + y_hgpbfir_4 = (-10.0) + + y_hgpbfir_5 = (-10.0) + + y_hgpbfir_6 = (-10.0) + + y_hgpbfir_7 = 10.0 + + y_hgpkir_1 = 0.9 + + y_hgpkir_2 = 43.1298484247 + + y_hgpkir_3 = (-43.0591386594) + + y_hgynid_1 = 454.348916939 + + y_hgynid_2 = (-54.3489169394) + + y_hgynid_3 = (-400.0) + + y_hgynid_4 = (-455.23665293) + + y_hgynid_5 = 55.2366529304 + + y_hgynid_6 = 400.0 + + y_hks_1 = 384.31948476 + + y_hks_2 = (-384.31948476) + + y_hks_3 = 15.68051524 + + y_hks_4 = (-15.68051524) + + y_hlept_1 = 400.0 + + y_hlept_2 = 400.0 + + y_hlept_3 = (-400.0) + + y_hmfpt_1 = 0.95 + + y_hqlfpr_1 = 0.95 + + y_hqlww_1 = 0.95 + + y_huqpct_1 = 0.95 + + y_huxb_1 = 0.324768405324 + + y_huxb_2 = 0.95 + + y_hxbt_1 = 0.725 + + y_hxbt_2 = 0.725 + + y_hxbt_3 = 290.0 + + y_hxbt_4 = (-290.0) + + y_hxbt_5 = 0.275 + + y_jccan_l_2 = 0.82051735145 + + y_jccan_l_3 = (-0.948637916333) + + y_jccan_l_4 = 0.121328058188 + + y_jccan_l_5 = 0.128120564883 + + y_jccan_l_6 = 1.35223326447 + + y_jccan_l_7 = 0.128120564883 + + y_jkcd_l_2 = 4.66817353822 + + y_kbfi_l_2 = 0.0281084105505 + + y_kbfi_l_3 = (-0.0265200751536) + + y_kbfi_l_4 = 0.0281084105505 + + y_kbfi_l_5 = (-0.248867790412) + + y_kbfi_l_6 = 0.971891589449 + + y_kcd_l_2 = 0.066147038262 + + y_kcd_l_3 = (-0.246673633735) + + y_kcd_l_4 = 0.933852961738 + + y_kh_l_2 = 0.00873032740269 + + y_kh_l_3 = (-0.249249311699) + + y_kh_l_4 = 0.991269672597 + + y_ki_l_1 = 1.44204786648 + + y_ki_l_2 = 0.014692062549 + + y_ki_l_3 = 0.250723990347 + + y_ki_l_4 = (-0.456739929026) + + y_ki_l_5 = 0.0711962153783 + + y_ki_l_6 = (-0.307228143176) + + y_ks_l_1 = 0.0025 + + y_leg_l_1 = 0.248485878175 + + y_leg_l_2 = 0.751514121825 + + y_leh_l_2 = 0.813979789462 + + y_leh_l_3 = 0.132451786431 + + y_leh_l_4 = 0.0535684241064 + + y_leo_l_1 = 20.7652726744 + + y_leo_l_2 = 0.756667597034 + + y_leo_l_3 = (-15.6501866511) + + y_leo_l_4 = (-0.756667597034) + + y_leo_l_5 = (-0.0164258334824) + + y_leppot_l_2 = (-0.0110028694424) + + y_leppot_l_3 = (-1.10028694424) + + y_leppot_l_4 = (-0.857254870696) + + y_lf_l_2 = 1.58659431972 + + y_lfpr_1 = 0.432392517171 + + y_lfpr_2 = 0.567607482829 + + y_lfpr_3 = (-0.000875189202097) + + y_lfpr_4 = 0.000875189202097 + + y_lhp_l_1 = 1.00059088506 + + y_lhp_l_2 = 0.202289789801 + + y_lhp_l_3 = (-0.202880674857) + + y_lhp_l_4 = 0.372064184885 + + y_lhp_l_5 = 0.627935815115 + + y_lhp_l_6 = (-0.755331857052) + + y_lhp_l_7 = (-0.00156983953779) + + y_lhp_l_8 = 0.127396041937 + + y_lhp_l_9 = 0.000318490104843 + + y_lur_1 = (-96.2208093896) + + y_lur_2 = 96.2208093896 + + y_lurnat_1 = 0.95 + + y_lww_l_1 = 0.804289649347 + + y_lww_l_2 = 0.00170379588201 + + y_lww_l_3 = 0.195710350653 + + y_lww_l_4 = 0.318481647196 + + y_lww_l_5 = (-0.318481647196) + + y_lww_l_6 = (-0.00079620411799) + + y_mfpt_l_1 = 0.0025 + + y_pbfir_l_1 = 0.0025 + + y_pcdr_l_1 = 1.50984819434 + + y_pcdr_l_2 = (-0.509848194342) + + y_pcer_l_1 = 0.248860953365 + + y_pcer_l_2 = (-0.248860953365) + + y_pcer_l_3 = (-0.248860953365) + + y_pcer_l_4 = 0.248860953365 + + y_pcfr_l_1 = 1.21019336782 + + y_pcfr_l_2 = (-0.14928038046) + + y_pcfr_l_3 = (-0.365198296745) + + y_pcfr_l_4 = 0.318574001625 + + y_pcfr_l_5 = (-0.338884189342) + + y_pcfr_l_6 = 0.333798755712 + + y_pchr_l_1 = 1.59806398567 + + y_pchr_l_2 = (-0.598063985667) + + y_pcnia_l_1 = 0.0025 + + y_pcor_l_1 = (-0.1436) + + y_pcor_l_2 = 0.1436 + + y_pcor_l_3 = (-0.217) + + y_pcor_l_4 = 0.217 + + y_pcpi_l_2 = 0.43067430272 + + y_pcpix_l_2 = 0.426412064374 + + y_pcxfe_l_1 = 0.0025 + + y_pegfr_l_1 = 0.0025 + + y_pegsr_l_1 = 0.0025 + + y_pgfl_l_1 = 0.525153490957 + + y_pgsl_l_1 = 0.514419453205 + + y_phouse_l_1 = 1.89031776892 + + y_phouse_l_2 = (-0.901886995515) + + y_phouse_l_3 = 0.0115692265899 + + y_phouse_l_4 = 0.0115692265899 + + y_phr_l_1 = 0.0025 + + y_pic4_1 = 100.0 + + y_pic4_2 = (-100.0) + + y_picnia_1 = 15.96 + + y_picnia_2 = (-15.96) + + y_picnia_3 = 29.04 + + y_picnia_4 = (-29.04) + + y_picx4_1 = 100.0 + + y_picx4_2 = (-100.0) + + y_picxfe_1 = 0.404860664116 + + y_picxfe_2 = 0.591171818183 + + y_picxfe_3 = 0.00396751770099 + + y_picxfe_4 = 0.462045372577 + + y_picxfe_5 = (-0.462045372577) + + y_pieci_1 = 0.00293156716662 + + y_pieci_2 = 0.00293156716662 + + y_pieci_3 = 0.00293156716662 + + y_pieci_4 = 0.146578358331 + + y_pieci_5 = 0.839226144659 + + y_pieci_6 = 0.00540079551024 + + y_pieci_7 = 0.00540079551024 + + y_pieci_8 = (-2.16031820409) + + y_pieci_9 = (-0.0143209721548) + + y_pieci_10 = 0.0143209721548 + + y_pieci_11 = 0.327959270689 + + y_pieci_12 = (-0.327959270689) + + y_pigdp_1 = 400.0 + + y_pigdp_2 = (-400.0) + + y_pipxnc_1 = (-796.0) + + y_pipxnc_2 = 0.462801 + + y_pipxnc_3 = (-0.462801) + + y_pipxnc_4 = 368.389596 + + y_pipxnc_5 = 0.229745 + + y_pipxnc_6 = (-0.229745) + + y_pipxnc_7 = 182.87702 + + y_pipxnc_8 = (-14.9334031956) + + y_pipxnc_9 = 14.9334031956 + + y_pipxnc_10 = 10.0 + + y_pipxnc_11 = (-10.0) + + y_pkbfir_1 = 0.960531663984 + + y_pkbfir_2 = 1.05983283594 + + y_pl_l_1 = 0.0025 + + y_pmo_l_1 = 0.622318401629 + + y_pmo_l_2 = 0.377681598371 + + y_pmo_l_3 = 0.00731956262431 + + y_pmo_l_4 = (-0.00731956262431) + + y_pmo_l_5 = (-0.629637964254) + + y_pmo_l_6 = 0.234396660333 + + y_pmo_l_7 = (-0.234396660333) + + y_pmo_l_8 = 0.765603339667 + + y_pmp_l_2 = 1.05645668526 + + y_poilr_l_1 = 1.17135063067 + + y_poilr_l_2 = (-0.346197996438) + + y_poilr_l_3 = (-0.390345197801) + + y_poilr_l_4 = 0.79951907837 + + y_ptr_1 = 0.9 + + y_ptr_2 = 0.05 + + y_ptr_3 = 0.05 + + y_pxb_l_2 = 1.01772402773 + + y_pxnc_l_1 = 0.0025 + + y_pxp_l_1 = 0.6469 + + y_pxp_l_2 = (-0.6469) + + y_pxp_l_3 = 0.3531 + + y_pxp_l_4 = (-0.3531) + + y_pxr_l_1 = 0.0025 + + y_qebfi_l_2 = 0.664481948351 + + y_qebfi_l_3 = 0.0787039173848 + + y_qebfi_l_4 = (-0.0787039173848) + + y_qebfi_l_5 = 7.87039173848 + + y_qec_l_1 = 0.935665935123 + + y_qec_l_2 = 0.0166517759473 + + y_qec_l_3 = (-0.139711201786) + + y_qec_l_4 = 0.135400942735 + + y_qec_l_5 = 0.0519925479811 + + y_qecd_l_2 = 3.98656310426 + + y_qecd_l_3 = 0.00498320388032 + + y_qecd_l_4 = 0.00498320388032 + + y_qecd_l_5 = 0.00498320388032 + + y_qecd_l_6 = 0.00498320388032 + + y_qecd_l_7 = 0.00498320388032 + + y_qecd_l_8 = 0.00498320388032 + + y_qecd_l_9 = 0.00498320388032 + + y_qecd_l_10 = 0.00498320388032 + + y_qecd_l_11 = (-0.0232956396718) + + y_qecd_l_12 = (-0.584353967629) + + y_qecd_l_13 = (-0.0242284661483) + + y_qeh_l_2 = 24.6010652056 + + y_qeh_l_3 = 0.0153756657535 + + y_qeh_l_4 = 0.0153756657535 + + y_qeh_l_5 = 0.0153756657535 + + y_qeh_l_6 = 0.0153756657535 + + y_qeh_l_7 = 0.0153756657535 + + y_qeh_l_8 = 0.0153756657535 + + y_qeh_l_9 = 0.0153756657535 + + y_qeh_l_10 = 0.0153756657535 + + y_qeh_l_11 = 0.0153756657535 + + y_qeh_l_12 = 0.0153756657535 + + y_qeh_l_13 = 0.0153756657535 + + y_qeh_l_14 = 0.0153756657535 + + y_qeh_l_15 = 0.0153756657535 + + y_qeh_l_16 = 0.0153756657535 + + y_qeh_l_17 = 0.0153756657535 + + y_qeh_l_18 = 0.0153756657535 + + y_qeh_l_19 = (-0.0270350700995) + + y_qkir_l_1 = 0.00188536673771 + + y_qlf_l_2 = 1.58692282562 + + y_qlww_l_1 = 0.0025 + + y_qpxnc_l_1 = 2.98507462687 + + y_qpxnc_l_2 = (-2.98507462687) + + y_qpxnc_l_3 = (-1.98507462687) + + y_qpxnc_l_4 = 1.98507462687 + + y_qpxp_l_1 = 0.7195976338 + + y_qpxp_l_2 = (-0.7195976338) + + y_qynidn_l_1 = 0.354822592523 + + y_qynidn_l_2 = 1.13587229235 + + y_qynidn_l_3 = (-0.135872292349) + + y_rbbbp_1 = (-0.189051) + + y_rbbbp_2 = 0.848879 + + y_rbbbp_3 = 0.160481423829 + + y_rbfi_1 = (-2.21124682364) + + y_rbfi_2 = 0.395 + + y_rbfi_3 = 0.395 + + y_rbfi_4 = (-0.395) + + y_rbfi_5 = (-0.5) + + y_rbfi_6 = 0.5 + + y_rcar_1 = 1.22665328945 + + y_rcar_2 = 0.0 + + y_rcar_3 = 0.696748171914 + + y_rcar_4 = 0.101669335039 + + y_rcar_5 = 0.201582493047 + + y_rccd_1 = 100.0 + + y_rcch_1 = 100.0 + + y_rcch_2 = (-0.0545840410668) + + y_rcch_3 = 0.7953 + + y_rcch_4 = 79.53 + + y_rcgain_1 = 0.32854362351 + + y_rcgain_2 = 0.225785775119 + + y_rcgain_3 = (-0.225785775119) + + y_reqp_1 = 0.808086 + + y_reqp_2 = 0.795819 + + y_reqp_3 = (-0.643090192434) + + y_rfynic_1 = 1.00400815341 + + y_rfynic_2 = (-0.49108746803) + + y_rfynic_3 = (-0.144424360986) + + y_rfynic_4 = 0.631503675605 + + y_rfynil_1 = 0.884413145643 + + y_rfynil_2 = (-0.00726474303036) + + y_rfynil_3 = (-0.171195169347) + + y_rfynil_4 = 0.0265702779079 + + y_rfynil_5 = (-0.132818819092) + + y_rfynil_6 = 0.0876033907073 + + y_rfynil_7 = 0.261434600384 + + y_rfynil_8 = 0.0179349568622 + + y_rg10p_1 = (-0.460658806872) + + y_rg10p_2 = 0.228721864424 + + y_rg10p_3 = 0.920104088065 + + y_rg10p_4 = 0.423854051406 + + y_rg10p_5 = (-0.210447922486) + + y_rg30p_1 = (-0.624829467707) + + y_rg30p_2 = 0.134994250522 + + y_rg30p_3 = 0.938108605708 + + y_rg30p_4 = 0.586157900756 + + y_rg30p_5 = (-0.126639268136) + + y_rg5p_1 = (-0.349564481) + + y_rg5p_2 = 0.90221329312 + + y_rg5p_3 = 0.315381721561 + + y_rgfint_1 = 0.845677566688 + + y_rgfint_2 = 0.154322433312 + + y_rgfint_3 = 0.00556931000493 + + y_rgfint_4 = (-0.00556931000493) + + y_rgw_1 = 0.00495 + + y_rgw_2 = 0.00271 + + y_rgw_3 = 0.00129 + + y_rgw_4 = 0.00105 + + y_rme_1 = 0.660306961037 + + y_rme_2 = 0.884200704474 + + y_rme_3 = (-0.544507665511) + + y_rme_4 = (-0.102549417082) + + y_rrff_1 = (-0.25) + + y_rrff_2 = (-0.25) + + y_rrff_3 = (-0.25) + + y_rrff_4 = (-0.25) + + y_rrtr_1 = 0.97 + + y_rrtr_2 = 0.03 + + y_rspnia_1 = 7.62633280279 + + y_rspnia_2 = (-7.62633280279) + + y_rtb_1 = 0.799718792152 + + y_rtb_2 = 0.11137355158 + + y_rtb_3 = 0.770122562667 + + y_rtb_4 = (-0.681214906399) + + y_rtbfi_l_2 = 0.0576949599629 + + y_rtbfi_l_3 = 5.76949599629 + + y_rtbfi_l_4 = (-0.0576949599629) + + y_rtbfi_l_5 = (-0.0123862111793) + + y_rtbfi_l_6 = 0.129531747065 + + y_rtbfi_l_7 = 0.0 + + y_rtbfi_l_8 = (-0.260110434765) + + y_rtbfi_l_9 = 0.943576128374 + + y_rtinv_1 = 0.00912489842966 + + y_rtinv_2 = (-0.00912489842966) + + y_rtinv_3 = 0.0330561789534 + + y_rtinv_4 = 0.0356516398072 + + y_rtinv_5 = 0.0329826447805 + + y_rtinv_6 = 0.0355066663735 + + y_rtinv_7 = (-0.066038823734) + + y_tcin_l_2 = 8.35657418879 + + y_tpn_l_2 = 7.01937651683 + + y_tpn_l_3 = 1.18756132659 + + y_tpn_l_4 = (-0.187561326587) + + y_trci_1 = 0.00706626139452 + + y_trci_2 = 0.810247648208 + + y_trci_3 = (-0.810247648208) + + y_trci_4 = (-0.00572542167653) + + y_trp_1 = 0.603942358608 + + y_trp_2 = (-0.603942358608) + + y_trp_3 = 0.236576213581 + + y_trp_4 = (-0.236576213581) + + y_trp_5 = 0.000630587773923 + + y_trptd_1 = 0.420215062775 + + y_trptd_2 = (-0.420215062775) + + y_trptd_3 = (-0.55) + + y_trptd_4 = (-0.422749789232) + + y_trptd_5 = 0.422749789232 + + y_trptd_6 = (-0.5) + + y_trpts_1 = (-0.0180202713644) + + y_trpts_2 = 0.0225987818683 + + y_trpts_3 = (-0.00457851050393) + + y_trpts_4 = 0.1 + + y_trpts_5 = 0.00075 + + y_tryh_1 = 0.144437010525 + + y_tryh_2 = (-0.0944218552605) + + y_tryh_3 = (-0.0500151552646) + + y_uxbt_l_1 = 0.0025 + + y_uynicpnr_1 = 0.779183 + + y_vbfi_1 = 5.96826486935 + + y_vbfi_2 = 1.41987523928 + + y_vbfi_3 = (-1.50480877253) + + y_vbfi_4 = (-1.50480877253) + + y_wpon_l_2 = 0.99460869287 + + y_wpon_l_3 = 0.00146536714744 + + y_wpon_l_4 = 0.408461833894 + + y_wpon_l_5 = (-0.408461833894) + + y_wpon_l_6 = 0.0498372673814 + + y_wpon_l_7 = (-0.0443498822123) + + y_wpon_l_8 = (-0.00103486242602) + + y_wpon_l_9 = 0.000938784388547 + + y_wpon_l_10 = 0.000938784388547 + + y_wpon_l_11 = 0.00466659001196 + + y_wpon_l_12 = (-0.00372780562341) + + y_wpsn_l_1 = 1.13587229235 + + y_wpsn_l_2 = (-0.135872292349) + + y_wpsn_l_3 = (-0.25) + + y_wpsn_l_4 = 0.25 + + y_xb_l_2 = 1.0 + + y_xb_l_3 = (-1.0) + + y_xbn_l_2 = 1.0198018271 + + y_xbn_l_3 = 1.0198018271 + + y_xbn_l_4 = 1.31175365227 + + y_xbn_l_5 = (-1.33155547937) + + y_xbn_l_6 = (-1.33155547937) + + y_xbo_l_1 = 0.0132470548943 + + y_xbt_l_1 = 0.725 + + y_xbt_l_2 = 0.725 + + y_xbt_l_3 = 0.725 + + y_xbt_l_4 = 0.275 + + y_xbtr_l_1 = 0.95 + + y_xfs_l_1 = 0.6849 + + y_xfs_l_2 = (-0.6849) + + y_xfs_l_3 = 0.0386 + + y_xfs_l_4 = (-0.0386) + + y_xfs_l_5 = 0.1324 + + y_xfs_l_6 = (-0.1324) + + y_xfs_l_7 = 0.0429 + + y_xfs_l_8 = (-0.0429) + + y_xfs_l_9 = 0.0223 + + y_xfs_l_10 = (-0.0223) + + y_xfs_l_11 = 0.0395 + + y_xfs_l_12 = (-0.0395) + + y_xfs_l_13 = 0.0691 + + y_xfs_l_14 = (-0.0691) + + y_xfs_l_15 = 0.1203 + + y_xfs_l_16 = (-0.1203) + + y_xfs_l_17 = (-0.1399) + + y_xfs_l_18 = 0.1399 + + y_xfs_l_19 = (-0.0101) + + y_xfs_l_20 = 0.0101 + + y_xfsn_l_2 = 1.00337294235 + + y_xfsn_l_3 = (-0.00363305240167) + + y_xfsn_l_4 = (-0.00337294235067) + + y_xfsn_l_5 = (-0.544852165261) + + y_xfsn_l_6 = 0.541479222911 + + y_xgap_1 = 100.0 + + y_xgap_2 = (-100.0) + + y_xgap2_1 = 100.0 + + y_xgap2_2 = (-100.0) + + y_xgdp_l_1 = 0.9985 + + y_xgdp_l_2 = (-0.9985) + + y_xgdp_l_3 = 0.6264 + + y_xgdp_l_4 = (-1.2513) + + y_xgdp_l_5 = 0.6249 + + y_xgdpn_l_2 = 1.0564013312 + + y_xgdpn_l_3 = 0.021847772281 + + y_xgdpn_l_4 = 0.0674519622926 + + y_xgdpn_l_5 = (-0.149062669624) + + y_xgdpn_l_6 = 0.00362083951871 + + y_xgdpn_l_7 = 0.00336160385466 + + y_xgdpn_l_8 = 0.543020588122 + + y_xgdpn_l_9 = (-0.539658984268) + + y_xp_l_1 = 0.6526679404 + + y_xp_l_2 = (-0.6526679404) + + y_xp_l_3 = 0.0361108836 + + y_xp_l_4 = (-0.0361108836) + + y_xp_l_5 = 0.11825695358 + + y_xp_l_6 = (-0.11825695358) + + y_xp_l_7 = 0.04216893278 + + y_xp_l_8 = (-0.04216893278) + + y_xp_l_9 = 0.0365822346 + + y_xp_l_10 = (-0.0365822346) + + y_xp_l_11 = 0.114213055 + + y_xp_l_12 = (-0.114213055) + + y_ydn_l_2 = 0.998336445483 + + y_ydn_l_3 = 1.13631857158 + + y_ydn_l_4 = (-0.136318571582) + + y_yh_l_2 = 0.526533658207 + + y_yh_l_3 = 0.178799300517 + + y_yh_l_4 = 0.294667041275 + + y_yhgap_1 = 100.0 + + y_yhgap_2 = (-100.0) + + y_yhibn_l_2 = 63.3559682371 + + y_yhl_l_2 = (-1.16884651007) + + y_yhln_l_2 = 1.14236648073 + + y_yhp_l_2 = (-1.10635973458) + + y_yhp_l_3 = 0.946560228789 + + y_yhp_l_4 = 0.0534397712107 + + y_yhpgap_1 = 100.0 + + y_yhpgap_2 = (-100.0) + + y_yhpntn_l_2 = 1.19125575899 + + y_yhpntn_l_3 = 1.19125575899 + + y_yhpntn_l_4 = 1.19125575899 + + y_yhpntn_l_5 = (-1.31798027859) + + y_yhpntn_l_6 = 9.31230191482 + + y_yhpntn_l_7 = (-1.11436836489) + + y_yhpntn_l_8 = (-5.43026982685) + + y_yhpntn_l_9 = (-0.71417194978) + + y_yhpntn_l_10 = (-1.64093920349) + + y_yhptn_l_2 = 0.975134043217 + + y_yhptn_l_3 = 0.563603989214 + + y_yhptn_l_4 = 0.102159206272 + + y_yhptn_l_5 = 0.268955804126 + + y_yhptn_l_6 = 0.0652810003888 + + y_yhshr_l_2 = 0.99999334858 + + y_yhshr_l_3 = (-0.99999334858) + + y_yhsn_l_2 = 8.07609651707 + + y_yhsn_l_3 = 2.34637489687 + + y_yhsn_l_4 = 4.27777403212 + + y_yhsn_l_5 = (-1.78462125185) + + y_yhsn_l_6 = (-11.6695043163) + + y_yhsn_l_7 = (-0.272296812186) + + y_yhsn_l_8 = 17.3716235947 + + y_yhsn_l_9 = 0.0261769342453 + + y_yhtgap_1 = 100.0 + + y_yhtgap_2 = (-100.0) + + y_yhtn_l_2 = 1.00122246375 + + y_ykbfin_l_2 = 0.501135005995 + + y_ykbfin_l_3 = 0.498864994005 + + y_ykin_l_2 = 15.1326363291 + + y_ykin_l_3 = 0.501553881106 + + y_ykin_l_4 = 0.498446118894 + + y_ynicpn_l_2 = 7.69278337234 + + y_ynicpn_l_3 = (-4.79530526339) + + y_ynicpn_l_4 = (-1.22202005096) + + y_ynicpn_l_5 = 8.96683950232 + + y_ynicpn_l_6 = (-0.675458057991) + + y_ynidn_l_1 = 0.000734588128108 + + y_ynidn_l_2 = 0.683167062078 + + y_ynidn_l_3 = (-0.000507771585891) + + y_ynidn_l_4 = (-0.790436568589) + + y_ynidn_l_5 = 0.107269506511 + + y_ynidn_l_6 = 0.209563431411 + + y_ynidn_l_7 = (-0.000157528146717) + + y_ynidn_l_8 = (-0.209563431411) + + y_yniln_l_2 = 0.977159070276 + + y_yniln_l_3 = 0.829114291162 + + y_yniln_l_4 = 0.829114291162 + + y_yniln_l_5 = 0.0418084260005 + + y_yniln_l_6 = 0.0418084260005 + + y_yniln_l_7 = 0.129077282837 + + y_yniln_l_8 = 0.129077282837 + + y_ynin_l_2 = 0.999999935846 + + y_ynin_l_3 = 1.17104491968 + + y_ynin_l_4 = 0.0640520865577 + + y_ynin_l_5 = (-0.0484572291581) + + y_ynin_l_6 = (-0.186639777078) + + y_ynirn_l_1 = 7.33377354801 + + y_ynirn_l_2 = 0.951263114856 + + y_ynirn_l_3 = (-0.951263114856) + + y_ynirn_l_4 = 0.00548690935542 + + y_ynirn_l_5 = (-0.00548690935542) + + y_ypn_l_2 = 0.988173952374 + + y_ypn_l_3 = 0.549385156514 + + y_ypn_l_4 = 0.159614656674 + + y_ypn_l_5 = 0.291000186812 + + y_zdivgr_1 = 0.00975726425743 + + y_zdivgr_2 = 0.990242735743 + + y_zebfi_1 = (-0.000431144211955) + + y_zebfi_2 = (-0.00050714173603) + + y_zebfi_3 = (-3.88181916088e-5) + + y_zebfi_4 = 0.00016798757544 + + y_zebfi_5 = (-0.000975251482943) + + y_zebfi_6 = 0.000417269685018 + + y_zebfi_7 = 9.80402248148e-6 + + y_zebfi_8 = 0.00040254489385 + + y_zebfi_9 = 0.000145632881593 + + y_zebfi_10 = 0.000809116564154 + + y_zebfi_11 = 0.000691481740712 + + y_zebfi_12 = (-0.00152462990113) + + y_zebfi_13 = 0.000182102122415 + + y_zebfi_14 = 0.000170960242897 + + y_zebfi_15 = 0.0142945657655 + + y_zebfi_16 = (-0.00425222899975) + + y_zebfi_17 = (-0.00503049733108) + + y_zebfi_18 = (-0.00112440248315) + + y_zebfi_19 = (-0.0038874369515) + + y_zebfi_20 = 0.00035570453849 + + y_zebfi_21 = (-0.00035570453849) + + y_zecd_1 = (-0.000424433044911) + + y_zecd_2 = (-0.000566112732916) + + y_zecd_3 = (-0.000427835415485) + + y_zecd_4 = 4.27545061866e-6 + + y_zecd_5 = (-0.00133363746841) + + y_zecd_6 = 0.00178510275432 + + y_zecd_7 = (-0.000271474405975) + + y_zecd_8 = 0.000459611864377 + + y_zecd_9 = 0.000428608849069 + + y_zecd_10 = (-0.00111248088805) + + y_zecd_11 = 3.61133130939e-5 + + y_zecd_12 = 7.97590705793e-5 + + y_zecd_13 = 0.00141410574269 + + y_zecd_14 = (-0.000639602744318) + + y_zecd_15 = (-0.00010841426451) + + y_zecd_16 = 0.000210363124201 + + y_zecd_17 = 0.000178061664134 + + y_zecd_18 = 0.000146912749167 + + y_zecd_19 = (-0.000139880426754) + + y_zecd_20 = (-3.38007573296e-5) + + y_zecd_21 = 0.000166975793706 + + y_zecd_22 = 0.000113506936821 + + y_zecd_23 = 0.000124123127885 + + y_zecd_24 = (-0.000203591971486) + + y_zecd_25 = 5.7989188193e-5 + + y_zecd_26 = 0.000114280775871 + + y_zecd_27 = 0.00255088738447 + + y_zecd_28 = (-0.001880611807) + + y_zecd_29 = 0.0308598105755 + + y_zecd_30 = (-0.00201324622316) + + y_zecd_31 = (-0.0365513581269) + + y_zecd_32 = (-0.00465896135484) + + y_zecd_33 = 0.0123637551294 + + y_zeco_1 = (-7.52202049496e-5) + + y_zeco_2 = (-7.94406933181e-5) + + y_zeco_3 = (-2.05931699614e-5) + + y_zeco_4 = 0.000100439779498 + + y_zeco_5 = 2.12832185698e-5 + + y_zeco_6 = 1.70353153588e-5 + + y_zeco_7 = 5.5012376381e-5 + + y_zeco_8 = 3.68085672111e-5 + + y_zeco_9 = (-0.000630171036922) + + y_zeco_10 = 0.000273875586514 + + y_zeco_11 = 0.000133019756131 + + y_zeco_12 = (-3.46619140531e-5) + + y_zeco_13 = 7.48142887307e-5 + + y_zeco_14 = (-0.000130139477521) + + y_zeco_15 = (-0.000574849476126) + + y_zeco_16 = 0.000315791553755 + + y_zeco_17 = 0.000397005436297 + + y_zeco_18 = 2.60636593368e-5 + + y_zeco_19 = (-6.06591388527e-5) + + y_zeco_20 = (-5.86151697491e-6) + + y_zeco_21 = 4.60869299242e-5 + + y_zeco_22 = (-3.67533909379e-5) + + y_zeco_23 = 0.000205501772024 + + y_zeco_24 = (-0.000240937714399) + + y_zeco_25 = (-0.000131812287659) + + y_zeco_26 = (-8.99812036284e-5) + + y_zeco_27 = 0.0011735331967 + + y_zeco_28 = 0.0732239724725 + + y_zeco_29 = (-0.0439002248803) + + y_zeco_30 = (-0.0221752554553) + + y_zeco_31 = 0.00192935493602 + + y_zeco_32 = (-0.00907784707292) + + y_zeh_1 = (-7.82590644729e-5) + + y_zeh_2 = (-5.22177650099e-5) + + y_zeh_3 = (-5.11617748264e-5) + + y_zeh_4 = 1.06118801985e-5 + + y_zeh_5 = 0.00015247021427 + + y_zeh_6 = 0.000115461916411 + + y_zeh_7 = 6.94775905299e-5 + + y_zeh_8 = 6.915284272e-6 + + y_zeh_9 = 0.000501695975955 + + y_zeh_10 = (-0.000531489463237) + + y_zeh_11 = (-0.000152716722883) + + y_zeh_12 = 4.42229725272e-5 + + y_zeh_13 = 0.000171026724111 + + y_zeh_14 = (-0.000344325005483) + + y_zeh_15 = 5.54746256755e-5 + + y_zeh_16 = 4.07823169105e-5 + + y_zeh_17 = (-3.07861940478e-5) + + y_zeh_18 = (-4.58464253978e-6) + + y_zeh_19 = 7.13641520669e-6 + + y_zeh_20 = 1.47073943648e-5 + + y_zeh_21 = 3.83084742308e-5 + + y_zeh_22 = 3.27196755109e-5 + + y_zeh_23 = (-6.99285491718e-5) + + y_zeh_24 = 4.55009661188e-7 + + y_zeh_25 = 5.50461376242e-5 + + y_zeh_26 = 2.86419262708e-5 + + y_zeh_27 = 0.00106943231558 + + y_zeh_28 = 0.00426630302385 + + y_zeh_29 = (-0.00573847474041) + + y_zeh_30 = (-0.00187609012218) + + y_zeh_31 = (-0.000659760712587) + + y_zeh_32 = 0.00400802255132 + + y_zgap05_1 = 0.0547936526434 + + y_zgap05_2 = 0.945206347357 + + y_zgap10_1 = 0.0300745581094 + + y_zgap10_2 = 0.969925441891 + + y_zgap30_1 = 0.014106588982 + + y_zgap30_2 = 0.985893411018 + + y_zgapc2_1 = (-0.0141848331986) + + y_zgapc2_2 = (-0.00438957847118) + + y_zgapc2_3 = (-0.00608986499063) + + y_zgapc2_4 = 0.00127453586676 + + y_zgapc2_5 = (-0.0426889990258) + + y_zgapc2_6 = 0.00775994605046 + + y_zgapc2_7 = 0.0191285668792 + + y_zgapc2_8 = (-0.00220795277592) + + y_zgapc2_9 = 0.194384536968 + + y_zgapc2_10 = (-0.0764007234264) + + y_zgapc2_11 = (-0.0113246023485) + + y_zgapc2_12 = (-0.0155518339662) + + y_zgapc2_13 = 0.0233897407937 + + y_zgapc2_14 = 0.018008438872 + + y_zlhp_1 = (-0.000202321377434) + + y_zlhp_2 = (-6.54709155556e-5) + + y_zlhp_3 = (-0.000172024683014) + + y_zlhp_4 = 3.13937564958e-5 + + y_zlhp_5 = (-0.00104747602255) + + y_zlhp_6 = 0.000259883906459 + + y_zlhp_7 = 0.00050790958016 + + y_zlhp_8 = (-4.19862687488e-5) + + y_zlhp_9 = 0.000321668804678 + + y_zlhp_10 = 0.000408423219508 + + y_zlhp_11 = (-0.00575496472936) + + y_zlhp_12 = 0.00581682769254 + + y_zlhp_13 = (-0.000203485929498) + + y_zlhp_14 = (-0.00027154348171) + + y_zlhp_15 = 0.685794690175 + + y_zlhp_16 = (-0.685794690175) + + y_zlhp_17 = (-0.685794690175) + + y_zlhp_18 = 0.685794690175 + + y_zlhp_19 = 0.000278310168582 + + y_zlhp_20 = 0.000278310168582 + + y_zpi10_1 = 0.0300745581094 + + y_zpi10_2 = 0.969925441891 + + y_zpi10f_1 = 0.0300745581094 + + y_zpi10f_2 = 0.969925441891 + + y_zpi5_1 = 0.0817876274963 + + y_zpi5_2 = 0.0221868418868 + + y_zpi5_3 = 0.0250194521826 + + y_zpi5_4 = (-9.00706244808e-5) + + y_zpi5_5 = (-0.145676547176) + + y_zpi5_6 = (-0.0311377360679) + + y_zpi5_7 = (-0.0294931929574) + + y_zpi5_8 = (-0.0275798582146) + + y_zpi5_9 = 0.233887334416 + + y_zpi5_10 = 0.871096149059 + + y_zpi5_11 = 0.174192252057 + + y_zpi5_12 = (-0.0718402312689) + + y_zpi5_13 = 0.0406637195158 + + y_zpi5_14 = 0.0449446239851 + + y_zpib5_1 = 21.9174610574 + + y_zpib5_2 = (-21.9174610574) + + y_zpib5_3 = 0.945206347357 + + y_zpic30_1 = 0.014106588982 + + y_zpic30_2 = 0.985893411018 + + y_zpicxfe_1 = 0.380818884672 + + y_zpicxfe_2 = 0.00113182715476 + + y_zpicxfe_3 = 0.00146351917605 + + y_zpicxfe_4 = 0.00225729733693 + + y_zpicxfe_5 = 0.0460967342223 + + y_zpicxfe_6 = 0.0338772671906 + + y_zpicxfe_7 = 0.0228924215171 + + y_zpicxfe_8 = 0.0112105032823 + + y_zpicxfe_9 = (-0.0140156100481) + + y_zpicxfe_10 = 0.0011222896601 + + y_zpicxfe_11 = 0.00760121840982 + + y_zpicxfe_12 = (-0.00299260406007) + + y_zpicxfe_13 = 0.0470383710002 + + y_zpicxfe_14 = (-0.0278318348119) + + y_zpicxfe_15 = (-0.00506170904133) + + y_zpicxfe_16 = (-0.00225028901719) + + y_zpicxfe_17 = 0.00828470603822 + + y_zpicxfe_18 = 0.500251545448 + + y_zpicxfe_19 = 11.937795061 + + y_zpicxfe_20 = (-11.937795061) + + y_zpicxfe_21 = 6.84395376806e-5 + + y_zpicxfe_22 = (-6.84395376806e-5) + + y_zpicxfe_23 = (-0.114076926212) + + y_zpicxfe_24 = 45.6307704848 + + y_zpicxfe_25 = (-0.00383816812034) + + y_zpicxfe_26 = 0.00383816812034 + + y_zpicxfe_27 = (-0.000695300677346) + + y_zpicxfe_28 = 0.000695300677346 + + y_zpieci_1 = (-0.026022539351) + + y_zpieci_2 = 0.00320414216918 + + y_zpieci_3 = 0.00402676215955 + + y_zpieci_4 = 0.00650489050087 + + y_zpieci_5 = 0.202430424141 + + y_zpieci_6 = 0.196252633802 + + y_zpieci_7 = 0.195837958296 + + y_zpieci_8 = 0.0246831983934 + + y_zpieci_9 = (-0.0328787076454) + + y_zpieci_10 = 0.00135903909754 + + y_zpieci_11 = 0.0229838005541 + + y_zpieci_12 = (-0.00862383586105) + + y_zpieci_13 = 0.148708914616 + + y_zpieci_14 = (-0.0777266665551) + + y_zpieci_15 = (-0.0137748704693) + + y_zpieci_16 = (-0.00648469451174) + + y_zpieci_17 = 0.0171597038548 + + y_zpieci_18 = 0.393082529889 + + y_zpieci_19 = (-4.49541220961) + + y_zpieci_20 = 4.49541220961 + + y_zpieci_21 = 0.000154587412169 + + y_zpieci_22 = (-0.000154587412169) + + y_zpieci_23 = 0.380795785368 + + y_zpieci_24 = (-152.318314147) + + y_zpieci_25 = (-0.0172443115476) + + y_zpieci_26 = 0.0172443115476 + + y_zpieci_27 = (-0.00416159167724) + + y_zpieci_28 = 0.00416159167724 + + y_zrff10_1 = 0.0300745581094 + + y_zrff10_2 = 0.969925441891 + + y_zrff30_1 = 0.014106588982 + + y_zrff30_2 = 0.985893411018 + + y_zrff5_1 = 0.0547936526434 + + y_zrff5_2 = 0.945206347357 + + y_zyh_l_1 = 8.4030342164e-5 + + y_zyh_l_2 = 0.000702312990074 + + y_zyh_l_3 = 0.00059883124856 + + y_zyh_l_4 = 0.000469068735294 + + y_zyh_l_5 = (-0.00211240097558) + + y_zyh_l_6 = 0.000165660225273 + + y_zyh_l_7 = (-0.000778039304358) + + y_zyh_l_8 = 0.000290681377414 + + y_zyh_l_9 = 0.000905762637463 + + y_zyh_l_10 = 0.00268742554252 + + y_zyh_l_11 = 0.000682359720547 + + y_zyh_l_12 = (-0.00125638870298) + + y_zyh_l_13 = (-0.00185424331619) + + y_zyh_l_14 = 0.00243409867739 + + y_zyh_l_15 = 0.00416166538503 + + y_zyh_l_16 = 0.000750155785081 + + y_zyh_l_17 = (-0.000192123546578) + + y_zyh_l_18 = (-0.000360282907619) + + y_zyhp_l_1 = 0.000863735995891 + + y_zyhp_l_2 = 0.00104399392499 + + y_zyhp_l_3 = 0.000941268897539 + + y_zyhp_l_4 = 0.000458916642717 + + y_zyhp_l_5 = (-0.00154443220559) + + y_zyhp_l_6 = (-0.000640772573525) + + y_zyhp_l_7 = (-0.000877216982815) + + y_zyhp_l_8 = (-4.0750285327e-5) + + y_zyhp_l_9 = (-0.00203693491032) + + y_zyhp_l_10 = 0.00307902287603 + + y_zyhp_l_11 = 0.00153367057932 + + y_zyhp_l_12 = (-0.000979443369808) + + y_zyhp_l_13 = (-0.00330791546136) + + y_zyhp_l_14 = 0.00310317204747 + + y_zyhp_l_15 = 0.00422069758369 + + y_zyhp_l_16 = 0.000130762149267 + + y_zyhp_l_17 = 5.34207087507e-5 + + y_zyhp_l_18 = (-0.00083494424337) + + y_zyhp_l_19 = 0.00225147864855 + + y_zyhp_l_20 = 0.000201405002604 + + y_zyhp_l_21 = (-0.000427498545256) + + y_zyhp_l_22 = (-0.000252078634627) + + y_zyhpst_l_1 = 0.0005 + + y_zyhst_l_1 = 0.0005 + + y_zyht_l_1 = (-0.000334830912493) + + y_zyht_l_2 = 0.000473468268016 + + y_zyht_l_3 = 0.000279807258553 + + y_zyht_l_4 = 0.000327960879431 + + y_zyht_l_5 = (-0.00250027976398) + + y_zyht_l_6 = 0.00088556815263 + + y_zyht_l_7 = (-0.00121488126811) + + y_zyht_l_8 = 8.52432970988e-5 + + y_zyht_l_9 = 0.00217284254807 + + y_zyht_l_10 = 0.00313028920932 + + y_zyht_l_11 = 0.00211420687194 + + y_zyht_l_12 = 0.000248144569686 + + y_zyht_l_13 = (-0.000746405493548) + + y_zyht_l_14 = 0.00274434958239 + + y_zyht_l_15 = 0.00270824568765 + + y_zyht_l_16 = 0.000679378874986 + + y_zyht_l_17 = 0.000308986169352 + + y_zyht_l_18 = (-6.93511660157e-5) + + y_zyht_l_19 = 0.00183760811329 + + y_zyht_l_20 = 0.000509649917682 + + y_zyht_l_21 = 9.47084299939e-5 + + y_zyht_l_22 = 0.0002426388839 + + y_zyhtst_l_1 = 0.0005 + + y_zynid_1 = (-0.000102077846072) + + y_zynid_2 = 0.000348695205252 + + y_zynid_3 = 0.000252250306328 + + y_zynid_4 = 0.00020597993691 + + y_zynid_5 = 0.000352649102887 + + y_zynid_6 = (-0.00091171528933) + + y_zynid_7 = (-0.000833252281803) + + y_zynid_8 = 0.000222028205174 + + y_zynid_9 = 0.00117029026307 + + y_zynid_10 = (-0.000704847602418) + + y_zynid_11 = (-0.00509852446865) + + y_zynid_12 = 0.00166112741624 + + y_zynid_13 = 0.000634556266817 + + y_zynid_14 = 0.0013968199402 + + y_zynid_15 = 0.00251911216766 + + y_zynid_16 = (-0.00251911216766) + + y_zynid_17 = 0.0129976207113 + + y_zynid_18 = (-0.0129976207113) + + y_zynid_19 = (-0.00311724318294) + + y_zynid_20 = 0.00311724318294 + + y_zynid_21 = (-0.0193734997949) + + y_zynid_22 = 0.0193734997949 + + y_zynid_23 = 0.00697401009889 + + y_zynid_24 = (-0.00697401009889) + + y_zynid_25 = 0.00296525365916 + + rho_trp_a = 0.0 + + trp_abar = 0.0 + + rho_fiscal = 0.97 + + rho_fiscalav = 0.9 + + fiscal_egfe = 0.01 + + fiscal_egfl = 0.01 + + av = 1.0 + + fbar_iscal = 0.0 + + fpxrr_lbar = 0.0 + + rho_fpxrr_l = 0.0 + + pmo_lbar = 0.0 + + rho_pmo_l = 0.0 + + emo_lbar = 0.0 + + rho_emo_l = 0.0 + + ugfdbtp_lbar = 0.0 + + rho_ugfdbtp_l = 0.0 + + rho_gfsrt = 0.0 + +end + diff --git a/test/FRBUS.mod b/test/FRBUS.mod new file mode 100644 index 000000000..3cd9dd96e --- /dev/null +++ b/test/FRBUS.mod @@ -0,0 +1,2107 @@ +var +debt_to_gdp delrff dpadj dpgap ebfi_l ebfin_l ecd_l ech_l ecnia_l ecnian_l eco_l egfe_l egfen_l egfet_l egfl_l egfln_l egflt_l egse_l egsen_l egset_l egsl_l egsln_l egslt_l eh_l ehn_l emn_l emo_l emo_ltilde emon_l emp_l empn_l ex_l exn_l fcbn_l fgdp_l fgdpt_l fiscal fiscalav fnicn_l fniln_l fnirn_l fpc_l fpi10 fpi10t fpic fpx_l fpxr_l fpxrr_l fpxrr_ltilde frl10 frs10 frstar ftcin_l fxgap fynicn_l fyniln_l gfdbtn_l gfdbtnp_l gfexpn_l gfintn_l gfrecn_l gfsrt gov_exp_share gtn_l gtr_l gtrd hgemp hggdp hggdpt hgpbfir hgpkir hgynid hks hlept hlprdt hmfpt hqlfpr hqlww huqpct huxb hxbt income_tax_share_of_gdp jccan_l jkcd_l kbfi_l kcd_l kh_l ki_l ks_l leg_l leh_l leo_l lep_l leppot_l lf_l lfpr lhp_l lprdt_l lur lurnat lww_l mfpt_l pbfir_l pcdr_l pcer_l pcfr_l pchr_l pcnia_l pcor_l pcpi_l pcpix_l pcxfe_l pegfr_l pegsr_l pgdp_l pgfl_l pgsl_l phouse_l phr_l pic4 picnia picx4 picxfe pieci pigdp pipl pipxnc pkbfir pl_l pmo_l pmo_ltilde pmp_l poil_l poilr_l ptr pxb_l pxnc_l pxp_l pxr_l qebfi_l qec_l qecd_l qeco_l qeh_l qkir_l qlf_l qlfpr qlhp_l qlww_l qpcnia_l qpl_l qpxb_l qpxnc_l qpxp_l qynidn_l rbbb rbbbp rbfi rcar rccd rcch rcgain req reqp rff rfynic rfynil rg10 rg10p rg30 rg30p rg5 rg5p rgfint rgw rme rrff rrtr rspnia rtb rtbfi_l rtinv rtr rule tcin_l tpn_l trci trp trp_a trpt trptd trpts tryh ugap ugfdbtp_l ugfsrp uleg_l uqpct_l uxbt_l uynicpnr vbfi wpo_l wpon_l wps_l wpsn_l xb_l xbn_l xbo_l xbt_l xbtr_l xfs_l xfsn_l xgap xgap2 xgdi_l xgdin_l xgdo_l xgdp_l xgdpn_l xgdpt_l xgdptn_l xp_l xpn_l ydn_l yh_l yhgap yhibn_l yhl_l yhln_l yhp_l yhpcd_l yhpgap yhpntn_l yhpshr_l yhptn_l yhshr_l yhsn_l yht_l yhtgap yhtn_l yhtshr_l ykbfin_l ykin_l ynicpn_l ynidn_l yniln_l ynin_l ynirn_l ypn_l zdivgr zebfi zecd zeco zeh zgap05 zgap10 zgap30 zgapc2 zlhp zpi10 zpi10f zpi5 zpib5 zpic30 zpic58 zpicxfe zpieci zrff10 zrff30 zrff5 zyh_l zyhp_l zyhpst_l zyhst_l zyht_l zyhtst_l zynid ; + +varexo +adjlegrt d79a d8095 d83 d87 ddockm ddockx dfmprr dglprd ebfi_l_aerr ecd_l_aerr ech_l_aerr eco_l_aerr egfe_l_aerr egfl_l_aerr egse_l_aerr egsl_l_aerr eh_l_aerr emo_l_aerr emp_l_aerr emptrt eradd ex_l_aerr fiscal_aerr fpitrg fpxrr_l_aerr fpxrrt fxgap_aerr gfdrt gfsrt_err gtrd_aerr gtrt hgpcdr hksr hmfpt_aerr hqlfpr_aerr hqlww_aerr jrbfi jrcd jrh ki_l_aerr leo_l_aerr lfpr_aerr lhp_l_aerr lqualt_l lurnat_aerr lww_l_aerr mfpt_l_aerr n16_l pbfir_l_aerr pcer_l_aerr pcfr_l_aerr pcfrt pegfr_l_aerr pegsr_l_aerr phouse_l_aerr phr_l_aerr picxfe_aerr pieci_aerr pitarg pkir pmo_l_aerr poilr_l_aerr poilrt pwstar_l pxr_l_aerr qleor rbbbp_aerr rcar_aerr rcgain_aerr reqp_aerr rfnict rfrs10 rfynic_aerr rfynil_aerr rg10p_aerr rg30p_aerr rg5p_aerr rgfint_aerr rme_aerr t47 tapddp tdpv trci_aerr trcit trfcim trfpm tritc trp_aerr trspp uemot ufcbr ufnir uftcin ugfdbt_l ugfdbtp_lerr upcpi upcpix upgfl upgsl upkbfir upmp upxb uvbfi uyd uyhibn uyhln uyhptn uyhsn uyhtn uyl uyni uyp ymsdn ynidn_l_aerr ynirn_l_aerr ; + +parameters +av emo_lbar fiscal_egfe fiscal_egfl fpxrr_lbar fbar_iscal mei_l mep_l pmo_lbar qpmo_l rho_emo_l rho_fiscal rho_fiscalav rho_fpxrr_l rho_gfsrt rho_pmo_l rho_qkir_l rho_trp_a rho_ugfdbtp_l rstar trp_abar ugfdbtp_lbar y_dpgap_1 y_dpgap_10 y_dpgap_11 y_dpgap_2 y_dpgap_3 y_dpgap_4 y_dpgap_5 y_dpgap_6 y_dpgap_7 y_dpgap_8 y_dpgap_9 y_ebfi_l_1 y_ebfi_l_2 y_ebfi_l_3 y_ebfi_l_4 y_ebfi_l_5 y_ebfi_l_6 y_ebfi_l_7 y_ebfi_l_8 y_ecd_l_1 y_ecd_l_2 y_ecd_l_3 y_ecd_l_4 y_ech_l_1 y_ech_l_2 y_ech_l_3 y_ech_l_4 y_ech_l_5 y_ecnia_l_1 y_ecnia_l_2 y_ecnia_l_3 y_ecnia_l_4 y_ecnia_l_5 y_ecnia_l_6 y_eco_l_1 y_eco_l_2 y_eco_l_3 y_eco_l_4 y_eco_l_5 y_eco_l_6 y_eco_l_7 y_eco_l_8 y_egfe_l_1 y_egfe_l_2 y_egfe_l_3 y_egfe_l_4 y_egfe_l_5 y_egfe_l_6 y_egfe_l_7 y_egfet_l_1 y_egfet_l_2 y_egfet_l_3 y_egfet_l_4 y_egfet_l_5 y_egfet_l_6 y_egfet_l_7 y_egfet_l_8 y_egfl_l_1 y_egfl_l_2 y_egfl_l_3 y_egfl_l_4 y_egfl_l_5 y_egfl_l_6 y_egfl_l_7 y_egflt_l_1 y_egflt_l_2 y_egflt_l_3 y_egflt_l_4 y_egflt_l_5 y_egflt_l_6 y_egflt_l_7 y_egse_l_1 y_egse_l_2 y_egse_l_3 y_egse_l_4 y_egse_l_5 y_egse_l_6 y_egse_l_7 y_egset_l_1 y_egset_l_2 y_egset_l_3 y_egset_l_4 y_egset_l_5 y_egset_l_6 y_egset_l_7 y_egset_l_8 y_egsl_l_1 y_egsl_l_2 y_egsl_l_3 y_egsl_l_4 y_egsl_l_5 y_egsl_l_6 y_egsl_l_7 y_egslt_l_1 y_egslt_l_2 y_egslt_l_3 y_egslt_l_4 y_egslt_l_5 y_egslt_l_6 y_egslt_l_7 y_eh_l_1 y_eh_l_2 y_eh_l_3 y_eh_l_4 y_eh_l_5 y_eh_l_6 y_eh_l_7 y_emn_l_2 y_emn_l_3 y_emo_l_1 y_emo_l_2 y_emo_l_3 y_emo_l_4 y_emo_l_5 y_emo_l_6 y_emo_l_7 y_emo_l_8 y_emo_l_9 y_emp_l_1 y_emp_l_2 y_emp_l_3 y_emp_l_4 y_emp_l_5 y_emp_l_6 y_ex_l_1 y_ex_l_10 y_ex_l_2 y_ex_l_3 y_ex_l_4 y_ex_l_5 y_ex_l_6 y_ex_l_7 y_ex_l_8 y_ex_l_9 y_fcbn_l_2 y_fcbn_l_3 y_fcbn_l_4 y_fcbn_l_5 y_fcbn_l_6 y_fcbn_l_7 y_fcbn_l_8 y_fgdp_l_2 y_fgdpt_l_1 y_fgdpt_l_2 y_fgdpt_l_3 y_fgdpt_l_4 y_fgdpt_l_5 y_fgdpt_l_6 y_fnicn_l_1 y_fnicn_l_2 y_fnicn_l_4 y_fnicn_l_5 y_fnicn_l_6 y_fnicn_l_7 y_fnicn_l_8 y_fniln_l_1 y_fniln_l_10 y_fniln_l_3 y_fniln_l_4 y_fniln_l_5 y_fniln_l_6 y_fniln_l_7 y_fniln_l_8 y_fniln_l_9 y_fnirn_l_2 y_fpc_l_2 y_fpi10_1 y_fpi10_2 y_fpi10_3 y_fpi10_4 y_fpi10_5 y_fpi10_6 y_fpi10t_1 y_fpi10t_2 y_fpic_1 y_fpic_2 y_fpxr_l_1 y_fpxr_l_2 y_fpxr_l_3 y_fpxr_l_4 y_fpxr_l_5 y_fpxr_l_6 y_fpxr_l_7 y_fpxrr_l_1 y_fpxrr_l_2 y_fpxrr_l_3 y_fpxrr_l_4 y_frl10_1 y_frl10_2 y_frl10_3 y_frl10_4 y_frl10_5 y_frl10_6 y_frs10_1 y_frs10_2 y_frs10_3 y_frs10_4 y_frs10_5 y_frs10_6 y_frs10_7 y_frs10_8 y_frstar_1 y_frstar_2 y_frstar_3 y_frstar_4 y_frstar_5 y_frstar_6 y_ftcin_l_2 y_fxgap_1 y_fxgap_10 y_fxgap_11 y_fxgap_12 y_fxgap_13 y_fxgap_2 y_fxgap_3 y_fxgap_4 y_fxgap_5 y_fxgap_6 y_fxgap_7 y_fxgap_8 y_fxgap_9 y_fynicn_l_2 y_fyniln_l_2 y_gfdbtnp_l_2 y_gfdbtnp_l_3 y_gfdbtnp_l_4 y_gfexpn_l_2 y_gfexpn_l_3 y_gfexpn_l_4 y_gfexpn_l_5 y_gfintn_l_2 y_gfrecn_l_2 y_gfrecn_l_3 y_gfrecn_l_4 y_gfrecn_l_5 y_gtr_l_2 y_gtr_l_3 y_gtrd_1 y_gtrd_2 y_gtrd_3 y_gtrd_4 y_gtrd_5 y_gtrd_6 y_gtrd_7 y_hgemp_1 y_hgemp_2 y_hgemp_3 y_hggdp_1 y_hggdp_2 y_hgpbfir_1 y_hgpbfir_2 y_hgpbfir_3 y_hgpbfir_4 y_hgpbfir_5 y_hgpbfir_6 y_hgpbfir_7 y_hgpkir_1 y_hgpkir_2 y_hgpkir_3 y_hgynid_1 y_hgynid_2 y_hgynid_3 y_hgynid_4 y_hgynid_5 y_hgynid_6 y_hks_1 y_hks_2 y_hks_3 y_hks_4 y_hlept_1 y_hlept_2 y_hlept_3 y_hmfpt_1 y_hqlfpr_1 y_hqlww_1 y_huqpct_1 y_huxb_1 y_huxb_2 y_hxbt_1 y_hxbt_2 y_hxbt_3 y_hxbt_4 y_hxbt_5 y_jccan_l_2 y_jccan_l_3 y_jccan_l_4 y_jccan_l_5 y_jccan_l_6 y_jccan_l_7 y_jkcd_l_2 y_kbfi_l_2 y_kbfi_l_3 y_kbfi_l_4 y_kbfi_l_5 y_kbfi_l_6 y_kcd_l_2 y_kcd_l_3 y_kcd_l_4 y_kh_l_2 y_kh_l_3 y_kh_l_4 y_ki_l_1 y_ki_l_2 y_ki_l_3 y_ki_l_4 y_ki_l_5 y_ki_l_6 y_ks_l_1 y_leg_l_1 y_leg_l_2 y_leh_l_2 y_leh_l_3 y_leh_l_4 y_leo_l_1 y_leo_l_2 y_leo_l_3 y_leo_l_4 y_leo_l_5 y_leppot_l_2 y_leppot_l_3 y_leppot_l_4 y_lf_l_2 y_lfpr_1 y_lfpr_2 y_lfpr_3 y_lfpr_4 y_lhp_l_1 y_lhp_l_2 y_lhp_l_3 y_lhp_l_4 y_lhp_l_5 y_lhp_l_6 y_lhp_l_7 y_lhp_l_8 y_lhp_l_9 y_lur_1 y_lur_2 y_lurnat_1 y_lww_l_1 y_lww_l_2 y_lww_l_3 y_lww_l_4 y_lww_l_5 y_lww_l_6 y_mfpt_l_1 y_pbfir_l_1 y_pcdr_l_1 y_pcdr_l_2 y_pcer_l_1 y_pcer_l_2 y_pcer_l_3 y_pcer_l_4 y_pcfr_l_1 y_pcfr_l_2 y_pcfr_l_3 y_pcfr_l_4 y_pcfr_l_5 y_pcfr_l_6 y_pchr_l_1 y_pchr_l_2 y_pcnia_l_1 y_pcor_l_1 y_pcor_l_2 y_pcor_l_3 y_pcor_l_4 y_pcpi_l_2 y_pcpix_l_2 y_pcxfe_l_1 y_pegfr_l_1 y_pegsr_l_1 y_pgfl_l_1 y_pgsl_l_1 y_phouse_l_1 y_phouse_l_2 y_phouse_l_3 y_phouse_l_4 y_phr_l_1 y_pic4_1 y_pic4_2 y_picnia_1 y_picnia_2 y_picnia_3 y_picnia_4 y_picx4_1 y_picx4_2 y_picxfe_1 y_picxfe_2 y_picxfe_3 y_picxfe_4 y_picxfe_5 y_pieci_1 y_pieci_10 y_pieci_11 y_pieci_12 y_pieci_2 y_pieci_3 y_pieci_4 y_pieci_5 y_pieci_6 y_pieci_7 y_pieci_8 y_pieci_9 y_pigdp_1 y_pigdp_2 y_pipxnc_1 y_pipxnc_10 y_pipxnc_11 y_pipxnc_2 y_pipxnc_3 y_pipxnc_4 y_pipxnc_5 y_pipxnc_6 y_pipxnc_7 y_pipxnc_8 y_pipxnc_9 y_pkbfir_1 y_pkbfir_2 y_pl_l_1 y_pmo_l_1 y_pmo_l_2 y_pmo_l_3 y_pmo_l_4 y_pmo_l_5 y_pmo_l_6 y_pmo_l_7 y_pmo_l_8 y_pmp_l_2 y_poilr_l_1 y_poilr_l_2 y_poilr_l_3 y_poilr_l_4 y_ptr_1 y_ptr_2 y_ptr_3 y_pxb_l_2 y_pxnc_l_1 y_pxp_l_1 y_pxp_l_2 y_pxp_l_3 y_pxp_l_4 y_pxr_l_1 y_qebfi_l_2 y_qebfi_l_3 y_qebfi_l_4 y_qebfi_l_5 y_qec_l_1 y_qec_l_2 y_qec_l_3 y_qec_l_4 y_qec_l_5 y_qecd_l_10 y_qecd_l_11 y_qecd_l_12 y_qecd_l_13 y_qecd_l_2 y_qecd_l_3 y_qecd_l_4 y_qecd_l_5 y_qecd_l_6 y_qecd_l_7 y_qecd_l_8 y_qecd_l_9 y_qeh_l_10 y_qeh_l_11 y_qeh_l_12 y_qeh_l_13 y_qeh_l_14 y_qeh_l_15 y_qeh_l_16 y_qeh_l_17 y_qeh_l_18 y_qeh_l_19 y_qeh_l_2 y_qeh_l_3 y_qeh_l_4 y_qeh_l_5 y_qeh_l_6 y_qeh_l_7 y_qeh_l_8 y_qeh_l_9 y_qkir_l_1 y_qlf_l_2 y_qlww_l_1 y_qpxnc_l_1 y_qpxnc_l_2 y_qpxnc_l_3 y_qpxnc_l_4 y_qpxp_l_1 y_qpxp_l_2 y_qynidn_l_1 y_qynidn_l_2 y_qynidn_l_3 y_rbbbp_1 y_rbbbp_2 y_rbbbp_3 y_rbfi_1 y_rbfi_2 y_rbfi_3 y_rbfi_4 y_rbfi_5 y_rbfi_6 y_rcar_1 y_rcar_2 y_rcar_3 y_rcar_4 y_rcar_5 y_rccd_1 y_rcch_1 y_rcch_2 y_rcch_3 y_rcch_4 y_rcgain_1 y_rcgain_2 y_rcgain_3 y_reqp_1 y_reqp_2 y_reqp_3 y_rfynic_1 y_rfynic_2 y_rfynic_3 y_rfynic_4 y_rfynil_1 y_rfynil_2 y_rfynil_3 y_rfynil_4 y_rfynil_5 y_rfynil_6 y_rfynil_7 y_rfynil_8 y_rg10p_1 y_rg10p_2 y_rg10p_3 y_rg10p_4 y_rg10p_5 y_rg30p_1 y_rg30p_2 y_rg30p_3 y_rg30p_4 y_rg30p_5 y_rg5p_1 y_rg5p_2 y_rg5p_3 y_rgfint_1 y_rgfint_2 y_rgfint_3 y_rgfint_4 y_rgw_1 y_rgw_2 y_rgw_3 y_rgw_4 y_rme_1 y_rme_2 y_rme_3 y_rme_4 y_rrff_1 y_rrff_2 y_rrff_3 y_rrff_4 y_rrtr_1 y_rrtr_2 y_rspnia_1 y_rspnia_2 y_rtb_1 y_rtb_2 y_rtb_3 y_rtb_4 y_rtbfi_l_2 y_rtbfi_l_3 y_rtbfi_l_4 y_rtbfi_l_5 y_rtbfi_l_6 y_rtbfi_l_7 y_rtbfi_l_8 y_rtbfi_l_9 y_rtinv_1 y_rtinv_2 y_rtinv_3 y_rtinv_4 y_rtinv_5 y_rtinv_6 y_rtinv_7 y_tcin_l_2 y_tpn_l_2 y_tpn_l_3 y_tpn_l_4 y_trci_1 y_trci_2 y_trci_3 y_trci_4 y_trp_1 y_trp_2 y_trp_3 y_trp_4 y_trp_5 y_trptd_1 y_trptd_2 y_trptd_3 y_trptd_4 y_trptd_5 y_trptd_6 y_trpts_1 y_trpts_2 y_trpts_3 y_trpts_4 y_trpts_5 y_tryh_1 y_tryh_2 y_tryh_3 y_ugfsrp_1 y_uleg_l_1 y_uleg_l_2 y_uleg_l_3 y_uxbt_l_1 y_uynicpnr_1 y_vbfi_1 y_vbfi_2 y_vbfi_3 y_vbfi_4 y_wpon_l_10 y_wpon_l_11 y_wpon_l_12 y_wpon_l_2 y_wpon_l_3 y_wpon_l_4 y_wpon_l_5 y_wpon_l_6 y_wpon_l_7 y_wpon_l_8 y_wpon_l_9 y_wpsn_l_1 y_wpsn_l_2 y_wpsn_l_3 y_wpsn_l_4 y_xb_l_2 y_xb_l_3 y_xbn_l_2 y_xbn_l_3 y_xbn_l_4 y_xbn_l_5 y_xbn_l_6 y_xbo_l_1 y_xbt_l_1 y_xbt_l_2 y_xbt_l_3 y_xbt_l_4 y_xbtr_l_1 y_xfs_l_1 y_xfs_l_10 y_xfs_l_11 y_xfs_l_12 y_xfs_l_13 y_xfs_l_14 y_xfs_l_15 y_xfs_l_16 y_xfs_l_17 y_xfs_l_18 y_xfs_l_19 y_xfs_l_2 y_xfs_l_20 y_xfs_l_3 y_xfs_l_4 y_xfs_l_5 y_xfs_l_6 y_xfs_l_7 y_xfs_l_8 y_xfs_l_9 y_xfsn_l_2 y_xfsn_l_3 y_xfsn_l_4 y_xfsn_l_5 y_xfsn_l_6 y_xgap2_1 y_xgap2_2 y_xgap_1 y_xgap_2 y_xgdp_l_1 y_xgdp_l_2 y_xgdp_l_3 y_xgdp_l_4 y_xgdp_l_5 y_xgdpn_l_2 y_xgdpn_l_3 y_xgdpn_l_4 y_xgdpn_l_5 y_xgdpn_l_6 y_xgdpn_l_7 y_xgdpn_l_8 y_xgdpn_l_9 y_xp_l_1 y_xp_l_10 y_xp_l_11 y_xp_l_12 y_xp_l_2 y_xp_l_3 y_xp_l_4 y_xp_l_5 y_xp_l_6 y_xp_l_7 y_xp_l_8 y_xp_l_9 y_ydn_l_2 y_ydn_l_3 y_ydn_l_4 y_yh_l_2 y_yh_l_3 y_yh_l_4 y_yhgap_1 y_yhgap_2 y_yhibn_l_2 y_yhl_l_2 y_yhln_l_2 y_yhp_l_2 y_yhp_l_3 y_yhp_l_4 y_yhpgap_1 y_yhpgap_2 y_yhpntn_l_10 y_yhpntn_l_2 y_yhpntn_l_3 y_yhpntn_l_4 y_yhpntn_l_5 y_yhpntn_l_6 y_yhpntn_l_7 y_yhpntn_l_8 y_yhpntn_l_9 y_yhptn_l_2 y_yhptn_l_3 y_yhptn_l_4 y_yhptn_l_5 y_yhptn_l_6 y_yhshr_l_2 y_yhshr_l_3 y_yhsn_l_2 y_yhsn_l_3 y_yhsn_l_4 y_yhsn_l_5 y_yhsn_l_6 y_yhsn_l_7 y_yhsn_l_8 y_yhsn_l_9 y_yhtgap_1 y_yhtgap_2 y_yhtn_l_2 y_ykbfin_l_2 y_ykbfin_l_3 y_ykin_l_2 y_ykin_l_3 y_ykin_l_4 y_ynicpn_l_2 y_ynicpn_l_3 y_ynicpn_l_4 y_ynicpn_l_5 y_ynicpn_l_6 y_ynidn_l_1 y_ynidn_l_2 y_ynidn_l_3 y_ynidn_l_4 y_ynidn_l_5 y_ynidn_l_6 y_ynidn_l_7 y_ynidn_l_8 y_yniln_l_2 y_yniln_l_3 y_yniln_l_4 y_yniln_l_5 y_yniln_l_6 y_yniln_l_7 y_yniln_l_8 y_ynin_l_2 y_ynin_l_3 y_ynin_l_4 y_ynin_l_5 y_ynin_l_6 y_ynirn_l_1 y_ynirn_l_2 y_ynirn_l_3 y_ynirn_l_4 y_ynirn_l_5 y_ypn_l_2 y_ypn_l_3 y_ypn_l_4 y_ypn_l_5 y_zdivgr_1 y_zdivgr_2 y_zebfi_1 y_zebfi_10 y_zebfi_11 y_zebfi_12 y_zebfi_13 y_zebfi_14 y_zebfi_15 y_zebfi_16 y_zebfi_17 y_zebfi_18 y_zebfi_19 y_zebfi_2 y_zebfi_20 y_zebfi_21 y_zebfi_3 y_zebfi_4 y_zebfi_5 y_zebfi_6 y_zebfi_7 y_zebfi_8 y_zebfi_9 y_zecd_1 y_zecd_10 y_zecd_11 y_zecd_12 y_zecd_13 y_zecd_14 y_zecd_15 y_zecd_16 y_zecd_17 y_zecd_18 y_zecd_19 y_zecd_2 y_zecd_20 y_zecd_21 y_zecd_22 y_zecd_23 y_zecd_24 y_zecd_25 y_zecd_26 y_zecd_27 y_zecd_28 y_zecd_29 y_zecd_3 y_zecd_30 y_zecd_31 y_zecd_32 y_zecd_33 y_zecd_4 y_zecd_5 y_zecd_6 y_zecd_7 y_zecd_8 y_zecd_9 y_zeco_1 y_zeco_10 y_zeco_11 y_zeco_12 y_zeco_13 y_zeco_14 y_zeco_15 y_zeco_16 y_zeco_17 y_zeco_18 y_zeco_19 y_zeco_2 y_zeco_20 y_zeco_21 y_zeco_22 y_zeco_23 y_zeco_24 y_zeco_25 y_zeco_26 y_zeco_27 y_zeco_28 y_zeco_29 y_zeco_3 y_zeco_30 y_zeco_31 y_zeco_32 y_zeco_4 y_zeco_5 y_zeco_6 y_zeco_7 y_zeco_8 y_zeco_9 y_zeh_1 y_zeh_10 y_zeh_11 y_zeh_12 y_zeh_13 y_zeh_14 y_zeh_15 y_zeh_16 y_zeh_17 y_zeh_18 y_zeh_19 y_zeh_2 y_zeh_20 y_zeh_21 y_zeh_22 y_zeh_23 y_zeh_24 y_zeh_25 y_zeh_26 y_zeh_27 y_zeh_28 y_zeh_29 y_zeh_3 y_zeh_30 y_zeh_31 y_zeh_32 y_zeh_4 y_zeh_5 y_zeh_6 y_zeh_7 y_zeh_8 y_zeh_9 y_zgap05_1 y_zgap05_2 y_zgap10_1 y_zgap10_2 y_zgap30_1 y_zgap30_2 y_zgapc2_1 y_zgapc2_10 y_zgapc2_11 y_zgapc2_12 y_zgapc2_13 y_zgapc2_14 y_zgapc2_2 y_zgapc2_3 y_zgapc2_4 y_zgapc2_5 y_zgapc2_6 y_zgapc2_7 y_zgapc2_8 y_zgapc2_9 y_zlhp_1 y_zlhp_10 y_zlhp_11 y_zlhp_12 y_zlhp_13 y_zlhp_14 y_zlhp_15 y_zlhp_16 y_zlhp_17 y_zlhp_18 y_zlhp_19 y_zlhp_2 y_zlhp_20 y_zlhp_3 y_zlhp_4 y_zlhp_5 y_zlhp_6 y_zlhp_7 y_zlhp_8 y_zlhp_9 y_zpi10_1 y_zpi10_2 y_zpi10f_1 y_zpi10f_2 y_zpi5_1 y_zpi5_10 y_zpi5_11 y_zpi5_12 y_zpi5_13 y_zpi5_14 y_zpi5_2 y_zpi5_3 y_zpi5_4 y_zpi5_5 y_zpi5_6 y_zpi5_7 y_zpi5_8 y_zpi5_9 y_zpib5_1 y_zpib5_2 y_zpib5_3 y_zpic30_1 y_zpic30_2 y_zpicxfe_1 y_zpicxfe_10 y_zpicxfe_11 y_zpicxfe_12 y_zpicxfe_13 y_zpicxfe_14 y_zpicxfe_15 y_zpicxfe_16 y_zpicxfe_17 y_zpicxfe_18 y_zpicxfe_19 y_zpicxfe_2 y_zpicxfe_20 y_zpicxfe_21 y_zpicxfe_22 y_zpicxfe_23 y_zpicxfe_24 y_zpicxfe_25 y_zpicxfe_26 y_zpicxfe_27 y_zpicxfe_28 y_zpicxfe_3 y_zpicxfe_4 y_zpicxfe_5 y_zpicxfe_6 y_zpicxfe_7 y_zpicxfe_8 y_zpicxfe_9 y_zpieci_1 y_zpieci_10 y_zpieci_11 y_zpieci_12 y_zpieci_13 y_zpieci_14 y_zpieci_15 y_zpieci_16 y_zpieci_17 y_zpieci_18 y_zpieci_19 y_zpieci_2 y_zpieci_20 y_zpieci_21 y_zpieci_22 y_zpieci_23 y_zpieci_24 y_zpieci_25 y_zpieci_26 y_zpieci_27 y_zpieci_28 y_zpieci_3 y_zpieci_4 y_zpieci_5 y_zpieci_6 y_zpieci_7 y_zpieci_8 y_zpieci_9 y_zrff10_1 y_zrff10_2 y_zrff30_1 y_zrff30_2 y_zrff5_1 y_zrff5_2 y_zyh_l_1 y_zyh_l_10 y_zyh_l_11 y_zyh_l_12 y_zyh_l_13 y_zyh_l_14 y_zyh_l_15 y_zyh_l_16 y_zyh_l_17 y_zyh_l_18 y_zyh_l_2 y_zyh_l_3 y_zyh_l_4 y_zyh_l_5 y_zyh_l_6 y_zyh_l_7 y_zyh_l_8 y_zyh_l_9 y_zyhp_l_1 y_zyhp_l_10 y_zyhp_l_11 y_zyhp_l_12 y_zyhp_l_13 y_zyhp_l_14 y_zyhp_l_15 y_zyhp_l_16 y_zyhp_l_17 y_zyhp_l_18 y_zyhp_l_19 y_zyhp_l_2 y_zyhp_l_20 y_zyhp_l_21 y_zyhp_l_22 y_zyhp_l_3 y_zyhp_l_4 y_zyhp_l_5 y_zyhp_l_6 y_zyhp_l_7 y_zyhp_l_8 y_zyhp_l_9 y_zyhpst_l_1 y_zyhst_l_1 y_zyht_l_1 y_zyht_l_10 y_zyht_l_11 y_zyht_l_12 y_zyht_l_13 y_zyht_l_14 y_zyht_l_15 y_zyht_l_16 y_zyht_l_17 y_zyht_l_18 y_zyht_l_19 y_zyht_l_2 y_zyht_l_20 y_zyht_l_21 y_zyht_l_22 y_zyht_l_3 y_zyht_l_4 y_zyht_l_5 y_zyht_l_6 y_zyht_l_7 y_zyht_l_8 y_zyht_l_9 y_zyhtst_l_1 y_zynid_1 y_zynid_10 y_zynid_11 y_zynid_12 y_zynid_13 y_zynid_14 y_zynid_15 y_zynid_16 y_zynid_17 y_zynid_18 y_zynid_19 y_zynid_2 y_zynid_20 y_zynid_21 y_zynid_22 y_zynid_23 y_zynid_24 y_zynid_25 y_zynid_3 y_zynid_4 y_zynid_5 y_zynid_6 y_zynid_7 y_zynid_8 y_zynid_9 ; + +% Parameter definitions: + mep_l = 0.0; + mei_l = 0.0; + qpmo_l = 0.0; + rstar = 0.0; + rho_qkir_l = 0.8; + y_dpgap_1 = 0.0025; + y_dpgap_2 = -0.103649883938; + y_dpgap_3 = 0.103649883938; + y_dpgap_4 = -0.341041547027; + y_dpgap_5 = 0.341041547027; + y_dpgap_6 = -0.121366054939; + y_dpgap_7 = 0.121366054939; + y_dpgap_8 = -0.104958882473; + y_dpgap_9 = 0.104958882473; + y_dpgap_10 = -0.328983631622; + y_dpgap_11 = 0.328983631622; + y_ebfi_l_1 = 1.27660626172; + y_ebfi_l_2 = 0.0453619253429; + y_ebfi_l_3 = -0.135655771316; + y_ebfi_l_4 = -0.18631241575; + y_ebfi_l_5 = 0.616485384319; + y_ebfi_l_6 = 0.383514615681; + y_ebfi_l_7 = -0.383514615681; + y_ebfi_l_8 = -0.000958786539202; + y_ecd_l_1 = 0.78385727975; + y_ecd_l_2 = 0.156149940356; + y_ecd_l_3 = 0.0599927798938; + y_ecd_l_4 = 0.0296796460069; + y_ech_l_1 = 1.71348425234; + y_ech_l_2 = -1.71348425234; + y_ech_l_3 = 9.76051187168; + y_ech_l_4 = -0.718706571642; + y_ech_l_5 = 0.718706571642; + y_ecnia_l_1 = 0.735; + y_ecnia_l_2 = -0.735; + y_ecnia_l_3 = 0.1055; + y_ecnia_l_4 = -0.1055; + y_ecnia_l_5 = 0.1595; + y_ecnia_l_6 = -0.1595; + y_eco_l_1 = 1.17546755467; + y_eco_l_2 = 0.109703169694; + y_eco_l_3 = -0.285170724366; + y_eco_l_4 = 0.692476259501; + y_eco_l_5 = 0.229572174835; + y_eco_l_6 = 0.0779515656641; + y_eco_l_7 = -0.229612885136; + y_eco_l_8 = -0.0779108553636; + y_egfe_l_1 = 0.726276173623; + y_egfe_l_2 = -1.38339974044; + y_egfe_l_3 = 0.0497143719338; + y_egfe_l_4 = 0.103593759929; + y_egfe_l_5 = 1.50381543495; + y_egfe_l_6 = -0.000983552448045; + y_egfe_l_7 = 0.000725681212301; + y_egfet_l_1 = 0.9; + y_egfet_l_2 = -0.1; + y_egfet_l_3 = -0.1; + y_egfet_l_4 = 0.1; + y_egfet_l_5 = 0.000625; + y_egfet_l_6 = 0.000625; + y_egfet_l_7 = 0.000625; + y_egfet_l_8 = 0.000625; + y_egfl_l_1 = 1.16197632264; + y_egfl_l_2 = -1.12731388567; + y_egfl_l_3 = -0.302868541805; + y_egfl_l_4 = 0.0613337937414; + y_egfl_l_5 = 1.2068723111; + y_egfl_l_6 = -0.00250725401078; + y_egfl_l_7 = 0.00235067489642; + y_egflt_l_1 = 0.9; + y_egflt_l_2 = -0.1; + y_egflt_l_3 = 0.1; + y_egflt_l_4 = 0.000625; + y_egflt_l_5 = 0.000625; + y_egflt_l_6 = 0.000625; + y_egflt_l_7 = 0.000625; + y_egse_l_1 = 1.00049378528; + y_egse_l_2 = -0.797614647892; + y_egse_l_3 = -0.128950321813; + y_egse_l_4 = -0.00262964990773; + y_egse_l_5 = 0.928700834331; + y_egse_l_6 = 0.00158066587876; + y_egse_l_7 = -0.000853766092194; + y_egset_l_1 = 0.9; + y_egset_l_2 = -0.1; + y_egset_l_3 = -0.1; + y_egset_l_4 = 0.1; + y_egset_l_5 = 0.000625; + y_egset_l_6 = 0.000625; + y_egset_l_7 = 0.000625; + y_egset_l_8 = 0.000625; + y_egsl_l_1 = 1.04483163655; + y_egsl_l_2 = -0.633546297018; + y_egsl_l_3 = -0.134688612832; + y_egsl_l_4 = -0.0215581541096; + y_egsl_l_5 = 0.744961427412; + y_egsl_l_6 = -0.00143256549309; + y_egsl_l_7 = 0.00176517379444; + y_egslt_l_1 = 0.9; + y_egslt_l_2 = -0.1; + y_egslt_l_3 = 0.1; + y_egslt_l_4 = 0.000625; + y_egslt_l_5 = 0.000625; + y_egslt_l_6 = 0.000625; + y_egslt_l_7 = 0.000625; + y_eh_l_1 = 1.3576278254; + y_eh_l_2 = 0.0130993143616; + y_eh_l_3 = -0.164666195693; + y_eh_l_4 = -0.206060944067; + y_eh_l_5 = -0.0282729007489; + y_eh_l_6 = 0.0282729007489; + y_eh_l_7 = -0.000786966438108; + y_emn_l_2 = 0.928554219554; + y_emn_l_3 = 0.0714457804463; + y_emo_l_1 = 0.819289500318; + y_emo_l_2 = -0.180710499682; + y_emo_l_3 = 1.31018224516; + y_emo_l_4 = 0.180710499682; + y_emo_l_5 = 0.0135818692772; + y_emo_l_6 = 0.00278890259237; + y_emo_l_7 = -0.0163707718696; + y_emo_l_8 = 0.723524924437; + y_emo_l_9 = -0.404694213855; + y_emp_l_1 = 40.1856146542; + y_emp_l_2 = 0.048026; + y_emp_l_3 = -0.048026; + y_emp_l_4 = -0.048026; + y_emp_l_5 = 0.048026; + y_emp_l_6 = 0.022115; + y_ex_l_1 = 0.892272127137; + y_ex_l_2 = -0.107727872863; + y_ex_l_3 = -0.107727872863; + y_ex_l_4 = -0.107727872863; + y_ex_l_5 = 0.107727872863; + y_ex_l_6 = 0.107727872863; + y_ex_l_7 = 0.0148164224533; + y_ex_l_8 = -0.0045419370785; + y_ex_l_9 = -0.0102744853748; + y_ex_l_10 = 1.01585705046; + y_fcbn_l_2 = -5.55068537239; + y_fcbn_l_3 = 6.86052021077; + y_fcbn_l_4 = -2.52909715822; + y_fcbn_l_5 = 1.9133340876; + y_fcbn_l_6 = -35.2463013113; + y_fcbn_l_7 = 0.305928232246; + y_fcbn_l_8 = 0.305928232246; + y_fgdp_l_2 = 0.01; + y_fgdpt_l_1 = 0.9; + y_fgdpt_l_2 = 0.1; + y_fgdpt_l_3 = 0.000625; + y_fgdpt_l_4 = 0.000625; + y_fgdpt_l_5 = 0.000625; + y_fgdpt_l_6 = 0.000625; + y_fnicn_l_1 = 0.993277528339; + y_fnicn_l_2 = 0.00672247166135; + y_fnicn_l_4 = 0.537028034851; + y_fnicn_l_5 = -0.537028034851; + y_fnicn_l_6 = -0.66631256176; + y_fnicn_l_7 = 0.66631256176; + y_fnicn_l_8 = 0.892965336399; + y_fniln_l_1 = 0.982046754178; + y_fniln_l_3 = 0.692942512139; + y_fniln_l_4 = 0.0100008124223; + y_fniln_l_5 = 0.00373870870246; + y_fniln_l_6 = 0.315405113519; + y_fniln_l_7 = -0.315405113519; + y_fniln_l_8 = -0.0591384587847; + y_fniln_l_9 = 0.0591384587847; + y_fniln_l_10 = 0.00421372469752; + y_fnirn_l_2 = -169.102652771; + y_fpc_l_2 = 0.0025; + y_fpi10_1 = 0.156993726433; + y_fpi10_2 = 0.156993726433; + y_fpi10_3 = 0.156993726433; + y_fpi10_4 = 0.156993726433; + y_fpi10_5 = 0.372025094268; + y_fpi10_6 = 0.32214582784; + y_fpi10t_1 = 0.95; + y_fpi10t_2 = 0.05; + y_fpic_1 = 0.678829880162; + y_fpic_2 = 0.321170119838; + y_fpxr_l_1 = 0.048; + y_fpxr_l_2 = -0.048; + y_fpxr_l_3 = -0.048; + y_fpxr_l_4 = 0.048; + y_fpxr_l_5 = 0.563832456119; + y_fpxr_l_6 = -0.726654492224; + y_fpxr_l_7 = 0.162822036105; + y_fpxrr_l_1 = 1.18364909386; + y_fpxrr_l_2 = -0.00291888934318; + y_fpxrr_l_3 = -0.211089676177; + y_fpxrr_l_4 = 0.00302407543125; + y_frl10_1 = 0.988458285734; + y_frl10_2 = -0.29200997295; + y_frl10_3 = -0.0655047670227; + y_frl10_4 = 0.369056454239; + y_frl10_5 = 0.12455118125; + y_frl10_6 = -0.12455118125; + y_frs10_1 = 4.78434763861; + y_frs10_2 = 0.0; + y_frs10_3 = 0.25; + y_frs10_4 = 0.25; + y_frs10_5 = 0.25; + y_frs10_6 = 0.25; + y_frs10_7 = 0.0; + y_frs10_8 = 0.0; + y_frstar_1 = 0.95; + y_frstar_2 = 0.05; + y_frstar_3 = -0.0125; + y_frstar_4 = -0.0125; + y_frstar_5 = -0.0125; + y_frstar_6 = -0.0125; + y_ftcin_l_2 = 190.397828213; + y_fxgap_1 = 1.29072367633; + y_fxgap_2 = -0.468009114875; + y_fxgap_3 = -0.0166666666667; + y_fxgap_4 = 0.00416666666667; + y_fxgap_5 = 0.00833333333333; + y_fxgap_6 = 0.0125; + y_fxgap_7 = 0.0125; + y_fxgap_8 = -0.0166666666667; + y_fxgap_9 = 0.00833333333333; + y_fxgap_10 = -0.0166666666667; + y_fxgap_11 = 0.00416666666667; + y_fxgap_12 = 0.05; + y_fxgap_13 = 0.0373455901902; + y_fynicn_l_2 = 0.203972136271; + y_fyniln_l_2 = 0.344642504397; + y_gfdbtnp_l_2 = 0.984645217482; + y_gfdbtnp_l_3 = 0.0737924242446; + y_gfdbtnp_l_4 = -0.0584376417269; + y_ugfsrp_1 = 0.947688; + y_uleg_l_1 = -0.0162972181781; + y_uleg_l_2 = 0.0162972181781; + y_uleg_l_3 = 0.1; + y_gfexpn_l_2 = 0.0964148144871; + y_gfexpn_l_3 = 0.19363872408; + y_gfexpn_l_4 = 0.600944699108; + y_gfexpn_l_5 = 0.109001762325; + y_gfintn_l_2 = 34.038852147; + y_gfrecn_l_2 = 0.5764571204; + y_gfrecn_l_3 = 0.0743675317358; + y_gfrecn_l_4 = 5.57251231588; + y_gfrecn_l_5 = 0.349175347864; + y_gtr_l_2 = 7.39501037898; + y_gtr_l_3 = 7.39501037898; + y_gtrd_1 = -0.000176387604876; + y_gtrd_2 = -0.000206546235356; + y_gtrd_3 = -4.93246174231e-5; + y_gtrd_4 = -4.93246174231e-5; + y_gtrd_5 = -4.93246174231e-5; + y_gtrd_6 = 0.862481931486; + y_gtrd_7 = 0.000309352740077; + y_hgemp_1 = 0.9; + y_hgemp_2 = 40.0; + y_hgemp_3 = -40.0; + y_hggdp_1 = 400.0; + y_hggdp_2 = -400.0; + y_hgpbfir_1 = 0.975; + y_hgpbfir_2 = 10.0; + y_hgpbfir_3 = 10.0; + y_hgpbfir_4 = -10.0; + y_hgpbfir_5 = -10.0; + y_hgpbfir_6 = -10.0; + y_hgpbfir_7 = 10.0; + y_hgpkir_1 = 0.9; + y_hgpkir_2 = 43.1298484247; + y_hgpkir_3 = -43.0591386594; + y_hgynid_1 = 454.348916939; + y_hgynid_2 = -54.3489169394; + y_hgynid_3 = -400.0; + y_hgynid_4 = -455.23665293; + y_hgynid_5 = 55.2366529304; + y_hgynid_6 = 400.0; + y_hks_1 = 384.31948476; + y_hks_2 = -384.31948476; + y_hks_3 = 15.68051524; + y_hks_4 = -15.68051524; + y_hlept_1 = 400.0; + y_hlept_2 = 400.0; + y_hlept_3 = -400.0; + y_hmfpt_1 = 0.95; + y_hqlfpr_1 = 0.95; + y_hqlww_1 = 0.95; + y_huqpct_1 = 0.95; + y_huxb_1 = 0.324768405324; + y_huxb_2 = 0.95; + y_hxbt_1 = 0.725; + y_hxbt_2 = 0.725; + y_hxbt_3 = 290.0; + y_hxbt_4 = -290.0; + y_hxbt_5 = 0.275; + y_jccan_l_2 = 0.82051735145; + y_jccan_l_3 = -0.948637916333; + y_jccan_l_4 = 0.121328058188; + y_jccan_l_5 = 0.128120564883; + y_jccan_l_6 = 1.35223326447; + y_jccan_l_7 = 0.128120564883; + y_jkcd_l_2 = 4.66817353822; + y_kbfi_l_2 = 0.0281084105505; + y_kbfi_l_3 = -0.0265200751536; + y_kbfi_l_4 = 0.0281084105505; + y_kbfi_l_5 = -0.248867790412; + y_kbfi_l_6 = 0.971891589449; + y_kcd_l_2 = 0.066147038262; + y_kcd_l_3 = -0.246673633735; + y_kcd_l_4 = 0.933852961738; + y_kh_l_2 = 0.00873032740269; + y_kh_l_3 = -0.249249311699; + y_kh_l_4 = 0.991269672597; + y_ki_l_1 = 1.44204786648; + y_ki_l_2 = 0.014692062549; + y_ki_l_3 = 0.250723990347; + y_ki_l_4 = -0.456739929026; + y_ki_l_5 = 0.0711962153783; + y_ki_l_6 = -0.307228143176; + y_ks_l_1 = 0.0025; + y_leg_l_1 = 0.248485878175; + y_leg_l_2 = 0.751514121825; + y_leh_l_2 = 0.813979789462; + y_leh_l_3 = 0.132451786431; + y_leh_l_4 = 0.0535684241064; + y_leo_l_1 = 20.7652726744; + y_leo_l_2 = 0.756667597034; + y_leo_l_3 = -15.6501866511; + y_leo_l_4 = -0.756667597034; + y_leo_l_5 = -0.0164258334824; + y_leppot_l_2 = -0.0110028694424; + y_leppot_l_3 = -1.10028694424; + y_leppot_l_4 = -0.857254870696; + y_lf_l_2 = 1.58659431972; + y_lfpr_1 = 0.432392517171; + y_lfpr_2 = 0.567607482829; + y_lfpr_3 = -0.000875189202097; + y_lfpr_4 = 0.000875189202097; + y_lhp_l_1 = 1.00059088506; + y_lhp_l_2 = 0.202289789801; + y_lhp_l_3 = -0.202880674857; + y_lhp_l_4 = 0.372064184885; + y_lhp_l_5 = 0.627935815115; + y_lhp_l_6 = -0.755331857052; + y_lhp_l_7 = -0.00156983953779; + y_lhp_l_8 = 0.127396041937; + y_lhp_l_9 = 0.000318490104843; + y_lur_1 = -96.2208093896; + y_lur_2 = 96.2208093896; + y_lurnat_1 = 0.95; + y_lww_l_1 = 0.804289649347; + y_lww_l_2 = 0.00170379588201; + y_lww_l_3 = 0.195710350653; + y_lww_l_4 = 0.318481647196; + y_lww_l_5 = -0.318481647196; + y_lww_l_6 = -0.00079620411799; + y_mfpt_l_1 = 0.0025; + y_pbfir_l_1 = 0.0025; + y_pcdr_l_1 = 1.50984819434; + y_pcdr_l_2 = -0.509848194342; + y_pcer_l_1 = 0.248860953365; + y_pcer_l_2 = -0.248860953365; + y_pcer_l_3 = -0.248860953365; + y_pcer_l_4 = 0.248860953365; + y_pcfr_l_1 = 1.21019336782; + y_pcfr_l_2 = -0.14928038046; + y_pcfr_l_3 = -0.365198296745; + y_pcfr_l_4 = 0.318574001625; + y_pcfr_l_5 = -0.338884189342; + y_pcfr_l_6 = 0.333798755712; + y_pchr_l_1 = 1.59806398567; + y_pchr_l_2 = -0.598063985667; + y_pcnia_l_1 = 0.0025; + y_pcor_l_1 = -0.1436; + y_pcor_l_2 = 0.1436; + y_pcor_l_3 = -0.217; + y_pcor_l_4 = 0.217; + y_pcpi_l_2 = 0.43067430272; + y_pcpix_l_2 = 0.426412064374; + y_pcxfe_l_1 = 0.0025; + y_pegfr_l_1 = 0.0025; + y_pegsr_l_1 = 0.0025; + y_pgfl_l_1 = 0.525153490957; + y_pgsl_l_1 = 0.514419453205; + y_phouse_l_1 = 1.89031776892; + y_phouse_l_2 = -0.901886995515; + y_phouse_l_3 = 0.0115692265899; + y_phouse_l_4 = 0.0115692265899; + y_phr_l_1 = 0.0025; + y_pic4_1 = 100.0; + y_pic4_2 = -100.0; + y_picnia_1 = 15.96; + y_picnia_2 = -15.96; + y_picnia_3 = 29.04; + y_picnia_4 = -29.04; + y_picx4_1 = 100.0; + y_picx4_2 = -100.0; + y_picxfe_1 = 0.404860664116; + y_picxfe_2 = 0.591171818183; + y_picxfe_3 = 0.00396751770099; + y_picxfe_4 = 0.462045372577; + y_picxfe_5 = -0.462045372577; + y_pieci_1 = 0.00293156716662; + y_pieci_2 = 0.00293156716662; + y_pieci_3 = 0.00293156716662; + y_pieci_4 = 0.146578358331; + y_pieci_5 = 0.839226144659; + y_pieci_6 = 0.00540079551024; + y_pieci_7 = 0.00540079551024; + y_pieci_8 = -2.16031820409; + y_pieci_9 = -0.0143209721548; + y_pieci_10 = 0.0143209721548; + y_pieci_11 = 0.327959270689; + y_pieci_12 = -0.327959270689; + y_pigdp_1 = 400.0; + y_pigdp_2 = -400.0; + y_pipxnc_1 = -796.0; + y_pipxnc_2 = 0.462801; + y_pipxnc_3 = -0.462801; + y_pipxnc_4 = 368.389596; + y_pipxnc_5 = 0.229745; + y_pipxnc_6 = -0.229745; + y_pipxnc_7 = 182.87702; + y_pipxnc_8 = -14.9334031956; + y_pipxnc_9 = 14.9334031956; + y_pipxnc_10 = 10.0; + y_pipxnc_11 = -10.0; + y_pkbfir_1 = 0.960531663984; + y_pkbfir_2 = 1.05983283594; + y_pl_l_1 = 0.0025; + y_pmo_l_1 = 0.622318401629; + y_pmo_l_2 = 0.377681598371; + y_pmo_l_3 = 0.00731956262431; + y_pmo_l_4 = -0.00731956262431; + y_pmo_l_5 = -0.629637964254; + y_pmo_l_6 = 0.234396660333; + y_pmo_l_7 = -0.234396660333; + y_pmo_l_8 = 0.765603339667; + y_pmp_l_2 = 1.05645668526; + y_poilr_l_1 = 1.17135063067; + y_poilr_l_2 = -0.346197996438; + y_poilr_l_3 = -0.390345197801; + y_poilr_l_4 = 0.79951907837; + y_ptr_1 = 0.9; + y_ptr_2 = 0.05; + y_ptr_3 = 0.05; + y_pxb_l_2 = 1.01772402773; + y_pxnc_l_1 = 0.0025; + y_pxp_l_1 = 0.6469; + y_pxp_l_2 = -0.6469; + y_pxp_l_3 = 0.3531; + y_pxp_l_4 = -0.3531; + y_pxr_l_1 = 0.0025; + y_qebfi_l_2 = 0.664481948351; + y_qebfi_l_3 = 0.0787039173848; + y_qebfi_l_4 = -0.0787039173848; + y_qebfi_l_5 = 7.87039173848; + y_qec_l_1 = 0.935665935123; + y_qec_l_2 = 0.0166517759473; + y_qec_l_3 = -0.139711201786; + y_qec_l_4 = 0.135400942735; + y_qec_l_5 = 0.0519925479811; + y_qecd_l_2 = 3.98656310426; + y_qecd_l_3 = 0.00498320388032; + y_qecd_l_4 = 0.00498320388032; + y_qecd_l_5 = 0.00498320388032; + y_qecd_l_6 = 0.00498320388032; + y_qecd_l_7 = 0.00498320388032; + y_qecd_l_8 = 0.00498320388032; + y_qecd_l_9 = 0.00498320388032; + y_qecd_l_10 = 0.00498320388032; + y_qecd_l_11 = -0.0232956396718; + y_qecd_l_12 = -0.584353967629; + y_qecd_l_13 = -0.0242284661483; + y_qeh_l_2 = 24.6010652056; + y_qeh_l_3 = 0.0153756657535; + y_qeh_l_4 = 0.0153756657535; + y_qeh_l_5 = 0.0153756657535; + y_qeh_l_6 = 0.0153756657535; + y_qeh_l_7 = 0.0153756657535; + y_qeh_l_8 = 0.0153756657535; + y_qeh_l_9 = 0.0153756657535; + y_qeh_l_10 = 0.0153756657535; + y_qeh_l_11 = 0.0153756657535; + y_qeh_l_12 = 0.0153756657535; + y_qeh_l_13 = 0.0153756657535; + y_qeh_l_14 = 0.0153756657535; + y_qeh_l_15 = 0.0153756657535; + y_qeh_l_16 = 0.0153756657535; + y_qeh_l_17 = 0.0153756657535; + y_qeh_l_18 = 0.0153756657535; + y_qeh_l_19 = -0.0270350700995; + y_qkir_l_1 = 0.00188536673771; + y_qlf_l_2 = 1.58692282562; + y_qlww_l_1 = 0.0025; + y_qpxnc_l_1 = 2.98507462687; + y_qpxnc_l_2 = -2.98507462687; + y_qpxnc_l_3 = -1.98507462687; + y_qpxnc_l_4 = 1.98507462687; + y_qpxp_l_1 = 0.7195976338; + y_qpxp_l_2 = -0.7195976338; + y_qynidn_l_1 = 0.354822592523; + y_qynidn_l_2 = 1.13587229235; + y_qynidn_l_3 = -0.135872292349; + y_rbbbp_1 = -0.189051; + y_rbbbp_2 = 0.848879; + y_rbbbp_3 = 0.160481423829; + y_rbfi_1 = -2.21124682364; + y_rbfi_2 = 0.395; + y_rbfi_3 = 0.395; + y_rbfi_4 = -0.395; + y_rbfi_5 = -0.5; + y_rbfi_6 = 0.5; + y_rcar_1 = 1.22665328945; + y_rcar_2 = 0.0; + y_rcar_3 = 0.696748171914; + y_rcar_4 = 0.101669335039; + y_rcar_5 = 0.201582493047; + y_rccd_1 = 100.0; + y_rcch_1 = 100.0; + y_rcch_2 = -0.0545840410668; + y_rcch_3 = 0.7953; + y_rcch_4 = 79.53; + y_rcgain_1 = 0.32854362351; + y_rcgain_2 = 0.225785775119; + y_rcgain_3 = -0.225785775119; + y_reqp_1 = 0.808086; + y_reqp_2 = 0.795819; + y_reqp_3 = -0.643090192434; + y_rfynic_1 = 1.00400815341; + y_rfynic_2 = -0.49108746803; + y_rfynic_3 = -0.144424360986; + y_rfynic_4 = 0.631503675605; + y_rfynil_1 = 0.884413145643; + y_rfynil_2 = -0.00726474303036; + y_rfynil_3 = -0.171195169347; + y_rfynil_4 = 0.0265702779079; + y_rfynil_5 = -0.132818819092; + y_rfynil_6 = 0.0876033907073; + y_rfynil_7 = 0.261434600384; + y_rfynil_8 = 0.0179349568622; + y_rg10p_1 = -0.460658806872; + y_rg10p_2 = 0.228721864424; + y_rg10p_3 = 0.920104088065; + y_rg10p_4 = 0.423854051406; + y_rg10p_5 = -0.210447922486; + y_rg30p_1 = -0.624829467707; + y_rg30p_2 = 0.134994250522; + y_rg30p_3 = 0.938108605708; + y_rg30p_4 = 0.586157900756; + y_rg30p_5 = -0.126639268136; + y_rg5p_1 = -0.349564481; + y_rg5p_2 = 0.90221329312; + y_rg5p_3 = 0.315381721561; + y_rgfint_1 = 0.845677566688; + y_rgfint_2 = 0.154322433312; + y_rgfint_3 = 0.00556931000493; + y_rgfint_4 = -0.00556931000493; + y_rgw_1 = 0.00495; + y_rgw_2 = 0.00271; + y_rgw_3 = 0.00129; + y_rgw_4 = 0.00105; + y_rme_1 = 0.660306961037; + y_rme_2 = 0.884200704474; + y_rme_3 = -0.544507665511; + y_rme_4 = -0.102549417082; + y_rrff_1 = -0.25; + y_rrff_2 = -0.25; + y_rrff_3 = -0.25; + y_rrff_4 = -0.25; + y_rrtr_1 = 0.97; + y_rrtr_2 = 0.03; + y_rspnia_1 = 7.62633280279; + y_rspnia_2 = -7.62633280279; + y_rtb_1 = 0.799718792152; + y_rtb_2 = 0.11137355158; + y_rtb_3 = 0.770122562667; + y_rtb_4 = -0.681214906399; + y_rtbfi_l_2 = 0.0576949599629; + y_rtbfi_l_3 = 5.76949599629; + y_rtbfi_l_4 = -0.0576949599629; + y_rtbfi_l_5 = -0.0123862111793; + y_rtbfi_l_6 = 0.129531747065; + y_rtbfi_l_7 = 0.0; + y_rtbfi_l_8 = -0.260110434765; + y_rtbfi_l_9 = 0.943576128374; + y_rtinv_1 = 0.00912489842966; + y_rtinv_2 = -0.00912489842966; + y_rtinv_3 = 0.0330561789534; + y_rtinv_4 = 0.0356516398072; + y_rtinv_5 = 0.0329826447805; + y_rtinv_6 = 0.0355066663735; + y_rtinv_7 = -0.066038823734; + y_tcin_l_2 = 8.35657418879; + y_tpn_l_2 = 7.01937651683; + y_tpn_l_3 = 1.18756132659; + y_tpn_l_4 = -0.187561326587; + y_trci_1 = 0.00706626139452; + y_trci_2 = 0.810247648208; + y_trci_3 = -0.810247648208; + y_trci_4 = -0.00572542167653; + y_trp_1 = 0.603942358608; + y_trp_2 = -0.603942358608; + y_trp_3 = 0.236576213581; + y_trp_4 = -0.236576213581; + y_trp_5 = 0.000630587773923; + y_trptd_1 = 0.420215062775; + y_trptd_2 = -0.420215062775; + y_trptd_3 = -0.55; + y_trptd_4 = -0.422749789232; + y_trptd_5 = 0.422749789232; + y_trptd_6 = -0.5; + y_trpts_1 = -0.0180202713644; + y_trpts_2 = 0.0225987818683; + y_trpts_3 = -0.00457851050393; + y_trpts_4 = 0.1; + y_trpts_5 = 0.00075; + y_tryh_1 = 0.144437010525; + y_tryh_2 = -0.0944218552605; + y_tryh_3 = -0.0500151552646; + y_uxbt_l_1 = 0.0025; + y_uynicpnr_1 = 0.779183; + y_vbfi_1 = 5.96826486935; + y_vbfi_2 = 1.41987523928; + y_vbfi_3 = -1.50480877253; + y_vbfi_4 = -1.50480877253; + y_wpon_l_2 = 0.99460869287; + y_wpon_l_3 = 0.00146536714744; + y_wpon_l_4 = 0.408461833894; + y_wpon_l_5 = -0.408461833894; + y_wpon_l_6 = 0.0498372673814; + y_wpon_l_7 = -0.0443498822123; + y_wpon_l_8 = -0.00103486242602; + y_wpon_l_9 = 0.000938784388547; + y_wpon_l_10 = 0.000938784388547; + y_wpon_l_11 = 0.00466659001196; + y_wpon_l_12 = -0.00372780562341; + y_wpsn_l_1 = 1.13587229235; + y_wpsn_l_2 = -0.135872292349; + y_wpsn_l_3 = -0.25; + y_wpsn_l_4 = 0.25; + y_xb_l_2 = 1.0; + y_xb_l_3 = -1.0; + y_xbn_l_2 = 1.0198018271; + y_xbn_l_3 = 1.0198018271; + y_xbn_l_4 = 1.31175365227; + y_xbn_l_5 = -1.33155547937; + y_xbn_l_6 = -1.33155547937; + y_xbo_l_1 = 0.0132470548943; + y_xbt_l_1 = 0.725; + y_xbt_l_2 = 0.725; + y_xbt_l_3 = 0.725; + y_xbt_l_4 = 0.275; + y_xbtr_l_1 = 0.95; + y_xfs_l_1 = 0.6849; + y_xfs_l_2 = -0.6849; + y_xfs_l_3 = 0.0386; + y_xfs_l_4 = -0.0386; + y_xfs_l_5 = 0.1324; + y_xfs_l_6 = -0.1324; + y_xfs_l_7 = 0.0429; + y_xfs_l_8 = -0.0429; + y_xfs_l_9 = 0.0223; + y_xfs_l_10 = -0.0223; + y_xfs_l_11 = 0.0395; + y_xfs_l_12 = -0.0395; + y_xfs_l_13 = 0.0691; + y_xfs_l_14 = -0.0691; + y_xfs_l_15 = 0.1203; + y_xfs_l_16 = -0.1203; + y_xfs_l_17 = -0.1399; + y_xfs_l_18 = 0.1399; + y_xfs_l_19 = -0.0101; + y_xfs_l_20 = 0.0101; + y_xfsn_l_2 = 1.00337294235; + y_xfsn_l_3 = -0.00363305240167; + y_xfsn_l_4 = -0.00337294235067; + y_xfsn_l_5 = -0.544852165261; + y_xfsn_l_6 = 0.541479222911; + y_xgap_1 = 100.0; + y_xgap_2 = -100.0; + y_xgap2_1 = 100.0; + y_xgap2_2 = -100.0; + y_xgdp_l_1 = 0.9985; + y_xgdp_l_2 = -0.9985; + y_xgdp_l_3 = 0.6264; + y_xgdp_l_4 = -1.2513; + y_xgdp_l_5 = 0.6249; + y_xgdpn_l_2 = 1.0564013312; + y_xgdpn_l_3 = 0.021847772281; + y_xgdpn_l_4 = 0.0674519622926; + y_xgdpn_l_5 = -0.149062669624; + y_xgdpn_l_6 = 0.00362083951871; + y_xgdpn_l_7 = 0.00336160385466; + y_xgdpn_l_8 = 0.543020588122; + y_xgdpn_l_9 = -0.539658984268; + y_xp_l_1 = 0.6526679404; + y_xp_l_2 = -0.6526679404; + y_xp_l_3 = 0.0361108836; + y_xp_l_4 = -0.0361108836; + y_xp_l_5 = 0.11825695358; + y_xp_l_6 = -0.11825695358; + y_xp_l_7 = 0.04216893278; + y_xp_l_8 = -0.04216893278; + y_xp_l_9 = 0.0365822346; + y_xp_l_10 = -0.0365822346; + y_xp_l_11 = 0.114213055; + y_xp_l_12 = -0.114213055; + y_ydn_l_2 = 0.998336445483; + y_ydn_l_3 = 1.13631857158; + y_ydn_l_4 = -0.136318571582; + y_yh_l_2 = 0.526533658207; + y_yh_l_3 = 0.178799300517; + y_yh_l_4 = 0.294667041275; + y_yhgap_1 = 100.0; + y_yhgap_2 = -100.0; + y_yhibn_l_2 = 63.3559682371; + y_yhl_l_2 = -1.16884651007; + y_yhln_l_2 = 1.14236648073; + y_yhp_l_2 = -1.10635973458; + y_yhp_l_3 = 0.946560228789; + y_yhp_l_4 = 0.0534397712107; + y_yhpgap_1 = 100.0; + y_yhpgap_2 = -100.0; + y_yhpntn_l_2 = 1.19125575899; + y_yhpntn_l_3 = 1.19125575899; + y_yhpntn_l_4 = 1.19125575899; + y_yhpntn_l_5 = -1.31798027859; + y_yhpntn_l_6 = 9.31230191482; + y_yhpntn_l_7 = -1.11436836489; + y_yhpntn_l_8 = -5.43026982685; + y_yhpntn_l_9 = -0.71417194978; + y_yhpntn_l_10 = -1.64093920349; + y_yhptn_l_2 = 0.975134043217; + y_yhptn_l_3 = 0.563603989214; + y_yhptn_l_4 = 0.102159206272; + y_yhptn_l_5 = 0.268955804126; + y_yhptn_l_6 = 0.0652810003888; + y_yhshr_l_2 = 0.99999334858; + y_yhshr_l_3 = -0.99999334858; + y_yhsn_l_2 = 8.07609651707; + y_yhsn_l_3 = 2.34637489687; + y_yhsn_l_4 = 4.27777403212; + y_yhsn_l_5 = -1.78462125185; + y_yhsn_l_6 = -11.6695043163; + y_yhsn_l_7 = -0.272296812186; + y_yhsn_l_8 = 17.3716235947; + y_yhsn_l_9 = 0.0261769342453; + y_yhtgap_1 = 100.0; + y_yhtgap_2 = -100.0; + y_yhtn_l_2 = 1.00122246375; + y_ykbfin_l_2 = 0.501135005995; + y_ykbfin_l_3 = 0.498864994005; + y_ykin_l_2 = 15.1326363291; + y_ykin_l_3 = 0.501553881106; + y_ykin_l_4 = 0.498446118894; + y_ynicpn_l_2 = 7.69278337234; + y_ynicpn_l_3 = -4.79530526339; + y_ynicpn_l_4 = -1.22202005096; + y_ynicpn_l_5 = 8.96683950232; + y_ynicpn_l_6 = -0.675458057991; + y_ynidn_l_1 = 0.000734588128108; + y_ynidn_l_2 = 0.683167062078; + y_ynidn_l_3 = -0.000507771585891; + y_ynidn_l_4 = -0.790436568589; + y_ynidn_l_5 = 0.107269506511; + y_ynidn_l_6 = 0.209563431411; + y_ynidn_l_7 = -0.000157528146717; + y_ynidn_l_8 = -0.209563431411; + y_yniln_l_2 = 0.977159070276; + y_yniln_l_3 = 0.829114291162; + y_yniln_l_4 = 0.829114291162; + y_yniln_l_5 = 0.0418084260005; + y_yniln_l_6 = 0.0418084260005; + y_yniln_l_7 = 0.129077282837; + y_yniln_l_8 = 0.129077282837; + y_ynin_l_2 = 0.999999935846; + y_ynin_l_3 = 1.17104491968; + y_ynin_l_4 = 0.0640520865577; + y_ynin_l_5 = -0.0484572291581; + y_ynin_l_6 = -0.186639777078; + y_ynirn_l_1 = 7.33377354801; + y_ynirn_l_2 = 0.951263114856; + y_ynirn_l_3 = -0.951263114856; + y_ynirn_l_4 = 0.00548690935542; + y_ynirn_l_5 = -0.00548690935542; + y_ypn_l_2 = 0.988173952374; + y_ypn_l_3 = 0.549385156514; + y_ypn_l_4 = 0.159614656674; + y_ypn_l_5 = 0.291000186812; + y_zdivgr_1 = 0.00975726425743; + y_zdivgr_2 = 0.990242735743; + y_zebfi_1 = -0.000431144211955; + y_zebfi_2 = -0.00050714173603; + y_zebfi_3 = -3.88181916088e-5; + y_zebfi_4 = 0.00016798757544; + y_zebfi_5 = -0.000975251482943; + y_zebfi_6 = 0.000417269685018; + y_zebfi_7 = 9.80402248148e-6; + y_zebfi_8 = 0.00040254489385; + y_zebfi_9 = 0.000145632881593; + y_zebfi_10 = 0.000809116564154; + y_zebfi_11 = 0.000691481740712; + y_zebfi_12 = -0.00152462990113; + y_zebfi_13 = 0.000182102122415; + y_zebfi_14 = 0.000170960242897; + y_zebfi_15 = 0.0142945657655; + y_zebfi_16 = -0.00425222899975; + y_zebfi_17 = -0.00503049733108; + y_zebfi_18 = -0.00112440248315; + y_zebfi_19 = -0.0038874369515; + y_zebfi_20 = 0.00035570453849; + y_zebfi_21 = -0.00035570453849; + y_zecd_1 = -0.000424433044911; + y_zecd_2 = -0.000566112732916; + y_zecd_3 = -0.000427835415485; + y_zecd_4 = 4.27545061866e-6; + y_zecd_5 = -0.00133363746841; + y_zecd_6 = 0.00178510275432; + y_zecd_7 = -0.000271474405975; + y_zecd_8 = 0.000459611864377; + y_zecd_9 = 0.000428608849069; + y_zecd_10 = -0.00111248088805; + y_zecd_11 = 3.61133130939e-5; + y_zecd_12 = 7.97590705793e-5; + y_zecd_13 = 0.00141410574269; + y_zecd_14 = -0.000639602744318; + y_zecd_15 = -0.00010841426451; + y_zecd_16 = 0.000210363124201; + y_zecd_17 = 0.000178061664134; + y_zecd_18 = 0.000146912749167; + y_zecd_19 = -0.000139880426754; + y_zecd_20 = -3.38007573296e-5; + y_zecd_21 = 0.000166975793706; + y_zecd_22 = 0.000113506936821; + y_zecd_23 = 0.000124123127885; + y_zecd_24 = -0.000203591971486; + y_zecd_25 = 5.7989188193e-5; + y_zecd_26 = 0.000114280775871; + y_zecd_27 = 0.00255088738447; + y_zecd_28 = -0.001880611807; + y_zecd_29 = 0.0308598105755; + y_zecd_30 = -0.00201324622316; + y_zecd_31 = -0.0365513581269; + y_zecd_32 = -0.00465896135484; + y_zecd_33 = 0.0123637551294; + y_zeco_1 = -7.52202049496e-5; + y_zeco_2 = -7.94406933181e-5; + y_zeco_3 = -2.05931699614e-5; + y_zeco_4 = 0.000100439779498; + y_zeco_5 = 2.12832185698e-5; + y_zeco_6 = 1.70353153588e-5; + y_zeco_7 = 5.5012376381e-5; + y_zeco_8 = 3.68085672111e-5; + y_zeco_9 = -0.000630171036922; + y_zeco_10 = 0.000273875586514; + y_zeco_11 = 0.000133019756131; + y_zeco_12 = -3.46619140531e-5; + y_zeco_13 = 7.48142887307e-5; + y_zeco_14 = -0.000130139477521; + y_zeco_15 = -0.000574849476126; + y_zeco_16 = 0.000315791553755; + y_zeco_17 = 0.000397005436297; + y_zeco_18 = 2.60636593368e-5; + y_zeco_19 = -6.06591388527e-5; + y_zeco_20 = -5.86151697491e-6; + y_zeco_21 = 4.60869299242e-5; + y_zeco_22 = -3.67533909379e-5; + y_zeco_23 = 0.000205501772024; + y_zeco_24 = -0.000240937714399; + y_zeco_25 = -0.000131812287659; + y_zeco_26 = -8.99812036284e-5; + y_zeco_27 = 0.0011735331967; + y_zeco_28 = 0.0732239724725; + y_zeco_29 = -0.0439002248803; + y_zeco_30 = -0.0221752554553; + y_zeco_31 = 0.00192935493602; + y_zeco_32 = -0.00907784707292; + y_zeh_1 = -7.82590644729e-5; + y_zeh_2 = -5.22177650099e-5; + y_zeh_3 = -5.11617748264e-5; + y_zeh_4 = 1.06118801985e-5; + y_zeh_5 = 0.00015247021427; + y_zeh_6 = 0.000115461916411; + y_zeh_7 = 6.94775905299e-5; + y_zeh_8 = 6.915284272e-6; + y_zeh_9 = 0.000501695975955; + y_zeh_10 = -0.000531489463237; + y_zeh_11 = -0.000152716722883; + y_zeh_12 = 4.42229725272e-5; + y_zeh_13 = 0.000171026724111; + y_zeh_14 = -0.000344325005483; + y_zeh_15 = 5.54746256755e-5; + y_zeh_16 = 4.07823169105e-5; + y_zeh_17 = -3.07861940478e-5; + y_zeh_18 = -4.58464253978e-6; + y_zeh_19 = 7.13641520669e-6; + y_zeh_20 = 1.47073943648e-5; + y_zeh_21 = 3.83084742308e-5; + y_zeh_22 = 3.27196755109e-5; + y_zeh_23 = -6.99285491718e-5; + y_zeh_24 = 4.55009661188e-7; + y_zeh_25 = 5.50461376242e-5; + y_zeh_26 = 2.86419262708e-5; + y_zeh_27 = 0.00106943231558; + y_zeh_28 = 0.00426630302385; + y_zeh_29 = -0.00573847474041; + y_zeh_30 = -0.00187609012218; + y_zeh_31 = -0.000659760712587; + y_zeh_32 = 0.00400802255132; + y_zgap05_1 = 0.0547936526434; + y_zgap05_2 = 0.945206347357; + y_zgap10_1 = 0.0300745581094; + y_zgap10_2 = 0.969925441891; + y_zgap30_1 = 0.014106588982; + y_zgap30_2 = 0.985893411018; + y_zgapc2_1 = -0.0141848331986; + y_zgapc2_2 = -0.00438957847118; + y_zgapc2_3 = -0.00608986499063; + y_zgapc2_4 = 0.00127453586676; + y_zgapc2_5 = -0.0426889990258; + y_zgapc2_6 = 0.00775994605046; + y_zgapc2_7 = 0.0191285668792; + y_zgapc2_8 = -0.00220795277592; + y_zgapc2_9 = 0.194384536968; + y_zgapc2_10 = -0.0764007234264; + y_zgapc2_11 = -0.0113246023485; + y_zgapc2_12 = -0.0155518339662; + y_zgapc2_13 = 0.0233897407937; + y_zgapc2_14 = 0.018008438872; + y_zlhp_1 = -0.000202321377434; + y_zlhp_2 = -6.54709155556e-5; + y_zlhp_3 = -0.000172024683014; + y_zlhp_4 = 3.13937564958e-5; + y_zlhp_5 = -0.00104747602255; + y_zlhp_6 = 0.000259883906459; + y_zlhp_7 = 0.00050790958016; + y_zlhp_8 = -4.19862687488e-5; + y_zlhp_9 = 0.000321668804678; + y_zlhp_10 = 0.000408423219508; + y_zlhp_11 = -0.00575496472936; + y_zlhp_12 = 0.00581682769254; + y_zlhp_13 = -0.000203485929498; + y_zlhp_14 = -0.00027154348171; + y_zlhp_15 = 0.685794690175; + y_zlhp_16 = -0.685794690175; + y_zlhp_17 = -0.685794690175; + y_zlhp_18 = 0.685794690175; + y_zlhp_19 = 0.000278310168582; + y_zlhp_20 = 0.000278310168582; + y_zpi10_1 = 0.0300745581094; + y_zpi10_2 = 0.969925441891; + y_zpi10f_1 = 0.0300745581094; + y_zpi10f_2 = 0.969925441891; + y_zpi5_1 = 0.0817876274963; + y_zpi5_2 = 0.0221868418868; + y_zpi5_3 = 0.0250194521826; + y_zpi5_4 = -9.00706244808e-5; + y_zpi5_5 = -0.145676547176; + y_zpi5_6 = -0.0311377360679; + y_zpi5_7 = -0.0294931929574; + y_zpi5_8 = -0.0275798582146; + y_zpi5_9 = 0.233887334416; + y_zpi5_10 = 0.871096149059; + y_zpi5_11 = 0.174192252057; + y_zpi5_12 = -0.0718402312689; + y_zpi5_13 = 0.0406637195158; + y_zpi5_14 = 0.0449446239851; + y_zpib5_1 = 21.9174610574; + y_zpib5_2 = -21.9174610574; + y_zpib5_3 = 0.945206347357; + y_zpic30_1 = 0.014106588982; + y_zpic30_2 = 0.985893411018; + y_zpicxfe_1 = 0.380818884672; + y_zpicxfe_2 = 0.00113182715476; + y_zpicxfe_3 = 0.00146351917605; + y_zpicxfe_4 = 0.00225729733693; + y_zpicxfe_5 = 0.0460967342223; + y_zpicxfe_6 = 0.0338772671906; + y_zpicxfe_7 = 0.0228924215171; + y_zpicxfe_8 = 0.0112105032823; + y_zpicxfe_9 = -0.0140156100481; + y_zpicxfe_10 = 0.0011222896601; + y_zpicxfe_11 = 0.00760121840982; + y_zpicxfe_12 = -0.00299260406007; + y_zpicxfe_13 = 0.0470383710002; + y_zpicxfe_14 = -0.0278318348119; + y_zpicxfe_15 = -0.00506170904133; + y_zpicxfe_16 = -0.00225028901719; + y_zpicxfe_17 = 0.00828470603822; + y_zpicxfe_18 = 0.500251545448; + y_zpicxfe_19 = 11.937795061; + y_zpicxfe_20 = -11.937795061; + y_zpicxfe_21 = 6.84395376806e-5; + y_zpicxfe_22 = -6.84395376806e-5; + y_zpicxfe_23 = -0.114076926212; + y_zpicxfe_24 = 45.6307704848; + y_zpicxfe_25 = -0.00383816812034; + y_zpicxfe_26 = 0.00383816812034; + y_zpicxfe_27 = -0.000695300677346; + y_zpicxfe_28 = 0.000695300677346; + y_zpieci_1 = -0.026022539351; + y_zpieci_2 = 0.00320414216918; + y_zpieci_3 = 0.00402676215955; + y_zpieci_4 = 0.00650489050087; + y_zpieci_5 = 0.202430424141; + y_zpieci_6 = 0.196252633802; + y_zpieci_7 = 0.195837958296; + y_zpieci_8 = 0.0246831983934; + y_zpieci_9 = -0.0328787076454; + y_zpieci_10 = 0.00135903909754; + y_zpieci_11 = 0.0229838005541; + y_zpieci_12 = -0.00862383586105; + y_zpieci_13 = 0.148708914616; + y_zpieci_14 = -0.0777266665551; + y_zpieci_15 = -0.0137748704693; + y_zpieci_16 = -0.00648469451174; + y_zpieci_17 = 0.0171597038548; + y_zpieci_18 = 0.393082529889; + y_zpieci_19 = -4.49541220961; + y_zpieci_20 = 4.49541220961; + y_zpieci_21 = 0.000154587412169; + y_zpieci_22 = -0.000154587412169; + y_zpieci_23 = 0.380795785368; + y_zpieci_24 = -152.318314147; + y_zpieci_25 = -0.0172443115476; + y_zpieci_26 = 0.0172443115476; + y_zpieci_27 = -0.00416159167724; + y_zpieci_28 = 0.00416159167724; + y_zrff10_1 = 0.0300745581094; + y_zrff10_2 = 0.969925441891; + y_zrff30_1 = 0.014106588982; + y_zrff30_2 = 0.985893411018; + y_zrff5_1 = 0.0547936526434; + y_zrff5_2 = 0.945206347357; + y_zyh_l_1 = 8.4030342164e-5; + y_zyh_l_2 = 0.000702312990074; + y_zyh_l_3 = 0.00059883124856; + y_zyh_l_4 = 0.000469068735294; + y_zyh_l_5 = -0.00211240097558; + y_zyh_l_6 = 0.000165660225273; + y_zyh_l_7 = -0.000778039304358; + y_zyh_l_8 = 0.000290681377414; + y_zyh_l_9 = 0.000905762637463; + y_zyh_l_10 = 0.00268742554252; + y_zyh_l_11 = 0.000682359720547; + y_zyh_l_12 = -0.00125638870298; + y_zyh_l_13 = -0.00185424331619; + y_zyh_l_14 = 0.00243409867739; + y_zyh_l_15 = 0.00416166538503; + y_zyh_l_16 = 0.000750155785081; + y_zyh_l_17 = -0.000192123546578; + y_zyh_l_18 = -0.000360282907619; + y_zyhp_l_1 = 0.000863735995891; + y_zyhp_l_2 = 0.00104399392499; + y_zyhp_l_3 = 0.000941268897539; + y_zyhp_l_4 = 0.000458916642717; + y_zyhp_l_5 = -0.00154443220559; + y_zyhp_l_6 = -0.000640772573525; + y_zyhp_l_7 = -0.000877216982815; + y_zyhp_l_8 = -4.0750285327e-5; + y_zyhp_l_9 = -0.00203693491032; + y_zyhp_l_10 = 0.00307902287603; + y_zyhp_l_11 = 0.00153367057932; + y_zyhp_l_12 = -0.000979443369808; + y_zyhp_l_13 = -0.00330791546136; + y_zyhp_l_14 = 0.00310317204747; + y_zyhp_l_15 = 0.00422069758369; + y_zyhp_l_16 = 0.000130762149267; + y_zyhp_l_17 = 5.34207087507e-5; + y_zyhp_l_18 = -0.00083494424337; + y_zyhp_l_19 = 0.00225147864855; + y_zyhp_l_20 = 0.000201405002604; + y_zyhp_l_21 = -0.000427498545256; + y_zyhp_l_22 = -0.000252078634627; + y_zyhpst_l_1 = 0.0005; + y_zyhst_l_1 = 0.0005; + y_zyht_l_1 = -0.000334830912493; + y_zyht_l_2 = 0.000473468268016; + y_zyht_l_3 = 0.000279807258553; + y_zyht_l_4 = 0.000327960879431; + y_zyht_l_5 = -0.00250027976398; + y_zyht_l_6 = 0.00088556815263; + y_zyht_l_7 = -0.00121488126811; + y_zyht_l_8 = 8.52432970988e-5; + y_zyht_l_9 = 0.00217284254807; + y_zyht_l_10 = 0.00313028920932; + y_zyht_l_11 = 0.00211420687194; + y_zyht_l_12 = 0.000248144569686; + y_zyht_l_13 = -0.000746405493548; + y_zyht_l_14 = 0.00274434958239; + y_zyht_l_15 = 0.00270824568765; + y_zyht_l_16 = 0.000679378874986; + y_zyht_l_17 = 0.000308986169352; + y_zyht_l_18 = -6.93511660157e-5; + y_zyht_l_19 = 0.00183760811329; + y_zyht_l_20 = 0.000509649917682; + y_zyht_l_21 = 9.47084299939e-5; + y_zyht_l_22 = 0.0002426388839; + y_zyhtst_l_1 = 0.0005; + y_zynid_1 = -0.000102077846072; + y_zynid_2 = 0.000348695205252; + y_zynid_3 = 0.000252250306328; + y_zynid_4 = 0.00020597993691; + y_zynid_5 = 0.000352649102887; + y_zynid_6 = -0.00091171528933; + y_zynid_7 = -0.000833252281803; + y_zynid_8 = 0.000222028205174; + y_zynid_9 = 0.00117029026307; + y_zynid_10 = -0.000704847602418; + y_zynid_11 = -0.00509852446865; + y_zynid_12 = 0.00166112741624; + y_zynid_13 = 0.000634556266817; + y_zynid_14 = 0.0013968199402; + y_zynid_15 = 0.00251911216766; + y_zynid_16 = -0.00251911216766; + y_zynid_17 = 0.0129976207113; + y_zynid_18 = -0.0129976207113; + y_zynid_19 = -0.00311724318294; + y_zynid_20 = 0.00311724318294; + y_zynid_21 = -0.0193734997949; + y_zynid_22 = 0.0193734997949; + y_zynid_23 = 0.00697401009889; + y_zynid_24 = -0.00697401009889; + y_zynid_25 = 0.00296525365916; + rho_trp_a = 0.0; + trp_abar = 0.0; + rho_fiscal = 0.97; + rho_fiscalav = 0.9; + fiscal_egfe = 0.01; + fiscal_egfl = 0.01; + av = 1.0; + fbar_iscal = 0.0; + fpxrr_lbar = 0.0; + rho_fpxrr_l = 0.0; + pmo_lbar = 0.0; + rho_pmo_l = 0.0; + emo_lbar = 0.0; + rho_emo_l = 0.0; + ugfdbtp_lbar = 0.0; + rho_ugfdbtp_l = 0.0; + rho_gfsrt = 0.0; + +model; + delrff(0) = rff(0) - rff(-1); + + dpadj(0) = dpadj(-1) + dpgap(-1); + + dpgap(0) = ((y_dpgap_1 * pipxnc(0) + y_dpgap_2 * phr_l(0)) - pxp_l(0)) + y_dpgap_3 * phr_l(-1) + pxp_l(-1) + y_dpgap_4 * pbfir_l(0) + y_dpgap_5 * pbfir_l(-1) + y_dpgap_6 * pegfr_l(0) + y_dpgap_7 * pegfr_l(-1) + y_dpgap_8 * pegsr_l(0) + y_dpgap_9 * pegsr_l(-1) + y_dpgap_10 * pxr_l(0) + y_dpgap_11 * pxr_l(-1); + + ebfi_l(0) = y_ebfi_l_8 * hgpbfir(-1) + y_ebfi_l_6 * xb_l(-1) + y_ebfi_l_5 * zebfi(0) + y_ebfi_l_1 * ebfi_l(-1) + ebfi_l_aerr + y_ebfi_l_2 * qebfi_l(-1) + y_ebfi_l_3 * ebfi_l(-2) + y_ebfi_l_4 * ebfi_l(-3) + y_ebfi_l_7 * xb_l(-2); + + ebfin_l(0) = pxp_l(0) + pbfir_l(0) + ebfi_l(0); + + ecd_l(0) = y_ecd_l_4 * zgapc2(0) + zecd(0) + y_ecd_l_1 * ecd_l(-1) + ecd_l_aerr + y_ecd_l_2 * qecd_l(-1) + y_ecd_l_3 * ecd_l(-2); + + ech_l(0) = y_ech_l_3 * ech_l_aerr + kh_l(-1) + ech_l(-1) * y_ech_l_1 + y_ech_l_2 * kh_l(-2) + y_ech_l_4 * ech_l(-2) + y_ech_l_5 * kh_l(-3); + + ecnia_l(0) = ecnia_l(-1) + eco_l(0) * y_ecnia_l_1 + eco_l(-1) * y_ecnia_l_2 + ecd_l(0) * y_ecnia_l_3 + ecd_l(-1) * y_ecnia_l_4 + ech_l(0) * y_ecnia_l_5 + ech_l(-1) * y_ecnia_l_6; + + ecnian_l(0) = ecnia_l(0) + pcnia_l(0); + + eco_l(0) = y_eco_l_8 * yht_l(-1) + y_eco_l_7 * yhl_l(-1) + y_eco_l_6 * yht_l(0) + y_eco_l_5 * yhl_l(0) + y_eco_l_4 * zeco(0) + eco_l(-1) * y_eco_l_1 + eco_l_aerr + y_eco_l_2 * qeco_l(-1) + y_eco_l_3 * eco_l(-2); + + egfe_l(0) = fiscal_egfe * fiscal(0) + y_egfe_l_7 * xgap2(-1) + y_egfe_l_6 * xgap2(0) + y_egfe_l_5 * egfet_l(0) + y_egfe_l_1 * egfe_l(-1) + egfe_l_aerr + y_egfe_l_2 * egfet_l(-1) + y_egfe_l_3 * egfe_l(-2) + y_egfe_l_4 * egfe_l(-3); + + egfen_l(0) = egfe_l(0) + pxp_l(0) + pegfr_l(0); + + egfet_l(0) = egfet_l(-1) * y_egfet_l_1 + pegfr_l(-1) * y_egfet_l_2 + pxp_l(-1) * y_egfet_l_3 + y_egfet_l_4 * xgdptn_l(-1) + y_egfet_l_5 * hggdpt(0) + y_egfet_l_6 * hggdpt(-1) + y_egfet_l_7 * hggdpt(-2) + y_egfet_l_8 * hggdpt(-3); + + egfl_l(0) = fiscal_egfl * fiscal(0) + xgap2(-1) * y_egfl_l_7 + xgap2(0) * y_egfl_l_6 + y_egfl_l_5 * egflt_l(0) + y_egfl_l_1 * egfl_l(-1) + egfl_l_aerr + y_egfl_l_2 * egflt_l(-1) + y_egfl_l_3 * egfl_l(-2) + y_egfl_l_4 * egfl_l(-3); + + egfln_l(0) = egfl_l(0) + pgfl_l(0); + + egflt_l(0) = egflt_l(-1) * y_egflt_l_1 + y_egflt_l_2 * pgfl_l(-1) + xgdptn_l(-1) * y_egflt_l_3 + hggdpt(0) * y_egflt_l_4 + hggdpt(-1) * y_egflt_l_5 + y_egflt_l_6 * hggdpt(-2) + y_egflt_l_7 * hggdpt(-3); + + egse_l(0) = xgap2(-1) * y_egse_l_7 + xgap2(0) * y_egse_l_6 + y_egse_l_5 * egset_l(0) + y_egse_l_1 * egse_l(-1) + egse_l_aerr + y_egse_l_2 * egset_l(-1) + y_egse_l_3 * egse_l(-2) + y_egse_l_4 * egse_l(-3); + + egsen_l(0) = egse_l(0) + pxp_l(0) + pegsr_l(0); + + egset_l(0) = egset_l(-1) * y_egset_l_1 + pegsr_l(-1) * y_egset_l_2 + pxp_l(-1) * y_egset_l_3 + xgdptn_l(-1) * y_egset_l_4 + hggdpt(0) * y_egset_l_5 + hggdpt(-1) * y_egset_l_6 + y_egset_l_7 * hggdpt(-2) + y_egset_l_8 * hggdpt(-3); + + egsl_l(0) = xgap2(-1) * y_egsl_l_7 + xgap2(0) * y_egsl_l_6 + y_egsl_l_5 * egslt_l(0) + y_egsl_l_1 * egsl_l(-1) + egsl_l_aerr + y_egsl_l_2 * egslt_l(-1) + y_egsl_l_3 * egsl_l(-2) + y_egsl_l_4 * egsl_l(-3); + + egsln_l(0) = egsl_l(0) + pgsl_l(0); + + egslt_l(0) = egslt_l(-1) * y_egslt_l_1 + y_egslt_l_2 * pgsl_l(-1) + xgdptn_l(-1) * y_egslt_l_3 + hggdpt(0) * y_egslt_l_4 + hggdpt(-1) * y_egslt_l_5 + y_egslt_l_6 * hggdpt(-2) + y_egslt_l_7 * hggdpt(-3); + + eh_l(0) = y_eh_l_7 * d83 + y_eh_l_5 * rme(-1) + zeh(0) + y_eh_l_1 * eh_l(-1) + eh_l_aerr + y_eh_l_2 * qeh_l(-1) + y_eh_l_3 * eh_l(-2) + y_eh_l_4 * eh_l(-3) + y_eh_l_6 * rme(-2); + + ehn_l(0) = eh_l(0) + phr_l(0) + pxp_l(0); + + emn_l(0) = emon_l(0) * y_emn_l_2 + empn_l(0) * y_emn_l_3; + + emo_l(0) = y_emo_l_9 * ddockm(-1) + y_emo_l_8 * ddockm + y_emo_l_7 * xgap2(-2) + xgap2(-1) * y_emo_l_6 + xgap2(0) * y_emo_l_5 + y_emo_l_4 * xgdpn_l(-1) + emo_l(-1) * y_emo_l_1 + emo_ltilde(0) + y_emo_l_2 * pmo_l(-1) + y_emo_l_3 * uemot(-1); + + emo_ltilde(0) = (1 - rho_emo_l) * emo_lbar + rho_emo_l * emo_ltilde(-1) + emo_l_aerr; + + emon_l(0) = emo_l(0) + pmo_l(0); + + emp_l(0) = xgdp_l(0) + emp_l_aerr + y_emp_l_1 * emptrt + y_emp_l_2 * pmp_l(0) + y_emp_l_3 * pxb_l(0) + y_emp_l_4 * pmp_l(-1) + y_emp_l_5 * pxb_l(-1) + xgap2(-1) * y_emp_l_6; + + empn_l(0) = emp_l(0) + pmp_l(0); + + ex_l(0) = y_ex_l_10 * ddockx + y_ex_l_1 * ex_l(-1) + ex_l_aerr + pxr_l(-1) * y_ex_l_2 + pxp_l(-1) * y_ex_l_3 + y_ex_l_4 * fpx_l(-1) + y_ex_l_5 * fgdp_l(-1) + y_ex_l_6 * fpc_l(-1) + y_ex_l_7 * fxgap(0) + y_ex_l_8 * fxgap(-1) + y_ex_l_9 * fxgap(-2); + + exn_l(0) = ex_l(0) + pxp_l(0) + pxr_l(0); + + fcbn_l(0) = exn_l(0) * y_fcbn_l_2 + emn_l(0) * y_fcbn_l_3 + y_fcbn_l_4 * fynicn_l(0) + y_fcbn_l_5 * fyniln_l(0) + y_fcbn_l_6 * ufcbr + pxb_l(0) * y_fcbn_l_7 + y_fcbn_l_8 * xbt_l(0); + + fgdp_l(0) = fgdpt_l(0) + fxgap(0) * y_fgdp_l_2; + + fgdpt_l(0) = y_fgdpt_l_1 * fgdpt_l(-1) + y_fgdpt_l_2 * xgdpt_l(-1) + hggdpt(0) * y_fgdpt_l_3 + hggdpt(-1) * y_fgdpt_l_4 + y_fgdpt_l_5 * hggdpt(-2) + y_fgdpt_l_6 * hggdpt(-3); + + fnicn_l(0) = y_fnicn_l_1 * fnicn_l(-1) + y_fnicn_l_2 * xgdptn_l(0) + y_fnicn_l_4 * fpc_l(0) + fpc_l(-1) * y_fnicn_l_5 + y_fnicn_l_6 * fpx_l(0) + fpx_l(-1) * y_fnicn_l_7 + y_fnicn_l_8 * rfnict; + + fniln_l(0) = y_fniln_l_1 * fniln_l(-1) + rfnict * y_fniln_l_3 + xgdptn_l(0) * y_fniln_l_4 + fcbn_l(0) * y_fniln_l_5 + y_fniln_l_6 * pgdp_l(0) + y_fniln_l_7 * pgdp_l(-1) + fpx_l(0) * y_fniln_l_8 + fpx_l(-1) * y_fniln_l_9 + y_fniln_l_10 * fnirn_l(0); + + fnirn_l(0) = y_fnirn_l_2 * ufnir + xgdpn_l(0); + + fpc_l(0) = fpc_l(-1) + y_fpc_l_2 * fpic(0); + + fpi10(0) = fxgap(-1) * y_fpi10_6 + y_fpi10_5 * fpitrg + y_fpi10_1 * fpi10(-1) + y_fpi10_2 * fpi10(-2) + y_fpi10_3 * fpi10(-3) + y_fpi10_4 * fpi10(-4); + + fpi10t(0) = y_fpi10t_1 * fpi10t(-1) + fpi10(0) * y_fpi10t_2; + + fpic(0) = fpi10(0) * y_fpic_1 + y_fpic_2 * fpic(-1); + + fpx_l(0) = (fpc_l(0) + fpxr_l(0)) - pcpi_l(0); + + fpxr_l(0) = fpxrr_l(0) + y_fpxr_l_1 * rg10(0) + y_fpxr_l_2 * zpi10f(0) + y_fpxr_l_3 * frl10(0) + fpi10t(0) * y_fpxr_l_4 + fnicn_l(0) * y_fpxr_l_5 + fniln_l(0) * y_fpxr_l_6 + xgdpn_l(0) * y_fpxr_l_7; + + fpxrr_l(0) = y_fpxrr_l_4 * fpxrrt + y_fpxrr_l_3 * fpxrr_l(-2) + y_fpxrr_l_1 * fpxrr_l(-1) + fpxrr_ltilde(0) + y_fpxrr_l_2 * fpxrrt(-1); + + fpxrr_ltilde(0) = (1 - rho_fpxrr_l) * fpxrr_lbar + rho_fpxrr_l * fpxrr_ltilde(-1) + fpxrr_l_aerr; + + frl10(0) = fxgap(-1) * y_frl10_6 + fxgap(0) * y_frl10_5 + y_frl10_4 * frs10(0) + y_frl10_1 * frl10(-1) + y_frl10_2 * frs10(-1) + y_frl10_3 * frl10(-2); + + frs10(0) = rfrs10 + fxgap(0) * y_frs10_8 + fpitrg * y_frs10_7 + y_frs10_1 * dfmprr + y_frs10_2 * frstar(-1) + fpi10(0) * y_frs10_3 + fpi10(-1) * y_frs10_4 + y_frs10_5 * fpi10(-2) + y_frs10_6 * fpi10(-3); + + frstar(0) = frstar(-1) * y_frstar_1 + frs10(0) * y_frstar_2 + fpi10(0) * y_frstar_3 + fpi10(-1) * y_frstar_4 + y_frstar_5 * fpi10(-2) + y_frstar_6 * fpi10(-3); + + ftcin_l(0) = y_ftcin_l_2 * uftcin + ynicpn_l(0); + + fxgap(0) = xgap2(-1) * y_fxgap_13 + frstar(0) * y_fxgap_12 + fpi10(-1) * y_fxgap_4 + frs10(-1) * y_fxgap_3 + fxgap_aerr + fxgap(-1) * y_fxgap_1 + y_fxgap_2 * fxgap(-2) + y_fxgap_5 * fpi10(-2) + y_fxgap_6 * fpi10(-3) + y_fxgap_7 * fpi10(-4) + y_fxgap_8 * frs10(-2) + y_fxgap_9 * fpi10(-5) + y_fxgap_10 * frs10(-3) + y_fxgap_11 * fpi10(-6); + + fynicn_l(0) = fnicn_l(-1) + y_fynicn_l_2 * rfynic(0); + + fyniln_l(0) = fniln_l(-1) + y_fyniln_l_2 * rfynil(0); + + gfdbtnp_l(0) = ugfdbtp_l(0) + y_gfdbtnp_l_2 * gfdbtnp_l(-1) + y_gfdbtnp_l_3 * gfexpn_l(0) + y_gfdbtnp_l_4 * gfrecn_l(0); + + gfdbtn_l(0) = gfdbtnp_l(0) + ugfdbt_l; + + ugfdbtp_l(0) = (1 - rho_ugfdbtp_l) * ugfdbtp_lbar + rho_ugfdbtp_l * ugfdbtp_l(-1) + ugfdbtp_lerr; + + ugfsrp(0) = y_ugfsrp_1 * ugfsrp(-1); + + uleg_l(0) = uleg_l(-1) + y_uleg_l_1 * leg_l(-1) + y_uleg_l_2 * lep_l(-1) + y_uleg_l_3 * adjlegrt; + + gfexpn_l(0) = egfln_l(0) * y_gfexpn_l_2 + egfen_l(0) * y_gfexpn_l_3 + y_gfexpn_l_4 * gtn_l(0) + y_gfexpn_l_5 * gfintn_l(0); + + gfintn_l(0) = y_gfintn_l_2 * rgfint(0) + gfdbtn_l(-1); + + gfrecn_l(0) = y_gfrecn_l_2 * tpn_l(0) + y_gfrecn_l_3 * tcin_l(0) + ugfsrp(0) * y_gfrecn_l_4 + xgdpn_l(0) * y_gfrecn_l_5; + + gtn_l(0) = pgdp_l(0) + gtr_l(0); + + gtr_l(0) = y_gtr_l_2 * gtrd(0) + y_gtr_l_3 * gtrt + xgdpt_l(0); + + gtrd(0) = 0.0014 * (fiscalav(0) - y_gtrd_6 * fiscalav(-1)) + y_gtrd_6 * gtrd(-1) + gtrd_aerr + xgap2(0) * y_gtrd_1 + xgap2(-1) * y_gtrd_2 + y_gtrd_3 * xgap2(-2) + y_gtrd_4 * xgap2(-3) + y_gtrd_5 * xgap2(-4) + y_gtrd_7 * xgap2(-5); + + hgemp(0) = y_hgemp_1 * hgemp(-1) + emp_l(0) * y_hgemp_2 + emp_l(-1) * y_hgemp_3; + + hggdp(0) = xgdp_l(0) * y_hggdp_1 + y_hggdp_2 * xgdp_l(-1); + + hggdpt(0) = hxbt(0) + huxb(0); + + hgpbfir(0) = hgpbfir(-1) * y_hgpbfir_1 + pbfir_l(0) * y_hgpbfir_2 + pxp_l(0) * y_hgpbfir_3 + pxb_l(0) * y_hgpbfir_4 + pbfir_l(-1) * y_hgpbfir_5 + pxp_l(-1) * y_hgpbfir_6 + pxb_l(-1) * y_hgpbfir_7; + + hgpkir(0) = y_hgpkir_1 * hgpkir(-1) + y_hgpkir_2 * pkir + y_hgpkir_3 * pkir(-1); + + hgynid(0) = ynicpn_l(0) * y_hgynid_1 + tcin_l(0) * y_hgynid_2 + pxb_l(0) * y_hgynid_3 + y_hgynid_4 * ynicpn_l(-1) + y_hgynid_5 * tcin_l(-1) + pxb_l(-1) * y_hgynid_6; + + hks(0) = y_hks_1 * kbfi_l(0) + y_hks_2 * kbfi_l(-1) + y_hks_3 * ki_l(0) + y_hks_4 * ki_l(-1) + hksr; + + hlept(0) = y_hlept_1 * hqlfpr(0) + y_hlept_2 * n16_l + y_hlept_3 * n16_l(-1); + + hlprdt(0) = (hxbt(0) - hlept(0)) - hqlww(0); + + hmfpt(0) = hmfpt_aerr + y_hmfpt_1 * hmfpt(-1); + + hqlfpr(0) = hqlfpr_aerr + y_hqlfpr_1 * hqlfpr(-1); + + hqlww(0) = hqlww_aerr + y_hqlww_1 * hqlww(-1); + + huqpct(0) = y_huqpct_1 * huqpct(-1); + + huxb(0) = y_huxb_1 * dglprd + y_huxb_2 * huxb(-1); + + hxbt(0) = hmfpt(0) + hks(0) * y_hxbt_5 + hlept(0) * y_hxbt_1 + hqlww(0) * y_hxbt_2 + y_hxbt_3 * lqualt_l + y_hxbt_4 * lqualt_l(-1); + + jccan_l(0) = xgdpn_l(0) + y_jccan_l_2 * jccan_l(-1) + xgdpn_l(-1) * y_jccan_l_3 + y_jccan_l_4 * pkbfir(-1) + kbfi_l(-1) * y_jccan_l_5 + y_jccan_l_6 * jrbfi + pxp_l(-1) * y_jccan_l_7; + + jkcd_l(0) = y_jkcd_l_2 * jrcd + kcd_l(-1); + + kbfi_l(0) = pbfir_l(0) * y_kbfi_l_2 + y_kbfi_l_3 * pkbfir(0) + ebfi_l(0) * y_kbfi_l_4 + jrbfi * y_kbfi_l_5 + kbfi_l(-1) * y_kbfi_l_6; + + kcd_l(0) = ecd_l(0) * y_kcd_l_2 + jrcd * y_kcd_l_3 + kcd_l(-1) * y_kcd_l_4; + + kh_l(0) = eh_l(0) * y_kh_l_2 + y_kh_l_3 * jrh + kh_l(-1) * y_kh_l_4; + + ki_l(0) = ki_l(-1) * y_ki_l_1 + ki_l_aerr + y_ki_l_2 * qkir_l(0) + y_ki_l_3 * xfs_l(-1) + y_ki_l_4 * ki_l(-2) + y_ki_l_5 * xfs_l(-2) + y_ki_l_6 * xfs_l(-3); + + ks_l(0) = ks_l(-1) + hks(0) * y_ks_l_1; + + leg_l(0) = (uleg_l(0) + egfl_l(0) * y_leg_l_1 + egsl_l(0) * y_leg_l_2) - lprdt_l(0); + + leh_l(0) = y_leh_l_2 * lep_l(0) + leg_l(0) * y_leh_l_3 + y_leh_l_4 * leo_l(0); + + leo_l(0) = xgap2(-1) * y_leo_l_5 + y_leo_l_4 * qlf_l(-1) + leo_l_aerr + y_leo_l_1 * qleor + qlf_l(0) + y_leo_l_2 * leo_l(-1) + y_leo_l_3 * qleor(-1); + + lep_l(0) = lhp_l(0) - lww_l(0); + + leppot_l(0) = qlf_l(0) + y_leppot_l_2 * lurnat(0) + qleor * y_leppot_l_3 + adjlegrt * y_leppot_l_4; + + lf_l(0) = n16_l + y_lf_l_2 * lfpr(0); + + lfpr(0) = hqlfpr(0) + y_lfpr_1 * lfpr(-1) + lfpr_aerr + y_lfpr_2 * qlfpr(-1) + y_lfpr_3 * lur(-1) + y_lfpr_4 * lurnat(-1); + + lhp_l(0) = y_lhp_l_7 * hlprdt(-1) + y_lhp_l_6 * xbo_l(-1) + y_lhp_l_5 * xbo_l(0) + y_lhp_l_4 * zlhp(0) + y_lhp_l_1 * lhp_l(-1) + lhp_l_aerr + y_lhp_l_2 * qlhp_l(-1) + y_lhp_l_3 * lhp_l(-2) + y_lhp_l_8 * xbo_l(-2) + y_lhp_l_9 * hlprdt(-2); + + lprdt_l(0) = (xbt_l(0) - leppot_l(0)) - qlww_l(0); + + lur(0) = leh_l(0) * y_lur_1 + lf_l(0) * y_lur_2; + + lurnat(0) = lurnat_aerr + lurnat(-1) * y_lurnat_1; + + lww_l(0) = y_lww_l_1 * lww_l(-1) + hqlww(0) * y_lww_l_2 + lww_l_aerr + y_lww_l_3 * qlww_l(-1) + lhp_l(0) * y_lww_l_4 + lhp_l(-1) * y_lww_l_5 + hlept(0) * y_lww_l_6; + + mfpt_l(0) = mfpt_l_aerr + mfpt_l(-1) + hmfpt(0) * y_mfpt_l_1; + + pbfir_l(0) = (pxp_l(-1) + dpadj(0) + pbfir_l(-1) + pbfir_l_aerr + pipxnc(0) * y_pbfir_l_1) - pxp_l(0); + + pcdr_l(0) = y_pcdr_l_1 * pcdr_l(-1) + y_pcdr_l_2 * pcdr_l(-2); + + pcer_l(0) = pcer_l(-1) + pcer_l_aerr + pmp_l(0) * y_pcer_l_1 + y_pcer_l_2 * pcxfe_l(0) + pmp_l(-1) * y_pcer_l_3 + y_pcer_l_4 * pcxfe_l(-1); + + pcfr_l(0) = y_pcfr_l_6 * pcfrt + y_pcfr_l_5 * pcfr_l(-4) + y_pcfr_l_4 * pcfr_l(-3) + y_pcfr_l_3 * pcfr_l(-2) + y_pcfr_l_1 * pcfr_l(-1) + pcfr_l_aerr + y_pcfr_l_2 * pcfrt(-1); + + pchr_l(0) = y_pchr_l_1 * pchr_l(-1) + y_pchr_l_2 * pchr_l(-2); + + pcnia_l(0) = pcnia_l(-1) + y_pcnia_l_1 * picnia(0); + + pcor_l(0) = pcor_l(-1) + pcdr_l(0) * y_pcor_l_1 + pcdr_l(-1) * y_pcor_l_2 + pchr_l(0) * y_pcor_l_3 + pchr_l(-1) * y_pcor_l_4; + + pcpi_l(0) = pcnia_l(0) + y_pcpi_l_2 * upcpi; + + pcpix_l(0) = pcxfe_l(0) + y_pcpix_l_2 * upcpix; + + pcxfe_l(0) = pcxfe_l(-1) + y_pcxfe_l_1 * picxfe(0); + + pegfr_l(0) = (pxp_l(-1) + dpadj(0) + pegfr_l(-1) + pegfr_l_aerr + pipxnc(0) * y_pegfr_l_1) - pxp_l(0); + + pegsr_l(0) = (pxp_l(-1) + dpadj(0) + pegsr_l(-1) + pegsr_l_aerr + pipxnc(0) * y_pegsr_l_1) - pxp_l(0); + + pgdp_l(0) = xgdpn_l(0) - xgdp_l(0); + + pgfl_l(0) = (y_pgfl_l_1 * upgfl + pl_l(0)) - lprdt_l(0); + + pgsl_l(0) = (pl_l(0) + y_pgsl_l_1 * upgsl) - lprdt_l(0); + + phouse_l(0) = pcnia_l(-1) * y_phouse_l_4 + pchr_l(-1) * y_phouse_l_3 + y_phouse_l_1 * phouse_l(-1) + phouse_l_aerr + y_phouse_l_2 * phouse_l(-2); + + phr_l(0) = (pxp_l(-1) + dpadj(0) + phr_l(-1) + phr_l_aerr + pipxnc(0) * y_phr_l_1) - pxp_l(0); + + pic4(0) = pcnia_l(0) * y_pic4_1 + y_pic4_2 * pcnia_l(-4); + + picnia(0) = picxfe(0) + pcer_l(0) * y_picnia_1 + pcer_l(-1) * y_picnia_2 + pcfr_l(0) * y_picnia_3 + pcfr_l(-1) * y_picnia_4; + + picx4(0) = pcxfe_l(0) * y_picx4_1 + y_picx4_2 * pcxfe_l(-4); + + picxfe(0) = picxfe_aerr + y_picxfe_1 * picxfe(-1) + y_picxfe_2 * zpicxfe(0) + y_picxfe_3 * ptr(-1) + y_picxfe_4 * qpcnia_l(-1) + pcnia_l(-1) * y_picxfe_5; + + pieci(0) = y_pieci_12 * pl_l(-1) + y_pieci_11 * qpl_l(-1) + lurnat(-1) * y_pieci_10 + lur(-1) * y_pieci_9 + huqpct(-1) * y_pieci_8 + hlprdt(-1) * y_pieci_7 + ptr(-1) * y_pieci_6 + y_pieci_5 * zpieci(0) + pieci_aerr + y_pieci_1 * pieci(-1) + y_pieci_2 * pieci(-2) + y_pieci_3 * pieci(-3) + y_pieci_4 * pieci(-4); + + pigdp(0) = pgdp_l(0) * y_pigdp_1 + pgdp_l(-1) * y_pigdp_2; + + pipl(0) = pieci(0); + + pipxnc(0) = y_pipxnc_11 * pxnc_l(-1) + y_pipxnc_10 * qpxnc_l(-1) + y_pipxnc_9 * fpxr_l(-1) + fpxr_l(0) * y_pipxnc_8 + picnia(0) + huqpct(0) * y_pipxnc_1 + y_pipxnc_2 * pipxnc(-1) + y_pipxnc_3 * picnia(-1) + huqpct(-1) * y_pipxnc_4 + y_pipxnc_5 * pipxnc(-2) + y_pipxnc_6 * picnia(-2) + y_pipxnc_7 * huqpct(-2); + + pkbfir(0) = y_pkbfir_1 * upkbfir + pbfir_l(0) * y_pkbfir_2; + + pl_l(0) = pl_l(-1) + pipl(0) * y_pl_l_1; + + pmo_l(0) = pmo_l(-1) * y_pmo_l_1 + pmo_ltilde(0) + y_pmo_l_2 * qpmo_l + fpc_l(-1) * y_pmo_l_3 + fpx_l(-1) * y_pmo_l_4 + pxb_l(-1) * y_pmo_l_5 + fpc_l(0) * y_pmo_l_6 + fpx_l(0) * y_pmo_l_7 + pxb_l(0) * y_pmo_l_8; + + pmo_ltilde(0) = (1 - rho_pmo_l) * pmo_lbar + rho_pmo_l * pmo_ltilde(-1) + pmo_l_aerr; + + pmp_l(0) = y_pmp_l_2 * upmp + poil_l(0); + + poil_l(0) = pxb_l(0) + poilr_l(0); + + poilr_l(0) = y_poilr_l_4 * poilrt + y_poilr_l_3 * poilr_l(-2) + y_poilr_l_1 * poilr_l(-1) + poilr_l_aerr + y_poilr_l_2 * poilrt(-1); + + ptr(0) = ptr(-1) * y_ptr_1 + picxfe(-1) * y_ptr_2 + y_ptr_3 * pitarg(-1); + + pxb_l(0) = pgdp_l(0) + y_pxb_l_2 * upxb; + + pxnc_l(0) = pxnc_l(-1) + pipxnc(0) * y_pxnc_l_1; + + pxp_l(0) = pxp_l(-1) + pcnia_l(0) * y_pxp_l_1 + pcnia_l(-1) * y_pxp_l_2 + pxnc_l(0) * y_pxp_l_3 + pxnc_l(-1) * y_pxp_l_4; + + pxr_l(0) = (pxp_l(-1) + dpadj(0) + pxr_l(-1) + pxr_l_aerr + pipxnc(0) * y_pxr_l_1) - pxp_l(0); + + qebfi_l(0) = xb_l(0) + y_qebfi_l_2 * vbfi(0) + hxbt(0) * y_qebfi_l_3 + hgpbfir(0) * y_qebfi_l_4 + jrbfi * y_qebfi_l_5; + + qec_l(0) = y_qec_l_1 * zyh_l(0) + y_qec_l_2 * zyht_l(0) + y_qec_l_3 * zyhp_l(0) + y_qec_l_4 * wpo_l(0) + y_qec_l_5 * wps_l(0); + + qecd_l(0) = y_qecd_l_13 * rccd(0) + pcdr_l(0) * y_qecd_l_12 + y_qecd_l_11 * hgpcdr + qec_l(0) + jrcd * y_qecd_l_2 + hggdpt(0) * y_qecd_l_3 + hggdpt(-1) * y_qecd_l_4 + y_qecd_l_5 * hggdpt(-2) + y_qecd_l_6 * hggdpt(-3) + y_qecd_l_7 * hggdpt(-4) + y_qecd_l_8 * hggdpt(-5) + y_qecd_l_9 * hggdpt(-6) + y_qecd_l_10 * hggdpt(-7); + + qeco_l(0) = qec_l(0) - pcor_l(0); + + qeh_l(0) = ((y_qeh_l_19 * rcch(0) + pcnia_l(0) + qec_l(0) + jrh * y_qeh_l_2 + hggdpt(0) * y_qeh_l_3 + hggdpt(-1) * y_qeh_l_4 + y_qeh_l_5 * hggdpt(-2) + y_qeh_l_6 * hggdpt(-3) + y_qeh_l_7 * hggdpt(-4) + y_qeh_l_8 * hggdpt(-5) + y_qeh_l_9 * hggdpt(-6) + y_qeh_l_10 * hggdpt(-7) + y_qeh_l_11 * hggdpt(-8) + y_qeh_l_12 * hggdpt(-9) + y_qeh_l_13 * hggdpt(-10) + y_qeh_l_14 * hggdpt(-11) + y_qeh_l_15 * hggdpt(-12) + y_qeh_l_16 * hggdpt(-13) + y_qeh_l_17 * hggdpt(-14) + y_qeh_l_18 * hggdpt(-15)) - phr_l(0)) - pxp_l(0); + + qkir_l(0) = dglprd * y_qkir_l_1 + rho_qkir_l * qkir_l(-1); + + qlf_l(0) = n16_l + y_qlf_l_2 * qlfpr(0); + + qlfpr(0) = hqlfpr(0) + qlfpr(-1); + + qlhp_l(0) = xbo_l(0) - lprdt_l(0); + + qlww_l(0) = qlww_l(-1) + hqlww(-1) * y_qlww_l_1; + + qpcnia_l(0) = qpxp_l(0) + uqpct_l(0); + + qpl_l(0) = (pxb_l(0) + pl_l(0)) - qpxb_l(0); + + qpxb_l(0) = (pl_l(0) + pwstar_l) - lprdt_l(0); + + qpxnc_l(0) = pxnc_l(0) + qpxp_l(0) * y_qpxnc_l_1 + pxp_l(0) * y_qpxnc_l_2 + qpcnia_l(0) * y_qpxnc_l_3 + pcnia_l(0) * y_qpxnc_l_4; + + qpxp_l(0) = pxp_l(0) + qpxb_l(0) * y_qpxp_l_1 + pxb_l(0) * y_qpxp_l_2; + + qynidn_l(0) = y_qynidn_l_1 * d79a + ynicpn_l(0) * y_qynidn_l_2 + tcin_l(0) * y_qynidn_l_3; + + rbbb(0) = rg10(0) + rbbbp(0); + + rbbbp(0) = rbbbp_aerr + y_rbbbp_1 * zgap10(0) + y_rbbbp_2 * rbbbp(-1) + y_rbbbp_3 * zgap10(-1); + + rbfi(0) = y_rbfi_1 * trfcim + y_rbfi_2 * rg5(0) + rbbb(0) * y_rbfi_3 + rg10(0) * y_rbfi_4 + y_rbfi_5 * zpib5(0) + y_rbfi_6 * req(0); + + rcar(0) = rcar_aerr + d79a * y_rcar_1 + y_rcar_2 * t47 + y_rcar_3 * rcar(-1) + rg5(0) * y_rcar_4 + y_rcar_5 * rg5(-1); + + rccd(0) = (rcar(0) + jrcd * y_rccd_1) - zpi5(0); + + rcch(0) = (jrh * y_rcch_1 + y_rcch_2 * trfpm + y_rcch_3 * rme(0) + y_rcch_4 * trspp) - zpi10(0); + + rcgain(0) = picx4(0) + rcgain_aerr + xgap2(0) * y_rcgain_1 + y_rcgain_2 * rcgain(-1) + y_rcgain_3 * picx4(-1); + + req(0) = (rg30(0) - zpic30(0)) + reqp(0); + + reqp(0) = reqp_aerr + rbbbp(0) * y_reqp_1 + y_reqp_2 * reqp(-1) + rbbbp(-1) * y_reqp_3; + + rfynic(0) = rfynil(0) * y_rfynic_4 + y_rfynic_1 * rfynic(-1) + rfynic_aerr + y_rfynic_2 * rfynil(-1) + y_rfynic_3 * rfynic(-2); + + rfynil(0) = reqp(0) * y_rfynil_8 + y_rfynil_7 * rtb(0) + rg10(0) * y_rfynil_6 + rfynil(-1) * y_rfynil_1 + rfynil_aerr + y_rfynil_2 * rg10(-1) + y_rfynil_3 * rtb(-1) + reqp(-1) * y_rfynil_4 + y_rfynil_5 * rfynil(-2); + + rg10(0) = zrff10(0) + rg10p(0); + + rg10p(0) = rg10p_aerr + zgap10(0) * y_rg10p_1 + y_rg10p_2 * d8095 + y_rg10p_3 * rg10p(-1) + zgap10(-1) * y_rg10p_4 + y_rg10p_5 * d8095(-1); + + rg30(0) = zrff30(0) + rg30p(0); + + rg30p(0) = rg30p_aerr + y_rg30p_1 * zgap30(0) + d8095 * y_rg30p_2 + y_rg30p_3 * rg30p(-1) + y_rg30p_4 * zgap30(-1) + y_rg30p_5 * d8095(-1); + + rg5(0) = zrff5(0) + rg5p(0); + + rg5p(0) = rg5p_aerr + y_rg5p_1 * zgap05(0) + y_rg5p_2 * rg5p(-1) + y_rg5p_3 * zgap05(-1); + + rgfint(0) = gfdbtn_l(-1) * y_rgfint_4 + rgfint_aerr + y_rgfint_1 * rgfint(-1) + y_rgfint_2 * rgw(-1) + y_rgfint_3 * gfdbtn_l(-2); + + rgw(0) = rtb(0) * y_rgw_1 + rg5(0) * y_rgw_2 + rg10(0) * y_rgw_3 + rg30(0) * y_rgw_4; + + rme(0) = rme(-1) * y_rme_1 + rme_aerr + rg10(0) * y_rme_2 + rg10(-1) * y_rme_3 + y_rme_4 * d87; + + rrff(0) = rff(0) + picxfe(0) * y_rrff_1 + picxfe(-1) * y_rrff_2 + y_rrff_3 * picxfe(-2) + y_rrff_4 * picxfe(-3); + + rrtr(0) = y_rrtr_1 * rrtr(-1) + rrff(0) * y_rrtr_2; + + rspnia(0) = y_rspnia_1 * yhsn_l(0) + y_rspnia_2 * ydn_l(0); + + rtb(0) = rff(-1) * y_rtb_4 + rff(0) * y_rtb_3 + rtb(-1) * y_rtb_1 + y_rtb_2 * rtb(-2); + + rtbfi_l(0) = (pxp_l(0) + rbfi(0) * y_rtbfi_l_2 + jrbfi * y_rtbfi_l_3 + hgpbfir(0) * y_rtbfi_l_4 + y_rtbfi_l_5 * tritc + trfcim * y_rtbfi_l_6 + y_rtbfi_l_7 * tapddp + y_rtbfi_l_8 * tdpv + pkbfir(0) * y_rtbfi_l_9) - pxb_l(0); + + rtinv(0) = pxb_l(0) * y_rtinv_7 + rbfi(0) * y_rtinv_1 + hgpkir(0) * y_rtinv_2 + pxp_l(0) * y_rtinv_3 + pkir * y_rtinv_4 + pxp_l(-1) * y_rtinv_5 + y_rtinv_6 * pkir(-1); + + rtr(0) = ptr(0) + rrtr(0); + + tcin_l(0) = ynicpn_l(0) + y_tcin_l_2 * trci(0); + + tpn_l(0) = y_tpn_l_2 * trp(0) + y_tpn_l_3 * ypn_l(0) + gtn_l(0) * y_tpn_l_4; + + trci(0) = xgap2(-1) * y_trci_4 + trci_aerr + trcit + xgap2(0) * y_trci_1 + y_trci_2 * trci(-1) + y_trci_3 * trcit(-1); + + trp(0) = xgap2(0) * y_trp_5 + trp_a(0) + trpt(0) + y_trp_1 * trp(-1) + y_trp_2 * trpt(-1) + y_trp_3 * trp(-2) + y_trp_4 * trpt(-2); + + trp_a(0) = (1 - rho_trp_a) * trp_abar + rho_trp_a * trp_a(-1) + trp_aerr; + + trpt(0) = trpts(0); + + trptd(0) = y_trptd_6 * gfdrt(-2) + y_trptd_5 * xgdpn_l(-2) + y_trptd_4 * gfdbtn_l(-2) + trpt(-1) + gfdbtnp_l(-1) * y_trptd_1 + xgdpn_l(-1) * y_trptd_2 + y_trptd_3 * gfdrt(-1); + + trpts(0) = xgap2(-1) * y_trpts_5 + trpt(-1) + y_trpts_1 * gfrecn_l(-1) + y_trpts_2 * gfexpn_l(-1) + xgdpn_l(-1) * y_trpts_3 + y_trpts_4 * gfsrt(-1); + + gfsrt(0) = rho_gfsrt * gfsrt(-1) + gfsrt_err; + + tryh(0) = tpn_l(0) * y_tryh_1 + y_tryh_2 * yhln_l(0) + y_tryh_3 * yhptn_l(0); + + uqpct_l(0) = huqpct(0) + uqpct_l(-1); + + uxbt_l(0) = uxbt_l(-1) + huxb(0) * y_uxbt_l_1; + + uynicpnr(0) = y_uynicpnr_1 * uynicpnr(-1); + + vbfi(0) = y_vbfi_1 * uvbfi + pkbfir(0) * y_vbfi_2 + pbfir_l(0) * y_vbfi_3 + rtbfi_l(0) * y_vbfi_4; + + wpo_l(0) = wpon_l(0) - pcnia_l(0); + + wpon_l(0) = y_wpon_l_2 * wpon_l(-1) + rcgain(0) * y_wpon_l_3 + phouse_l(0) * y_wpon_l_4 + phouse_l(-1) * y_wpon_l_5 + ydn_l(0) * y_wpon_l_6 + ecnian_l(0) * y_wpon_l_7 + y_wpon_l_8 * yhibn_l(0) + pcdr_l(0) * y_wpon_l_9 + pcnia_l(0) * y_wpon_l_10 + ecd_l(0) * y_wpon_l_11 + jkcd_l(0) * y_wpon_l_12; + + wps_l(0) = wpsn_l(0) - pcnia_l(0); + + wpsn_l(0) = ynicpn_l(0) * y_wpsn_l_1 + tcin_l(0) * y_wpsn_l_2 + req(0) * y_wpsn_l_3 + y_wpsn_l_4 * zdivgr(0); + + xb_l(0) = y_xb_l_2 * xbn_l(0) + pxb_l(0) * y_xb_l_3; + + xbn_l(0) = pxb_l(0) * y_xbn_l_2 + xbo_l(0) * y_xbn_l_3 + xgdpn_l(0) * y_xbn_l_4 + y_xbn_l_5 * xgdo_l(0) + pgdp_l(0) * y_xbn_l_6; + + xbo_l(0) = xbt_l(0) + xgap2(0) * y_xbo_l_1; + + xbt_l(0) = mfpt_l(0) + leppot_l(0) * y_xbt_l_1 + qlww_l(0) * y_xbt_l_2 + lqualt_l * y_xbt_l_3 + ks_l(0) * y_xbt_l_4 + xbtr_l(0); + + xbtr_l(0) = y_xbtr_l_1 * xbtr_l(-1); + + xfs_l(0) = xfs_l(-1) + ecnia_l(0) * y_xfs_l_1 + ecnia_l(-1) * y_xfs_l_2 + eh_l(0) * y_xfs_l_3 + eh_l(-1) * y_xfs_l_4 + ebfi_l(0) * y_xfs_l_5 + ebfi_l(-1) * y_xfs_l_6 + egfe_l(0) * y_xfs_l_7 + egfe_l(-1) * y_xfs_l_8 + egfl_l(0) * y_xfs_l_9 + egfl_l(-1) * y_xfs_l_10 + egse_l(0) * y_xfs_l_11 + egse_l(-1) * y_xfs_l_12 + egsl_l(0) * y_xfs_l_13 + egsl_l(-1) * y_xfs_l_14 + ex_l(0) * y_xfs_l_15 + ex_l(-1) * y_xfs_l_16 + emo_l(0) * y_xfs_l_17 + emo_l(-1) * y_xfs_l_18 + emp_l(0) * y_xfs_l_19 + emp_l(-1) * y_xfs_l_20; + + xfsn_l(0) = xgdpn_l(0) * y_xfsn_l_2 + pkir * y_xfsn_l_3 + pxp_l(0) * y_xfsn_l_4 + ki_l(0) * y_xfsn_l_5 + ki_l(-1) * y_xfsn_l_6; + + xgap(0) = xbo_l(0) * y_xgap_1 + xbt_l(0) * y_xgap_2; + + xgap2(0) = xgdo_l(0) * y_xgap2_1 + xgdpt_l(0) * y_xgap2_2; + + xgdi_l(0) = mei_l + xgdo_l(0); + + xgdin_l(0) = pgdp_l(0) + xgdi_l(0); + + xgdo_l(0) = xgdp_l(0) - mep_l; + + xgdp_l(0) = xgdp_l(-1) + xfs_l(0) * y_xgdp_l_1 + xfs_l(-1) * y_xgdp_l_2 + ki_l(0) * y_xgdp_l_3 + ki_l(-1) * y_xgdp_l_4 + y_xgdp_l_5 * ki_l(-2); + + xgdpn_l(0) = y_xgdpn_l_2 * xpn_l(0) + egfln_l(0) * y_xgdpn_l_3 + egsln_l(0) * y_xgdpn_l_4 + emn_l(0) * y_xgdpn_l_5 + pkir * y_xgdpn_l_6 + pxp_l(0) * y_xgdpn_l_7 + ki_l(0) * y_xgdpn_l_8 + ki_l(-1) * y_xgdpn_l_9; + + xgdpt_l(0) = xbt_l(0) + uxbt_l(0); + + xgdptn_l(0) = pgdp_l(0) + xgdpt_l(0); + + xp_l(0) = xp_l(-1) + ecnia_l(0) * y_xp_l_1 + ecnia_l(-1) * y_xp_l_2 + eh_l(0) * y_xp_l_3 + eh_l(-1) * y_xp_l_4 + ebfi_l(0) * y_xp_l_5 + ebfi_l(-1) * y_xp_l_6 + egfe_l(0) * y_xp_l_7 + egfe_l(-1) * y_xp_l_8 + egse_l(0) * y_xp_l_9 + egse_l(-1) * y_xp_l_10 + ex_l(0) * y_xp_l_11 + ex_l(-1) * y_xp_l_12; + + xpn_l(0) = pxp_l(0) + xp_l(0); + + ydn_l(0) = y_ydn_l_2 * uyd + ypn_l(0) * y_ydn_l_3 + tpn_l(0) * y_ydn_l_4; + + yh_l(0) = yhl_l(0) * y_yh_l_2 + yht_l(0) * y_yh_l_3 + y_yh_l_4 * yhp_l(0); + + yhgap(0) = y_yhgap_1 * yhshr_l(0) + y_yhgap_2 * zyhst_l(0); + + yhibn_l(0) = xgdpn_l(0) + y_yhibn_l_2 * uyhibn; + + yhl_l(0) = (yhln_l(0) + tryh(0) * y_yhl_l_2) - pcnia_l(0); + + yhln_l(0) = y_yhln_l_2 * uyhln + yniln_l(0); + + yhp_l(0) = (tryh(0) * y_yhp_l_2 + yhptn_l(0) * y_yhp_l_3 + y_yhp_l_4 * yhpntn_l(0)) - pcnia_l(0); + + yhpcd_l(0) = kcd_l(-1); + + yhpgap(0) = y_yhpgap_1 * yhpshr_l(0) + y_yhpgap_2 * zyhpst_l(0); + + yhpntn_l(0) = pcnia_l(0) * y_yhpntn_l_2 + pcdr_l(0) * y_yhpntn_l_3 + yhpcd_l(0) * y_yhpntn_l_4 + yhibn_l(0) * y_yhpntn_l_5 + ynicpn_l(0) * y_yhpntn_l_6 + tcin_l(0) * y_yhpntn_l_7 + y_yhpntn_l_8 * ynidn_l(0) + zpi10(0) * y_yhpntn_l_9 + gfdbtn_l(0) * y_yhpntn_l_10; + + yhpshr_l(0) = yhp_l(0) - yh_l(0); + + yhptn_l(0) = y_yhptn_l_2 * uyhptn + y_yhptn_l_3 * ynirn_l(0) + gfintn_l(0) * y_yhptn_l_4 + ynidn_l(0) * y_yhptn_l_5 + yhibn_l(0) * y_yhptn_l_6; + + yhshr_l(0) = yh_l(0) * y_yhshr_l_2 + xgdp_l(0) * y_yhshr_l_3; + + yhsn_l(0) = yhln_l(0) * y_yhsn_l_2 + y_yhsn_l_3 * yhtn_l(0) + yhptn_l(0) * y_yhsn_l_4 + tpn_l(0) * y_yhsn_l_5 + ecnian_l(0) * y_yhsn_l_6 + yhibn_l(0) * y_yhsn_l_7 + y_yhsn_l_8 * uyhsn + xgdptn_l(0) * y_yhsn_l_9; + + yht_l(0) = yhtn_l(0) - pcnia_l(0); + + yhtgap(0) = y_yhtgap_1 * yhtshr_l(0) + y_yhtgap_2 * zyhtst_l(0); + + yhtn_l(0) = gtn_l(0) + y_yhtn_l_2 * uyhtn; + + yhtshr_l(0) = yht_l(0) - yh_l(0); + + ykbfin_l(0) = pxb_l(0) + rtbfi_l(0) + kbfi_l(0) * y_ykbfin_l_2 + kbfi_l(-1) * y_ykbfin_l_3; + + ykin_l(0) = pxb_l(0) + rtinv(0) * y_ykin_l_2 + ki_l(0) * y_ykin_l_3 + ki_l(-1) * y_ykin_l_4; + + ynicpn_l(0) = y_ynicpn_l_2 * ynin_l(0) + yniln_l(0) * y_ynicpn_l_3 + ynirn_l(0) * y_ynicpn_l_4 + uynicpnr(0) * y_ynicpn_l_5 + xgdpn_l(0) * y_ynicpn_l_6; + + ynidn_l(0) = zynid(0) + y_ynidn_l_8 * pxb_l(-2) + y_ynidn_l_7 * ymsdn(-2) + y_ynidn_l_6 * ynidn_l(-2) + y_ynidn_l_5 * qynidn_l(-1) + ynidn_l_aerr + pxb_l(-1) * y_ynidn_l_4 + pxb_l(0) + y_ynidn_l_1 * ymsdn + y_ynidn_l_2 * ynidn_l(-1) + y_ynidn_l_3 * ymsdn(-1); + + yniln_l(0) = y_yniln_l_2 * uyl + pl_l(0) * y_yniln_l_3 + lhp_l(0) * y_yniln_l_4 + pgfl_l(0) * y_yniln_l_5 + egfl_l(0) * y_yniln_l_6 + pgsl_l(0) * y_yniln_l_7 + egsl_l(0) * y_yniln_l_8; + + ynin_l(0) = y_ynin_l_2 * uyni + xgdin_l(0) * y_ynin_l_3 + fynicn_l(0) * y_ynin_l_4 + fyniln_l(0) * y_ynin_l_5 + jccan_l(0) * y_ynin_l_6; + + ynirn_l(0) = xgdpn_l(0) + y_ynirn_l_1 * ynirn_l_aerr + y_ynirn_l_2 * ynirn_l(-1) + xgdpn_l(-1) * y_ynirn_l_3 + rbbb(0) * y_ynirn_l_4 + y_ynirn_l_5 * rbbb(-1); + + ypn_l(0) = y_ypn_l_2 * uyp + yhln_l(0) * y_ypn_l_3 + yhtn_l(0) * y_ypn_l_4 + yhptn_l(0) * y_ypn_l_5; + + zdivgr(0) = y_zdivgr_1 * hgynid(1) + y_zdivgr_2 * zdivgr(1); + + zebfi(0) = hgpbfir(-1) * y_zebfi_21 + y_zebfi_20 * hxbt(-1) + qebfi_l(-1) * y_zebfi_15 + y_zebfi_11 * xgap(-1) + ptr(-1) * y_zebfi_10 + y_zebfi_9 * rtr(-1) + rff(-1) * y_zebfi_5 + picnia(-1) * y_zebfi_1 + y_zebfi_2 * picnia(-2) + y_zebfi_3 * picnia(-3) + y_zebfi_4 * picnia(-4) + y_zebfi_6 * rff(-2) + y_zebfi_7 * rff(-3) + y_zebfi_8 * rff(-4) + y_zebfi_12 * xgap(-2) + y_zebfi_13 * xgap(-3) + y_zebfi_14 * xgap(-4) + y_zebfi_16 * qebfi_l(-2) + y_zebfi_17 * qebfi_l(-3) + y_zebfi_18 * qebfi_l(-4) + y_zebfi_19 * qebfi_l(-5); + + zecd(0) = y_zecd_33 * qecd_l(-5) + y_zecd_32 * qecd_l(-4) + y_zecd_31 * qecd_l(-3) + y_zecd_30 * qecd_l(-2) + qecd_l(-1) * y_zecd_29 + hggdpt(-1) * y_zecd_27 + y_zecd_23 * yhpgap(-1) + y_zecd_19 * yhtgap(-1) + y_zecd_15 * yhgap(-1) + rtr(-1) * y_zecd_14 + ptr(-1) * y_zecd_13 + xgap2(-1) * y_zecd_9 + rff(-1) * y_zecd_5 + picnia(-1) * y_zecd_1 + y_zecd_2 * picnia(-2) + y_zecd_3 * picnia(-3) + y_zecd_4 * picnia(-4) + y_zecd_6 * rff(-2) + y_zecd_7 * rff(-3) + y_zecd_8 * rff(-4) + y_zecd_10 * xgap2(-2) + y_zecd_11 * xgap2(-3) + y_zecd_12 * xgap2(-4) + y_zecd_16 * yhgap(-2) + y_zecd_17 * yhgap(-3) + y_zecd_18 * yhgap(-4) + y_zecd_20 * yhtgap(-2) + y_zecd_21 * yhtgap(-3) + y_zecd_22 * yhtgap(-4) + y_zecd_24 * yhpgap(-2) + y_zecd_25 * yhpgap(-3) + y_zecd_26 * yhpgap(-4) + y_zecd_28 * hgpcdr(-1); + + zeco(0) = qeco_l(-1) * y_zeco_28 + hggdpt(-1) * y_zeco_27 + yhpgap(-1) * y_zeco_23 + yhtgap(-1) * y_zeco_19 + yhgap(-1) * y_zeco_15 + rtr(-1) * y_zeco_14 + ptr(-1) * y_zeco_13 + xgap2(-1) * y_zeco_9 + rff(-1) * y_zeco_5 + picnia(-1) * y_zeco_1 + y_zeco_2 * picnia(-2) + y_zeco_3 * picnia(-3) + y_zeco_4 * picnia(-4) + y_zeco_6 * rff(-2) + y_zeco_7 * rff(-3) + y_zeco_8 * rff(-4) + y_zeco_10 * xgap2(-2) + y_zeco_11 * xgap2(-3) + y_zeco_12 * xgap2(-4) + y_zeco_16 * yhgap(-2) + y_zeco_17 * yhgap(-3) + y_zeco_18 * yhgap(-4) + y_zeco_20 * yhtgap(-2) + y_zeco_21 * yhtgap(-3) + y_zeco_22 * yhtgap(-4) + y_zeco_24 * yhpgap(-2) + y_zeco_25 * yhpgap(-3) + y_zeco_26 * yhpgap(-4) + y_zeco_29 * qeco_l(-2) + y_zeco_30 * qeco_l(-3) + y_zeco_31 * qeco_l(-4) + y_zeco_32 * qeco_l(-5); + + zeh(0) = qeh_l(-1) * y_zeh_28 + hggdpt(-1) * y_zeh_27 + yhpgap(-1) * y_zeh_23 + yhtgap(-1) * y_zeh_19 + yhgap(-1) * y_zeh_15 + rtr(-1) * y_zeh_14 + ptr(-1) * y_zeh_13 + xgap2(-1) * y_zeh_9 + rff(-1) * y_zeh_5 + picnia(-1) * y_zeh_1 + y_zeh_2 * picnia(-2) + y_zeh_3 * picnia(-3) + y_zeh_4 * picnia(-4) + y_zeh_6 * rff(-2) + y_zeh_7 * rff(-3) + y_zeh_8 * rff(-4) + y_zeh_10 * xgap2(-2) + y_zeh_11 * xgap2(-3) + y_zeh_12 * xgap2(-4) + y_zeh_16 * yhgap(-2) + y_zeh_17 * yhgap(-3) + y_zeh_18 * yhgap(-4) + y_zeh_20 * yhtgap(-2) + y_zeh_21 * yhtgap(-3) + y_zeh_22 * yhtgap(-4) + y_zeh_24 * yhpgap(-2) + y_zeh_25 * yhpgap(-3) + y_zeh_26 * yhpgap(-4) + y_zeh_29 * qeh_l(-2) + y_zeh_30 * qeh_l(-3) + y_zeh_31 * qeh_l(-4) + y_zeh_32 * qeh_l(-5); + + zgap05(0) = xgap(0) * y_zgap05_1 + y_zgap05_2 * zgap05(1); + + zgap10(0) = xgap(0) * y_zgap10_1 + y_zgap10_2 * zgap10(1); + + zgap30(0) = xgap(0) * y_zgap30_1 + y_zgap30_2 * zgap30(1); + + zgapc2(0) = rtr(-1) * y_zgapc2_14 + ptr(-1) * y_zgapc2_13 + xgap2(-1) * y_zgapc2_9 + rff(-1) * y_zgapc2_5 + picnia(-1) * y_zgapc2_1 + y_zgapc2_2 * picnia(-2) + y_zgapc2_3 * picnia(-3) + y_zgapc2_4 * picnia(-4) + y_zgapc2_6 * rff(-2) + y_zgapc2_7 * rff(-3) + y_zgapc2_8 * rff(-4) + y_zgapc2_10 * xgap2(-2) + y_zgapc2_11 * xgap2(-3) + y_zgapc2_12 * xgap2(-4); + + zlhp(0) = hqlww(-1) * y_zlhp_20 + y_zlhp_19 * hlept(-1) + y_zlhp_17 * lprdt_l(-1) + xbo_l(-1) * y_zlhp_15 + xgap(-1) * y_zlhp_11 + ptr(-1) * y_zlhp_10 + rtr(-1) * y_zlhp_9 + rff(-1) * y_zlhp_5 + picnia(-1) * y_zlhp_1 + y_zlhp_2 * picnia(-2) + y_zlhp_3 * picnia(-3) + y_zlhp_4 * picnia(-4) + y_zlhp_6 * rff(-2) + y_zlhp_7 * rff(-3) + y_zlhp_8 * rff(-4) + y_zlhp_12 * xgap(-2) + y_zlhp_13 * xgap(-3) + y_zlhp_14 * xgap(-4) + y_zlhp_16 * xbo_l(-2) + y_zlhp_18 * lprdt_l(-2); + + zpi10(0) = picnia(0) * y_zpi10_1 + y_zpi10_2 * zpi10(1); + + zpi10f(0) = picnia(0) * y_zpi10f_1 + y_zpi10f_2 * zpi10f(1); + + zpi5(0) = xgap(-1) * y_zpi5_11 + ptr(-1) * y_zpi5_10 + rtr(-1) * y_zpi5_9 + rff(-1) * y_zpi5_5 + picnia(-1) * y_zpi5_1 + y_zpi5_2 * picnia(-2) + y_zpi5_3 * picnia(-3) + y_zpi5_4 * picnia(-4) + y_zpi5_6 * rff(-2) + y_zpi5_7 * rff(-3) + y_zpi5_8 * rff(-4) + y_zpi5_12 * xgap(-2) + y_zpi5_13 * xgap(-3) + y_zpi5_14 * xgap(-4); + + zpib5(0) = pxb_l(0) * y_zpib5_1 + pxb_l(-1) * y_zpib5_2 + y_zpib5_3 * zpib5(1); + + zpic30(0) = picnia(0) * y_zpic30_1 + y_zpic30_2 * zpic30(1); + + zpic58(0) = pic4(8); + + zpicxfe(0) = lurnat(-1) * y_zpicxfe_26 + lur(-1) * y_zpicxfe_25 + huqpct(-1) * y_zpicxfe_24 + hlprdt(-1) * y_zpicxfe_23 + pl_l(-1) * y_zpicxfe_22 + qpl_l(-1) * y_zpicxfe_21 + pcnia_l(-1) * y_zpicxfe_20 + qpcnia_l(-1) * y_zpicxfe_19 + ptr(-1) * y_zpicxfe_18 + rtr(-1) * y_zpicxfe_17 + xgap2(-1) * y_zpicxfe_13 + rff(-1) * y_zpicxfe_9 + pieci(-1) * y_zpicxfe_5 + picxfe(-1) * y_zpicxfe_1 + y_zpicxfe_2 * picxfe(-2) + y_zpicxfe_3 * picxfe(-3) + y_zpicxfe_4 * picxfe(-4) + y_zpicxfe_6 * pieci(-2) + y_zpicxfe_7 * pieci(-3) + y_zpicxfe_8 * pieci(-4) + y_zpicxfe_10 * rff(-2) + y_zpicxfe_11 * rff(-3) + y_zpicxfe_12 * rff(-4) + y_zpicxfe_14 * xgap2(-2) + y_zpicxfe_15 * xgap2(-3) + y_zpicxfe_16 * xgap2(-4) + y_zpicxfe_27 * lur(-2) + y_zpicxfe_28 * lurnat(-2); + + zpieci(0) = lurnat(-1) * y_zpieci_26 + lur(-1) * y_zpieci_25 + huqpct(-1) * y_zpieci_24 + hlprdt(-1) * y_zpieci_23 + pl_l(-1) * y_zpieci_22 + qpl_l(-1) * y_zpieci_21 + pcnia_l(-1) * y_zpieci_20 + qpcnia_l(-1) * y_zpieci_19 + ptr(-1) * y_zpieci_18 + rtr(-1) * y_zpieci_17 + xgap2(-1) * y_zpieci_13 + rff(-1) * y_zpieci_9 + pieci(-1) * y_zpieci_5 + picxfe(-1) * y_zpieci_1 + y_zpieci_2 * picxfe(-2) + y_zpieci_3 * picxfe(-3) + y_zpieci_4 * picxfe(-4) + y_zpieci_6 * pieci(-2) + y_zpieci_7 * pieci(-3) + y_zpieci_8 * pieci(-4) + y_zpieci_10 * rff(-2) + y_zpieci_11 * rff(-3) + y_zpieci_12 * rff(-4) + y_zpieci_14 * xgap2(-2) + y_zpieci_15 * xgap2(-3) + y_zpieci_16 * xgap2(-4) + y_zpieci_27 * lur(-2) + y_zpieci_28 * lurnat(-2); + + zrff10(0) = rff(0) * y_zrff10_1 + y_zrff10_2 * zrff10(1); + + zrff30(0) = rff(0) * y_zrff30_1 + y_zrff30_2 * zrff30(1); + + zrff5(0) = rff(0) * y_zrff5_1 + y_zrff5_2 * zrff5(1); + + zyh_l(0) = xgdpt_l(0) + zyhst_l(0) + yhgap(-1) * y_zyh_l_16 + yhgap(0) * y_zyh_l_15 + rtr(0) * y_zyh_l_14 + ptr(0) * y_zyh_l_13 + xgap2(-1) * y_zyh_l_10 + xgap2(0) * y_zyh_l_9 + rff(-1) * y_zyh_l_6 + rff(0) * y_zyh_l_5 + picnia(0) * y_zyh_l_1 + picnia(-1) * y_zyh_l_2 + y_zyh_l_3 * picnia(-2) + y_zyh_l_4 * picnia(-3) + y_zyh_l_7 * rff(-2) + y_zyh_l_8 * rff(-3) + y_zyh_l_11 * xgap2(-2) + y_zyh_l_12 * xgap2(-3) + y_zyh_l_17 * yhgap(-2) + y_zyh_l_18 * yhgap(-3); + + zyhp_l(0) = xgdpt_l(0) + zyhst_l(0) + zyhpst_l(0) + yhpgap(-1) * y_zyhp_l_20 + yhpgap(0) * y_zyhp_l_19 + yhgap(-1) * y_zyhp_l_16 + yhgap(0) * y_zyhp_l_15 + rtr(0) * y_zyhp_l_14 + ptr(0) * y_zyhp_l_13 + xgap2(-1) * y_zyhp_l_10 + xgap2(0) * y_zyhp_l_9 + rff(-1) * y_zyhp_l_6 + rff(0) * y_zyhp_l_5 + picnia(0) * y_zyhp_l_1 + picnia(-1) * y_zyhp_l_2 + y_zyhp_l_3 * picnia(-2) + y_zyhp_l_4 * picnia(-3) + y_zyhp_l_7 * rff(-2) + y_zyhp_l_8 * rff(-3) + y_zyhp_l_11 * xgap2(-2) + y_zyhp_l_12 * xgap2(-3) + y_zyhp_l_17 * yhgap(-2) + y_zyhp_l_18 * yhgap(-3) + y_zyhp_l_21 * yhpgap(-2) + y_zyhp_l_22 * yhpgap(-3); + + zyhpst_l(0) = zyhpst_l(-1) + yhpgap(-1) * y_zyhpst_l_1; + + zyhst_l(0) = zyhst_l(-1) + yhgap(-1) * y_zyhst_l_1; + + zyht_l(0) = xgdpt_l(0) + zyhst_l(0) + zyhtst_l(0) + yhtgap(-1) * y_zyht_l_20 + yhtgap(0) * y_zyht_l_19 + yhgap(-1) * y_zyht_l_16 + yhgap(0) * y_zyht_l_15 + rtr(0) * y_zyht_l_14 + ptr(0) * y_zyht_l_13 + xgap2(-1) * y_zyht_l_10 + xgap2(0) * y_zyht_l_9 + rff(-1) * y_zyht_l_6 + rff(0) * y_zyht_l_5 + picnia(0) * y_zyht_l_1 + picnia(-1) * y_zyht_l_2 + y_zyht_l_3 * picnia(-2) + y_zyht_l_4 * picnia(-3) + y_zyht_l_7 * rff(-2) + y_zyht_l_8 * rff(-3) + y_zyht_l_11 * xgap2(-2) + y_zyht_l_12 * xgap2(-3) + y_zyht_l_17 * yhgap(-2) + y_zyht_l_18 * yhgap(-3) + y_zyht_l_21 * yhtgap(-2) + y_zyht_l_22 * yhtgap(-3); + + zyhtst_l(0) = zyhtst_l(-1) + yhtgap(-1) * y_zyhtst_l_1; + + zynid(0) = hggdpt(-1) * y_zynid_25 + pxb_l(-1) * y_zynid_16 + qynidn_l(-1) * y_zynid_15 + xgap(-1) * y_zynid_11 + ptr(-1) * y_zynid_10 + rtr(-1) * y_zynid_9 + rff(-1) * y_zynid_5 + picnia(-1) * y_zynid_1 + y_zynid_2 * picnia(-2) + y_zynid_3 * picnia(-3) + y_zynid_4 * picnia(-4) + y_zynid_6 * rff(-2) + y_zynid_7 * rff(-3) + y_zynid_8 * rff(-4) + y_zynid_12 * xgap(-2) + y_zynid_13 * xgap(-3) + y_zynid_14 * xgap(-4) + y_zynid_17 * qynidn_l(-2) + y_zynid_18 * pxb_l(-2) + y_zynid_19 * qynidn_l(-3) + y_zynid_20 * pxb_l(-3) + y_zynid_21 * qynidn_l(-4) + y_zynid_22 * pxb_l(-4) + y_zynid_23 * qynidn_l(-5) + y_zynid_24 * pxb_l(-5); + + ugap(0) = lur(0) - lurnat(0); + + rff(0) = rule(0) + eradd; + + rule(0) = ((rff(-1) * 0.85 + rstar * 0.15 + picx4(0) * 0.225) - 0.075 * pitarg) + xgap2(0) * 0.15; + + fiscal(0) = (1 - rho_fiscal) * fbar_iscal + rho_fiscal * fiscal(-1) + fiscal_aerr; + + fiscalav(0) = av * fiscal(0) + fiscalav(-1) * rho_fiscalav; + + gov_exp_share(0) = egfe_l(0) * y_xfs_l_7 * 100; + + income_tax_share_of_gdp(0) = 100 * (y_yh_l_2 * (-y_yhl_l_2 - 1) + y_yh_l_4 * (-y_yhp_l_2 - 1)) * tryh(0); + + debt_to_gdp(0) = -(gfdbtnp_l(0)) * y_gfdbtnp_l_4 * y_gfrecn_l_4 * y_gfrecn_l_5 * 100; + +end; + +shocks; +var adjlegrt = 1; +var d79a = 1; +var d8095 = 1; +var d83 = 1; +var d87 = 1; +var ddockm = 1; +var ddockx = 1; +var dfmprr = 1; +var dglprd = 1; +var ebfi_l_aerr = 1; +var ecd_l_aerr = 1; +var ech_l_aerr = 1; +var eco_l_aerr = 1; +var egfe_l_aerr = 1; +var egfl_l_aerr = 1; +var egse_l_aerr = 1; +var egsl_l_aerr = 1; +var eh_l_aerr = 1; +var emo_l_aerr = 1; +var emp_l_aerr = 1; +var emptrt = 1; +var eradd = 1; +var ex_l_aerr = 1; +var fiscal_aerr = 1; +var fpitrg = 1; +var fpxrr_l_aerr = 1; +var fpxrrt = 1; +var fxgap_aerr = 1; +var gfdrt = 1; +var gfsrt_err = 1; +var gtrd_aerr = 1; +var gtrt = 1; +var hgpcdr = 1; +var hksr = 1; +var hmfpt_aerr = 1; +var hqlfpr_aerr = 1; +var hqlww_aerr = 1; +var jrbfi = 1; +var jrcd = 1; +var jrh = 1; +var ki_l_aerr = 1; +var leo_l_aerr = 1; +var lfpr_aerr = 1; +var lhp_l_aerr = 1; +var lqualt_l = 1; +var lurnat_aerr = 1; +var lww_l_aerr = 1; +var mfpt_l_aerr = 1; +var n16_l = 1; +var pbfir_l_aerr = 1; +var pcer_l_aerr = 1; +var pcfr_l_aerr = 1; +var pcfrt = 1; +var pegfr_l_aerr = 1; +var pegsr_l_aerr = 1; +var phouse_l_aerr = 1; +var phr_l_aerr = 1; +var picxfe_aerr = 1; +var pieci_aerr = 1; +var pitarg = 1; +var pkir = 1; +var pmo_l_aerr = 1; +var poilr_l_aerr = 1; +var poilrt = 1; +var pwstar_l = 1; +var pxr_l_aerr = 1; +var qleor = 1; +var rbbbp_aerr = 1; +var rcar_aerr = 1; +var rcgain_aerr = 1; +var reqp_aerr = 1; +var rfnict = 1; +var rfrs10 = 1; +var rfynic_aerr = 1; +var rfynil_aerr = 1; +var rg10p_aerr = 1; +var rg30p_aerr = 1; +var rg5p_aerr = 1; +var rgfint_aerr = 1; +var rme_aerr = 1; +var t47 = 1; +var tapddp = 1; +var tdpv = 1; +var trci_aerr = 1; +var trcit = 1; +var trfcim = 1; +var trfpm = 1; +var tritc = 1; +var trp_aerr = 1; +var trspp = 1; +var uemot = 1; +var ufcbr = 1; +var ufnir = 1; +var uftcin = 1; +var ugfdbt_l = 1; +var ugfdbtp_lerr = 1; +var upcpi = 1; +var upcpix = 1; +var upgfl = 1; +var upgsl = 1; +var upkbfir = 1; +var upmp = 1; +var upxb = 1; +var uvbfi = 1; +var uyd = 1; +var uyhibn = 1; +var uyhln = 1; +var uyhptn = 1; +var uyhsn = 1; +var uyhtn = 1; +var uyl = 1; +var uyni = 1; +var uyp = 1; +var ymsdn = 1; +var ynidn_l_aerr = 1; +var ynirn_l_aerr = 1; +end; + +initval; + debt_to_gdp = 0.0; + delrff = 0.0; + dpadj = 0.0; + dpgap = 0.0; + ebfi_l = 0.0; + ebfin_l = 0.0; + ecd_l = 0.0; + ech_l = 0.0; + ecnia_l = 0.0; + ecnian_l = 0.0; + eco_l = 0.0; + egfe_l = 0.0; + egfen_l = 0.0; + egfet_l = 0.0; + egfl_l = 0.0; + egfln_l = 0.0; + egflt_l = 0.0; + egse_l = 0.0; + egsen_l = 0.0; + egset_l = 0.0; + egsl_l = 0.0; + egsln_l = 0.0; + egslt_l = 0.0; + eh_l = 0.0; + ehn_l = 0.0; + emn_l = 0.0; + emo_l = 0.0; + emo_ltilde = 0.0; + emon_l = 0.0; + emp_l = 0.0; + empn_l = 0.0; + ex_l = 0.0; + exn_l = 0.0; + fcbn_l = 0.0; + fgdp_l = 0.0; + fgdpt_l = 0.0; + fiscal = 0.0; + fiscalav = 0.0; + fnicn_l = 0.0; + fniln_l = 0.0; + fnirn_l = 0.0; + fpc_l = 0.0; + fpi10 = 0.0; + fpi10t = 0.0; + fpic = 0.0; + fpx_l = 0.0; + fpxr_l = 0.0; + fpxrr_l = 0.0; + fpxrr_ltilde = 0.0; + frl10 = 0.0; + frs10 = 0.0; + frstar = 0.0; + ftcin_l = 0.0; + fxgap = 0.0; + fynicn_l = 0.0; + fyniln_l = 0.0; + gfdbtn_l = 0.0; + gfdbtnp_l = 0.0; + gfexpn_l = 0.0; + gfintn_l = 0.0; + gfrecn_l = 0.0; + gfsrt = 0.0; + gov_exp_share = 0.0; + gtn_l = 0.0; + gtr_l = 0.0; + gtrd = 0.0; + hgemp = -0.0; + hggdp = 0.0; + hggdpt = 0.0; + hgpbfir = 0.0; + hgpkir = 0.0; + hgynid = 0.0; + hks = 0.0; + hlept = 0.0; + hlprdt = 0.0; + hmfpt = 0.0; + hqlfpr = 0.0; + hqlww = 0.0; + huqpct = 0.0; + huxb = 0.0; + hxbt = 0.0; + income_tax_share_of_gdp = 0.0; + jccan_l = 0.0; + jkcd_l = 0.0; + kbfi_l = 0.0; + kcd_l = 0.0; + kh_l = 0.0; + ki_l = 0.0; + ks_l = 0.0; + leg_l = 0.0; + leh_l = 0.0; + leo_l = 0.0; + lep_l = 0.0; + leppot_l = 0.0; + lf_l = 0.0; + lfpr = 0.0; + lhp_l = 0.0; + lprdt_l = 0.0; + lur = 0.0; + lurnat = 0.0; + lww_l = 0.0; + mfpt_l = 0.0; + pbfir_l = 0.0; + pcdr_l = 0.0; + pcer_l = 0.0; + pcfr_l = 0.0; + pchr_l = 0.0; + pcnia_l = 0.0; + pcor_l = 0.0; + pcpi_l = 0.0; + pcpix_l = 0.0; + pcxfe_l = 0.0; + pegfr_l = 0.0; + pegsr_l = 0.0; + pgdp_l = 0.0; + pgfl_l = 0.0; + pgsl_l = 0.0; + phouse_l = 0.0; + phr_l = 0.0; + pic4 = 0.0; + picnia = 0.0; + picx4 = 0.0; + picxfe = 0.0; + pieci = 0.0; + pigdp = 0.0; + pipl = 0.0; + pipxnc = 0.0; + pkbfir = 0.0; + pl_l = 0.0; + pmo_l = 0.0; + pmo_ltilde = 0.0; + pmp_l = 0.0; + poil_l = 0.0; + poilr_l = 0.0; + ptr = 0.0; + pxb_l = 0.0; + pxnc_l = 0.0; + pxp_l = 0.0; + pxr_l = 0.0; + qebfi_l = 0.0; + qec_l = 0.0; + qecd_l = 0.0; + qeco_l = 0.0; + qeh_l = 0.0; + qkir_l = 0.0; + qlf_l = 0.0; + qlfpr = 0.0; + qlhp_l = 0.0; + qlww_l = 0.0; + qpcnia_l = 0.0; + qpl_l = 0.0; + qpxb_l = 0.0; + qpxnc_l = 0.0; + qpxp_l = 0.0; + qynidn_l = 0.0; + rbbb = 0.0; + rbbbp = 0.0; + rbfi = 0.0; + rcar = 0.0; + rccd = 0.0; + rcch = 0.0; + rcgain = 0.0; + req = 0.0; + reqp = 0.0; + rff = 0.0; + rfynic = 0.0; + rfynil = 0.0; + rg10 = 0.0; + rg10p = 0.0; + rg30 = 0.0; + rg30p = 0.0; + rg5 = 0.0; + rg5p = 0.0; + rgfint = 0.0; + rgw = 0.0; + rme = 0.0; + rrff = 0.0; + rrtr = 0.0; + rspnia = 0.0; + rtb = 0.0; + rtbfi_l = 0.0; + rtinv = 0.0; + rtr = 0.0; + rule = 0.0; + tcin_l = 0.0; + tpn_l = 0.0; + trci = 0.0; + trp = 0.0; + trp_a = 0.0; + trpt = 0.0; + trptd = 0.0; + trpts = 0.0; + tryh = 0.0; + ugap = 0.0; + ugfdbtp_l = 0.0; + ugfsrp = 0.0; + uleg_l = 0.0; + uqpct_l = 0.0; + uxbt_l = 0.0; + uynicpnr = 0.0; + vbfi = 0.0; + wpo_l = 0.0; + wpon_l = 0.0; + wps_l = 0.0; + wpsn_l = 0.0; + xb_l = 0.0; + xbn_l = 0.0; + xbo_l = 0.0; + xbt_l = 0.0; + xbtr_l = 0.0; + xfs_l = 0.0; + xfsn_l = 0.0; + xgap = 0.0; + xgap2 = 0.0; + xgdi_l = 0.0; + xgdin_l = 0.0; + xgdo_l = 0.0; + xgdp_l = 0.0; + xgdpn_l = 0.0; + xgdpt_l = 0.0; + xgdptn_l = 0.0; + xp_l = 0.0; + xpn_l = 0.0; + ydn_l = 0.0; + yh_l = 0.0; + yhgap = 0.0; + yhibn_l = 0.0; + yhl_l = 0.0; + yhln_l = 0.0; + yhp_l = 0.0; + yhpcd_l = 0.0; + yhpgap = 0.0; + yhpntn_l = 0.0; + yhpshr_l = 0.0; + yhptn_l = 0.0; + yhshr_l = 0.0; + yhsn_l = 0.0; + yht_l = 0.0; + yhtgap = 0.0; + yhtn_l = 0.0; + yhtshr_l = 0.0; + ykbfin_l = 0.0; + ykin_l = 0.0; + ynicpn_l = 0.0; + ynidn_l = 0.0; + yniln_l = 0.0; + ynin_l = 0.0; + ynirn_l = 0.0; + ypn_l = 0.0; + zdivgr = 0.0; + zebfi = 0.0; + zecd = 0.0; + zeco = 0.0; + zeh = 0.0; + zgap05 = 0.0; + zgap10 = 0.0; + zgap30 = 0.0; + zgapc2 = 0.0; + zlhp = 0.0; + zpi10 = 0.0; + zpi10f = 0.0; + zpi5 = 0.0; + zpib5 = 0.0; + zpic30 = 0.0; + zpic58 = 0.0; + zpicxfe = 0.0; + zpieci = 0.0; + zrff10 = 0.0; + zrff30 = 0.0; + zrff5 = 0.0; + zyh_l = 0.0; + zyhp_l = 0.0; + zyhpst_l = 0.0; + zyhst_l = 0.0; + zyht_l = 0.0; + zyhtst_l = 0.0; + zynid = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/GNSS_2010.jl b/test/GNSS_2010.jl new file mode 100644 index 000000000..73cb04e12 --- /dev/null +++ b/test/GNSS_2010.jl @@ -0,0 +1,297 @@ +using MacroModelling + +@model GNSS_2010 begin + (1 - a_i) * exp(ee_z[0]) * (c_p[0] - a_i * c_p[-1]) ^ (-1) = lam_p[0] + + j * exp(ee_j[0]) / h_p[0] - lam_p[0] * q_h[0] + beta_p * lam_p[1] * q_h[1] = 0 + + lam_p[0] = beta_p * lam_p[1] * (1 + r_d[0]) / pie[1] + + (1 - exp(eps_l[0])) * l_p[0] + exp(eps_l[0]) * l_p[0] ^ (1 + phi) / w_p[0] / lam_p[0] - pie_wp[0] * kappa_w * (pie_wp[0] - pie[-1] ^ ind_w * piss ^ (1 - ind_w)) + lam_p[1] * beta_p * kappa_w / lam_p[0] * (pie_wp[1] - piss ^ (1 - ind_w) * pie[0] ^ ind_w) * pie_wp[1] ^ 2 / pie[1] = 0 + + pie_wp[0] = w_p[0] * pie[0] / w_p[-1] + + c_p[0] + q_h[0] * (h_p[0] - h_p[-1]) + d_p[0] = l_p[0] * w_p[0] + (1 + r_d[-1]) * d_p[-1] / pie[0] + J_R[0] / gamma_p + + (1 - a_i) * exp(ee_z[0]) * (c_i[0] - a_i * c_i[-1]) ^ (-1) = lam_i[0] + + j * exp(ee_j[0]) / h_i[0] - q_h[0] * lam_i[0] + q_h[1] * beta_i * lam_i[1] + q_h[1] * pie[1] * s_i[0] * exp(m_i[0]) = 0 + + lam_i[0] - beta_i * lam_i[1] * (1 + r_bh[0]) / pie[1] = s_i[0] * (1 + r_bh[0]) + + (1 - exp(eps_l[0])) * l_i[0] + exp(eps_l[0]) * l_i[0] ^ (1 + phi) / w_i[0] / lam_i[0] - kappa_w * pie_wi[0] * (pie_wi[0] - pie[-1] ^ ind_w * piss ^ (1 - ind_w)) + lam_i[1] * kappa_w * beta_i / lam_i[0] * (pie_wi[1] - piss ^ (1 - ind_w) * pie[0] ^ ind_w) * pie_wi[1] ^ 2 / pie[1] = 0 + + pie_wi[0] = pie[0] * w_i[0] / w_i[-1] + + c_i[0] + q_h[0] * (h_i[0] - h_i[-1]) + (1 + r_bh[-1]) * b_i[-1] / pie[0] = l_i[0] * w_i[0] + b_i[0] + + (1 + r_bh[0]) * b_i[0] = exp(m_i[0]) * q_h[1] * pie[1] * h_i[0] + + K[0] = (1 - deltak) * K[-1] + I[0] * (1 - kappa_i / 2 * (I[0] * exp(ee_qk[0]) / I[-1] - 1) ^ 2) + + 1 = q_k[0] * (1 - kappa_i / 2 * (I[0] * exp(ee_qk[0]) / I[-1] - 1) ^ 2 - (I[0] * exp(ee_qk[0]) / I[-1] - 1) * kappa_i * I[0] * exp(ee_qk[0]) / I[-1]) + kappa_i * exp(ee_qk[1]) * beta_e * lam_e[1] / lam_e[0] * q_k[1] * (exp(ee_qk[1]) * I[1] / I[0] - 1) * (I[1] / I[0]) ^ 2 + + (1 - a_i) * (c_e[0] - a_i * c_e[-1]) ^ (-1) = lam_e[0] + + q_k[1] * pie[1] * (1 - deltak) * s_e[0] * exp(m_e[0]) + beta_e * lam_e[1] * ((1 - deltak) * q_k[1] + r_k[1] * u[1] - (eksi_1 * (u[1] - 1) + eksi_2 / 2 * (u[1] - 1) ^ 2)) = q_k[0] * lam_e[0] + + w_p[0] = ni * (1 - alpha) * y_e[0] / (l_pd[0] * x[0]) + + w_i[0] = (1 - alpha) * y_e[0] * (1 - ni) / (x[0] * l_id[0]) + + lam_e[0] - s_e[0] * (1 + r_be[0]) = beta_e * lam_e[1] * (1 + r_be[0]) / pie[1] + + r_k[0] = eksi_1 + eksi_2 * (u[0] - 1) + + c_e[0] + (1 + r_be[-1]) * b_ee[-1] / pie[0] + w_p[0] * l_pd[0] + w_i[0] * l_id[0] + q_k[0] * k_e[0] + (eksi_1 * (u[0] - 1) + eksi_2 / 2 * (u[0] - 1) ^ 2) * k_e[-1] = y_e[0] / x[0] + b_ee[0] + q_k[0] * (1 - deltak) * k_e[-1] + + y_e[0] = exp(A_e[0]) * (u[0] * k_e[-1]) ^ alpha * (l_pd[0] ^ ni * l_id[0] ^ (1 - ni)) ^ (1 - alpha) + + (1 + r_be[0]) * b_ee[0] = exp(m_e[0]) * q_k[1] * pie[1] * (1 - deltak) * k_e[0] + + r_k[0] = exp(A_e[0]) * alpha * (l_pd[0] ^ ni * l_id[0] ^ (1 - ni)) ^ (1 - alpha) * u[0] ^ (alpha - 1) * k_e[-1] ^ (alpha - 1) / x[0] + + R_b[0] = r_ib[0] + ( - kappa_kb) * (K_b[0] / B[0] - vi) * (K_b[0] / B[0]) ^ 2 + + pie[0] * K_b[0] = (1 - delta_kb) * K_b[-1] / exp(eps_K_b[0]) + j_B[-1] + + gamma_b * d_b[0] = d_p[0] * gamma_p + + gamma_b * b_h[0] = b_i[0] * gamma_i + + gamma_b * b_e[0] = b_ee[0] * gamma_e + + b_h[0] + b_e[0] = K_b[0] + d_b[0] + + lam_p[1] * beta_p * kappa_d / lam_p[0] * (r_d[1] / r_d[0] - (r_d[0] / r_d[-1]) ^ ind_d) * (r_d[1] / r_d[0]) ^ 2 * d_b[1] / d_b[0] + exp(mk_d[0]) / (exp(mk_d[0]) - 1) - 1 - r_ib[0] * exp(mk_d[0]) / (exp(mk_d[0]) - 1) / r_d[0] - r_d[0] * kappa_d * (r_d[0] / r_d[-1] - (r_d[-1] / AUX_ENDO_LAG_54_1[-1]) ^ ind_d) / r_d[-1] = 0 + + 1 + beta_p * lam_p[1] / lam_p[0] * kappa_be * (r_be[1] / r_be[0] - (r_be[0] / r_be[-1]) ^ ind_be) * (r_be[1] / r_be[0]) ^ 2 * b_e[1] / b_e[0] - exp(mk_be[0]) / (exp(mk_be[0]) - 1) + R_b[0] * exp(mk_be[0]) / (exp(mk_be[0]) - 1) / r_be[0] - r_be[0] * kappa_be * (r_be[0] / r_be[-1] - (r_be[-1] / AUX_ENDO_LAG_52_1[-1]) ^ ind_be) / r_be[-1] = 0 + + 1 + beta_p * lam_p[1] / lam_p[0] * kappa_bh * (r_bh[1] / r_bh[0] - (r_bh[0] / r_bh[-1]) ^ ind_bh) * (r_bh[1] / r_bh[0]) ^ 2 * b_h[1] / b_h[0] - exp(mk_bh[0]) / (exp(mk_bh[0]) - 1) + R_b[0] * exp(mk_bh[0]) / (exp(mk_bh[0]) - 1) / r_bh[0] - r_bh[0] * kappa_bh * (r_bh[0] / r_bh[-1] - (r_bh[-1] / AUX_ENDO_LAG_53_1[-1]) ^ ind_bh) / r_bh[-1] = 0 + + j_B[0] = r_bh[0] * b_h[0] + r_be[0] * b_e[0] - r_d[0] * d_b[0] - kappa_d * r_d[0] * d_b[0] / 2 * (r_d[0] / r_d[-1] - 1) ^ 2 - kappa_be * r_be[0] * b_e[0] / 2 * (r_be[0] / r_be[-1] - 1) ^ 2 - kappa_bh * r_bh[0] * b_h[0] / 2 * (r_bh[0] / r_bh[-1] - 1) ^ 2 - kappa_kb * K_b[0] / 2 * (K_b[0] / B[0] - vi) ^ 2 + + J_R[0] = Y[0] * (1 - 1 / x[0] - kappa_p / 2 * (pie[0] - pie[-1] ^ ind_p * piss ^ (1 - ind_p)) ^ 2) + + 1 - exp(eps_y[0]) + exp(eps_y[0]) / x[0] - (pie[0] - pie[-1] ^ ind_p * piss ^ (1 - ind_p)) * pie[0] * kappa_p + kappa_p * lam_p[1] * beta_p * pie[1] / lam_p[0] * (pie[1] - piss ^ (1 - ind_p) * pie[0] ^ ind_p) * Y[1] / Y[0] = 0 + + C[0] = c_p[0] * gamma_p + c_i[0] * gamma_i + c_e[0] * gamma_e + + BH[0] = gamma_b * b_h[0] + + BE[0] = gamma_b * b_e[0] + + B[0] = BH[0] + BE[0] + + D[0] = d_p[0] * gamma_p + + Y[0] = y_e[0] * gamma_e + + J_B[0] = gamma_b * j_B[0] + + l_pd[0] * gamma_e = l_p[0] * gamma_p + + l_id[0] * gamma_e = l_i[0] * gamma_i + + h = h_p[0] * gamma_p + h_i[0] * gamma_i + + K[0] = k_e[0] * gamma_e + + Y1[0] = K[0] + C[0] - (1 - deltak) * K[-1] + + PIW[0] = pie[0] * (w_p[0] + w_i[0]) / (w_p[-1] + w_i[-1]) + + 1 + r_ib[0] = (1 + r_ib_ss) ^ (1 - rho_ib) * (1 + r_ib[-1]) ^ rho_ib * ((pie[0] / piss) ^ phi_pie * (Y1[0] / Y1[-1]) ^ phi_y) ^ (1 - rho_ib) * (1 + sigma__r_ib * e_r_ib[x]) + + exp(ee_z[0]) = 1 - rho_ee_z + rho_ee_z * exp(ee_z[-1]) + sigma__z * e_z[x] + + exp(A_e[0]) = 1 - rho_A_e + rho_A_e * exp(A_e[-1]) + sigma__A_e * e_A_e[x] + + exp(ee_j[0]) = 1 - rho_ee_j + rho_ee_j * exp(ee_j[-1]) - sigma__j * e_j[x] + + exp(m_i[0]) = (1 - rho_mi) * m_i_ss + rho_mi * exp(m_i[-1]) + sigma__mi * e_mi[x] + + exp(m_e[0]) = (1 - rho_me) * m_e_ss + rho_me * exp(m_e[-1]) + sigma__me * e_me[x] + + exp(mk_d[0]) = (1 - rho_mk_d) * mk_d_ss + rho_mk_d * exp(mk_d[-1]) + sigma__mk_d * e_mk_d[x] + + exp(mk_be[0]) = (1 - rho_mk_be) * mk_be_ss + rho_mk_be * exp(mk_be[-1]) + sigma__mk_be * e_mk_be[x] + + exp(mk_bh[0]) = (1 - rho_mk_bh) * mk_bh_ss + rho_mk_bh * exp(mk_bh[-1]) + sigma__mk_bh * e_mk_bh[x] + + exp(ee_qk[0]) = 1 - rho_ee_qk + rho_ee_qk * exp(ee_qk[-1]) + sigma__qk * e_qk[x] + + exp(eps_y[0]) = (1 - rho_eps_y) * eps_y_ss + rho_eps_y * exp(eps_y[-1]) + sigma__y * e_y[x] + + exp(eps_l[0]) = (1 - rho_eps_l) * eps_l_ss + rho_eps_l * exp(eps_l[-1]) + sigma__l * e_l[x] + + exp(eps_K_b[0]) = 1 - rho_eps_K_b + rho_eps_K_b * exp(eps_K_b[-1]) + sigma__eps_K_b * e_eps_K_b[x] + + rr_e[0] = lam_e[0] - beta_e * lam_e[1] * (1 + r_be[0]) / pie[1] + + bm[0] = r_bh[-1] * b_h[-1] / (b_h[-1] + b_e[-1]) + r_be[-1] * b_e[-1] / (b_h[-1] + b_e[-1]) - r_d[-1] + + spr_b[0] = r_bh[0] * 0.5 + r_be[0] * 0.5 - r_d[0] + + AUX_ENDO_LAG_54_1[0] = r_d[-1] + + AUX_ENDO_LAG_52_1[0] = r_be[-1] + + AUX_ENDO_LAG_53_1[0] = r_bh[-1] + +end + + +@parameters GNSS_2010 begin + beta_p = 0.9943 + + beta_i = 0.975 + + j = 0.2 + + phi = 1.0 + + m_i_ss = 0.7 + + m_e_ss = 0.35 + + alpha = 0.25 + + eps_d = -1.46025 + + eps_bh = 2.932806 + + eps_be = 2.932806 + + eps_y_ss = 6.0 + + eps_l_ss = 5.0 + + gamma_p = 1.0 + + gamma_i = 1.0 + + ni = 0.8 + + gamma_b = 1.0 + + gamma_e = 1.0 + + deltak = 0.025 + + piss = 1.0 + + h = 1.0 + + vi = 0.09 + + ind_d = 0.0 + + ind_be = 0.0 + + ind_bh = 0.0 + + rho_ee_z = 0.3935275324257052 + + rho_A_e = 0.9390001567894549 + + rho_ee_j = 0.9211790941478728 + + rho_me = 0.8938651443507468 + + rho_mi = 0.9286486478061776 + + rho_mk_d = 0.8380479641501677 + + rho_mk_bh = 0.8194621730335763 + + rho_mk_be = 0.8342810056222126 + + rho_ee_qk = 0.5474914620444137 + + rho_eps_y = 0.3047340963457367 + + rho_eps_l = 0.639922254764848 + + rho_eps_K_b = 0.8129795852441276 + + kappa_p = 28.65019653869527 + + kappa_w = 99.89828358530188 + + kappa_i = 10.182155670839322 + + kappa_d = 3.5029734165847466 + + kappa_be = 9.36382331915174 + + kappa_bh = 10.086654447226444 + + kappa_kb = 11.068335540791962 + + phi_pie = 1.9816026561910398 + + rho_ib = 0.7685551455946952 + + phi_y = 0.3459149657035201 + + ind_p = 0.16051347848216171 + + ind_w = 0.27569624058316433 + + a_i = 0.8559521971842566 + + sigma__z = 0.0144 + + sigma__A_e = 0.0062 + + sigma__j = 0.0658 + + sigma__me = 0.0034 + + sigma__mi = 0.0023 + + sigma__mk_d = 0.0488 + + sigma__mk_bh = 0.0051 + + sigma__mk_be = 0.1454 + + sigma__qk = 0.0128 + + sigma__r_ib = 0.0018 + + sigma__y = 1.0099 + + sigma__l = 0.3721 + + sigma__eps_K_b = 0.05 + + beta_b = beta_p + + beta_e = beta_i + + mk_d_ss = eps_d/(eps_d-1) + + mk_bh_ss = eps_bh/(eps_bh-1) + + mk_be_ss = eps_be/(eps_be-1) + + r_ib_ss = (eps_d-1)*(piss/beta_p-1)/eps_d + + r_be_ss = (eps_be * r_ib_ss) / (eps_be - 1) + + r_bh_ss = (eps_bh * r_ib_ss) / (eps_bh - 1) + + r_k_ss = (-((1 - deltak)) - ((piss * (1 - deltak) * m_e_ss) / beta_e) * (1 / (1 + r_be_ss) - beta_e / piss)) + 1 / beta_e + + eksi_1 = r_k_ss + + eksi_2 = r_k_ss*0.1 + + eps_b = (eps_bh + eps_be) / 2 + + delta_kb = r_ib_ss/vi*(eps_d-eps_b+eps_d*vi*(eps_b-1))/((eps_d-1)*(eps_b-1)) + +end + diff --git a/test/GNSS_2010.mod b/test/GNSS_2010.mod new file mode 100644 index 000000000..81afa1014 --- /dev/null +++ b/test/GNSS_2010.mod @@ -0,0 +1,307 @@ +var +A_e B BE BH C D I J_B J_R K K_b PIW R_b Y Y1 b_e b_ee b_h b_i bm c_e c_i c_p d_b d_p ee_j ee_qk ee_z eps_K_b eps_l eps_y h_i h_p j_B k_e l_i l_id l_p l_pd lam_e lam_i lam_p m_e m_i mk_be mk_bh mk_d pie pie_wi pie_wp q_h q_k r_be r_bh r_d r_ib r_k rr_e s_e s_i spr_b u w_i w_p x y_e ; + +varexo +e_A_e e_eps_K_b e_j e_l e_me e_mi e_mk_be e_mk_bh e_mk_d e_qk e_r_ib e_y e_z ; + +parameters +a_i alpha beta_e beta_i beta_p delta_kb deltak eksi_1 eksi_2 eps_l_ss eps_y_ss gamma_b gamma_e gamma_i gamma_p h ind_be ind_bh ind_d ind_p ind_w j kappa_be kappa_bh kappa_d kappa_i kappa_kb kappa_p kappa_w m_e_ss m_i_ss mk_be_ss mk_bh_ss mk_d_ss ni phi phi_pie phi_y piss r_ib_ss rho_A_e rho_ee_j rho_ee_qk rho_ee_z rho_eps_K_b rho_eps_l rho_eps_y rho_ib rho_me rho_mi rho_mk_be rho_mk_bh rho_mk_d vi sigma__A_e sigma__eps_K_b sigma__j sigma__l sigma__me sigma__mi sigma__mk_be sigma__mk_bh sigma__mk_d sigma__qk sigma__r_ib sigma__y sigma__z ; + +% Parameter definitions: + beta_p = 0.9943; + beta_i = 0.975; + j = 0.2; + phi = 1.0; + m_i_ss = 0.7; + m_e_ss = 0.35; + alpha = 0.25; + eps_d = -1.46025; + eps_bh = 2.932806; + eps_be = 2.932806; + eps_y_ss = 6.0; + eps_l_ss = 5.0; + gamma_p = 1.0; + gamma_i = 1.0; + ni = 0.8; + gamma_b = 1.0; + gamma_e = 1.0; + deltak = 0.025; + piss = 1.0; + h = 1.0; + vi = 0.09; + ind_d = 0.0; + ind_be = 0.0; + ind_bh = 0.0; + rho_ee_z = 0.3935275324257052; + rho_A_e = 0.9390001567894549; + rho_ee_j = 0.9211790941478728; + rho_me = 0.8938651443507468; + rho_mi = 0.9286486478061776; + rho_mk_d = 0.8380479641501677; + rho_mk_bh = 0.8194621730335763; + rho_mk_be = 0.8342810056222126; + rho_ee_qk = 0.5474914620444137; + rho_eps_y = 0.3047340963457367; + rho_eps_l = 0.639922254764848; + rho_eps_K_b = 0.8129795852441276; + kappa_p = 28.65019653869527; + kappa_w = 99.89828358530188; + kappa_i = 10.182155670839322; + kappa_d = 3.5029734165847466; + kappa_be = 9.36382331915174; + kappa_bh = 10.086654447226444; + kappa_kb = 11.068335540791962; + phi_pie = 1.9816026561910398; + rho_ib = 0.7685551455946952; + phi_y = 0.3459149657035201; + ind_p = 0.16051347848216171; + ind_w = 0.27569624058316433; + a_i = 0.8559521971842566; + sigma__z = 0.0144; + sigma__A_e = 0.0062; + sigma__j = 0.0658; + sigma__me = 0.0034; + sigma__mi = 0.0023; + sigma__mk_d = 0.0488; + sigma__mk_bh = 0.0051; + sigma__mk_be = 0.1454; + sigma__qk = 0.0128; + sigma__r_ib = 0.0018; + sigma__y = 1.0099; + sigma__l = 0.3721; + sigma__eps_K_b = 0.05; + beta_b = beta_p; + beta_e = beta_i; + mk_d_ss = eps_d / (eps_d - 1); + mk_bh_ss = eps_bh / (eps_bh - 1); + mk_be_ss = eps_be / (eps_be - 1); + r_ib_ss = ((eps_d - 1) * (piss / beta_p - 1)) / eps_d; + r_be_ss = (eps_be * r_ib_ss) / (eps_be - 1); + r_bh_ss = (eps_bh * r_ib_ss) / (eps_bh - 1); + r_k_ss = (-((1 - deltak)) - ((piss * (1 - deltak) * m_e_ss) / beta_e) * (1 / (1 + r_be_ss) - beta_e / piss)) + 1 / beta_e; + eksi_1 = r_k_ss; + eksi_2 = r_k_ss * 0.1; + eps_b = (eps_bh + eps_be) / 2; + delta_kb = ((r_ib_ss / vi) * ((eps_d - eps_b) + eps_d * vi * (eps_b - 1))) / ((eps_d - 1) * (eps_b - 1)); + +model; + (1 - a_i) * exp(ee_z(0)) * (c_p(0) - a_i * c_p(-1)) ^ -1 = lam_p(0); + + ((j * exp(ee_j(0))) / h_p(0) - lam_p(0) * q_h(0)) + beta_p * lam_p(1) * q_h(1) = 0; + + lam_p(0) = (beta_p * lam_p(1) * (1 + r_d(0))) / pie(1); + + (((1 - exp(eps_l(0))) * l_p(0) + ((exp(eps_l(0)) * l_p(0) ^ (1 + phi)) / w_p(0)) / lam_p(0)) - pie_wp(0) * kappa_w * (pie_wp(0) - pie(-1) ^ ind_w * piss ^ (1 - ind_w))) + (((kappa_w * beta_p * lam_p(1)) / lam_p(0)) * (pie_wp(1) - piss ^ (1 - ind_w) * pie(0) ^ ind_w) * pie_wp(1) ^ 2) / pie(1) = 0; + + pie_wp(0) = (pie(0) * w_p(0)) / w_p(-1); + + c_p(0) + q_h(0) * (h_p(0) - h_p(-1)) + d_p(0) = l_p(0) * w_p(0) + ((1 + r_d(-1)) * d_p(-1)) / pie(0) + J_R(0) / gamma_p; + + (1 - a_i) * exp(ee_z(0)) * (c_i(0) - a_i * c_i(-1)) ^ -1 = lam_i(0); + + ((j * exp(ee_j(0))) / h_i(0) - q_h(0) * lam_i(0)) + q_h(1) * beta_i * lam_i(1) + pie(1) * q_h(1) * s_i(0) * exp(m_i(0)) = 0; + + lam_i(0) - (beta_i * lam_i(1) * (1 + r_bh(0))) / pie(1) = s_i(0) * (1 + r_bh(0)); + + (((1 - exp(eps_l(0))) * l_i(0) + ((exp(eps_l(0)) * l_i(0) ^ (1 + phi)) / w_i(0)) / lam_i(0)) - pie_wi(0) * kappa_w * (pie_wi(0) - pie(-1) ^ ind_w * piss ^ (1 - ind_w))) + (((kappa_w * beta_i * lam_i(1)) / lam_i(0)) * (pie_wi(1) - piss ^ (1 - ind_w) * pie(0) ^ ind_w) * pie_wi(1) ^ 2) / pie(1) = 0; + + pie_wi(0) = (pie(0) * w_i(0)) / w_i(-1); + + c_i(0) + q_h(0) * (h_i(0) - h_i(-1)) + ((1 + r_bh(-1)) * b_i(-1)) / pie(0) = l_i(0) * w_i(0) + b_i(0); + + (1 + r_bh(0)) * b_i(0) = pie(1) * h_i(0) * q_h(1) * exp(m_i(0)); + + K(0) = (1 - deltak) * K(-1) + I(0) * (1 - (kappa_i / 2) * ((I(0) * exp(ee_qk(0))) / I(-1) - 1) ^ 2); + + 1 = q_k(0) * ((1 - (kappa_i / 2) * ((I(0) * exp(ee_qk(0))) / I(-1) - 1) ^ 2) - (exp(ee_qk(0)) * I(0) * kappa_i * ((I(0) * exp(ee_qk(0))) / I(-1) - 1)) / I(-1)) + ((exp(ee_qk(1)) * kappa_i * beta_e * lam_e(1)) / lam_e(0)) * q_k(1) * ((I(1) * exp(ee_qk(1))) / I(0) - 1) * (I(1) / I(0)) ^ 2; + + (1 - a_i) * (c_e(0) - a_i * c_e(-1)) ^ -1 = lam_e(0); + + (1 - deltak) * pie(1) * q_k(1) * s_e(0) * exp(m_e(0)) + beta_e * lam_e(1) * (((1 - deltak) * q_k(1) + r_k(1) * u(1)) - (eksi_1 * (u(1) - 1) + (eksi_2 / 2) * (u(1) - 1) ^ 2)) = q_k(0) * lam_e(0); + + w_p(0) = (ni * (1 - alpha) * y_e(0)) / (l_pd(0) * x(0)); + + w_i(0) = (y_e(0) * (1 - alpha) * (1 - ni)) / (x(0) * l_id(0)); + + lam_e(0) - s_e(0) * (1 + r_be(0)) = (beta_e * lam_e(1) * (1 + r_be(0))) / pie(1); + + r_k(0) = eksi_1 + eksi_2 * (u(0) - 1); + + c_e(0) + ((1 + r_be(-1)) * b_ee(-1)) / pie(0) + w_p(0) * l_pd(0) + w_i(0) * l_id(0) + q_k(0) * k_e(0) + (eksi_1 * (u(0) - 1) + (eksi_2 / 2) * (u(0) - 1) ^ 2) * k_e(-1) = y_e(0) / x(0) + b_ee(0) + k_e(-1) * (1 - deltak) * q_k(0); + + y_e(0) = exp(A_e(0)) * (u(0) * k_e(-1)) ^ alpha * (l_pd(0) ^ ni * l_id(0) ^ (1 - ni)) ^ (1 - alpha); + + (1 + r_be(0)) * b_ee(0) = (1 - deltak) * k_e(0) * pie(1) * q_k(1) * exp(m_e(0)); + + r_k(0) = ((l_pd(0) ^ ni * l_id(0) ^ (1 - ni)) ^ (1 - alpha) * alpha * exp(A_e(0)) * u(0) ^ (alpha - 1) * k_e(-1) ^ (alpha - 1)) / x(0); + + R_b(0) = r_ib(0) + -kappa_kb * (K_b(0) / B(0) - vi) * (K_b(0) / B(0)) ^ 2; + + pie(0) * K_b(0) = ((1 - delta_kb) * K_b(-1)) / exp(eps_K_b(0)) + j_B(-1); + + gamma_b * d_b(0) = d_p(0) * gamma_p; + + gamma_b * b_h(0) = b_i(0) * gamma_i; + + gamma_b * b_e(0) = b_ee(0) * gamma_e; + + b_h(0) + b_e(0) = K_b(0) + d_b(0); + + ((((((kappa_d * beta_p * lam_p(1)) / lam_p(0)) * (r_d(1) / r_d(0) - (r_d(0) / r_d(-1)) ^ ind_d) * (r_d(1) / r_d(0)) ^ 2 * d_b(1)) / d_b(0) + exp(mk_d(0)) / (exp(mk_d(0)) - 1)) - 1) - ((r_ib(0) * exp(mk_d(0))) / (exp(mk_d(0)) - 1)) / r_d(0)) - (r_d(0) * kappa_d * (r_d(0) / r_d(-1) - (r_d(-1) / r_d(-2)) ^ ind_d)) / r_d(-1) = 0; + + ((((((beta_p * lam_p(1)) / lam_p(0)) * kappa_be * (r_be(1) / r_be(0) - (r_be(0) / r_be(-1)) ^ ind_be) * (r_be(1) / r_be(0)) ^ 2 * b_e(1)) / b_e(0) + 1) - exp(mk_be(0)) / (exp(mk_be(0)) - 1)) + ((R_b(0) * exp(mk_be(0))) / (exp(mk_be(0)) - 1)) / r_be(0)) - (r_be(0) * kappa_be * (r_be(0) / r_be(-1) - (r_be(-1) / r_be(-2)) ^ ind_be)) / r_be(-1) = 0; + + ((((((beta_p * lam_p(1)) / lam_p(0)) * kappa_bh * (r_bh(1) / r_bh(0) - (r_bh(0) / r_bh(-1)) ^ ind_bh) * (r_bh(1) / r_bh(0)) ^ 2 * b_h(1)) / b_h(0) + 1) - exp(mk_bh(0)) / (exp(mk_bh(0)) - 1)) + ((R_b(0) * exp(mk_bh(0))) / (exp(mk_bh(0)) - 1)) / r_bh(0)) - (r_bh(0) * kappa_bh * (r_bh(0) / r_bh(-1) - (r_bh(-1) / r_bh(-2)) ^ ind_bh)) / r_bh(-1) = 0; + + j_B(0) = (((((r_bh(0) * b_h(0) + r_be(0) * b_e(0)) - r_d(0) * d_b(0)) - ((d_b(0) * r_d(0) * kappa_d) / 2) * (r_d(0) / r_d(-1) - 1) ^ 2) - ((b_e(0) * r_be(0) * kappa_be) / 2) * (r_be(0) / r_be(-1) - 1) ^ 2) - ((b_h(0) * r_bh(0) * kappa_bh) / 2) * (r_bh(0) / r_bh(-1) - 1) ^ 2) - ((K_b(0) * kappa_kb) / 2) * (K_b(0) / B(0) - vi) ^ 2; + + J_R(0) = Y(0) * ((1 - 1 / x(0)) - (kappa_p / 2) * (pie(0) - pie(-1) ^ ind_p * piss ^ (1 - ind_p)) ^ 2); + + (((1 - exp(eps_y(0))) + exp(eps_y(0)) / x(0)) - pie(0) * kappa_p * (pie(0) - pie(-1) ^ ind_p * piss ^ (1 - ind_p))) + (((pie(1) * beta_p * lam_p(1)) / lam_p(0)) * kappa_p * (pie(1) - piss ^ (1 - ind_p) * pie(0) ^ ind_p) * Y(1)) / Y(0) = 0; + + C(0) = c_p(0) * gamma_p + c_i(0) * gamma_i + c_e(0) * gamma_e; + + BH(0) = gamma_b * b_h(0); + + BE(0) = gamma_b * b_e(0); + + B(0) = BH(0) + BE(0); + + D(0) = d_p(0) * gamma_p; + + Y(0) = y_e(0) * gamma_e; + + J_B(0) = gamma_b * j_B(0); + + l_pd(0) * gamma_e = l_p(0) * gamma_p; + + l_id(0) * gamma_e = l_i(0) * gamma_i; + + h = h_p(0) * gamma_p + h_i(0) * gamma_i; + + K(0) = k_e(0) * gamma_e; + + Y1(0) = (C(0) + K(0)) - (1 - deltak) * K(-1); + + PIW(0) = (pie(0) * (w_p(0) + w_i(0))) / (w_p(-1) + w_i(-1)); + + 1 + r_ib(0) = (1 + r_ib_ss) ^ (1 - rho_ib) * (1 + r_ib(-1)) ^ rho_ib * ((pie(0) / piss) ^ phi_pie * (Y1(0) / Y1(-1)) ^ phi_y) ^ (1 - rho_ib) * (1 + sigma__r_ib * e_r_ib); + + exp(ee_z(0)) = (1 - rho_ee_z) + rho_ee_z * exp(ee_z(-1)) + sigma__z * e_z; + + exp(A_e(0)) = (1 - rho_A_e) + rho_A_e * exp(A_e(-1)) + sigma__A_e * e_A_e; + + exp(ee_j(0)) = ((1 - rho_ee_j) + rho_ee_j * exp(ee_j(-1))) - sigma__j * e_j; + + exp(m_i(0)) = (1 - rho_mi) * m_i_ss + rho_mi * exp(m_i(-1)) + sigma__mi * e_mi; + + exp(m_e(0)) = (1 - rho_me) * m_e_ss + rho_me * exp(m_e(-1)) + sigma__me * e_me; + + exp(mk_d(0)) = (1 - rho_mk_d) * mk_d_ss + rho_mk_d * exp(mk_d(-1)) + sigma__mk_d * e_mk_d; + + exp(mk_be(0)) = (1 - rho_mk_be) * mk_be_ss + rho_mk_be * exp(mk_be(-1)) + sigma__mk_be * e_mk_be; + + exp(mk_bh(0)) = (1 - rho_mk_bh) * mk_bh_ss + rho_mk_bh * exp(mk_bh(-1)) + sigma__mk_bh * e_mk_bh; + + exp(ee_qk(0)) = (1 - rho_ee_qk) + rho_ee_qk * exp(ee_qk(-1)) + sigma__qk * e_qk; + + exp(eps_y(0)) = (1 - rho_eps_y) * eps_y_ss + rho_eps_y * exp(eps_y(-1)) + sigma__y * e_y; + + exp(eps_l(0)) = (1 - rho_eps_l) * eps_l_ss + rho_eps_l * exp(eps_l(-1)) + sigma__l * e_l; + + exp(eps_K_b(0)) = (1 - rho_eps_K_b) + rho_eps_K_b * exp(eps_K_b(-1)) + sigma__eps_K_b * e_eps_K_b; + + rr_e(0) = lam_e(0) - (beta_e * lam_e(1) * (1 + r_be(0))) / pie(1); + + bm(0) = ((r_bh(-1) * b_h(-1)) / (b_h(-1) + b_e(-1)) + (r_be(-1) * b_e(-1)) / (b_h(-1) + b_e(-1))) - r_d(-1); + + spr_b(0) = (r_bh(0) * 0.5 + r_be(0) * 0.5) - r_d(0); + +end; + +shocks; +var e_A_e = 1; +var e_eps_K_b = 1; +var e_j = 1; +var e_l = 1; +var e_me = 1; +var e_mi = 1; +var e_mk_be = 1; +var e_mk_bh = 1; +var e_mk_d = 1; +var e_qk = 1; +var e_r_ib = 1; +var e_y = 1; +var e_z = 1; +end; + +initval; + A_e = 0.0; + B = 3.117036687497955; + BE = 1.962121473934388; + BH = 1.1549152135635667; + C = 1.1393929668291012; + D = 2.836503385623122; + I = 0.14585183894796955; + J_B = 0.029421382957178336; + J_R = 0.21911103145570737; + K = 5.8340735579187815; + K_b = 0.28053330187483305; + PIW = 1.000000000000001; + R_b = 0.009658494610859531; + Y = 1.3146661887342532; + Y1 = 1.2852448057770707; + b_e = 1.962121473934388; + b_ee = 1.962121473934388; + b_h = 1.1549152135635667; + b_i = 1.1549152135635667; + bm = 0.008922954442736093; + c_e = 0.09928082266626682; + c_i = 0.14740726273499982; + c_p = 0.8927048814278347; + d_b = 2.836503385623122; + d_p = 2.836503385623122; + ee_j = 0.0; + ee_qk = 0.0; + ee_z = 0.0; + eps_K_b = 0.0; + eps_l = 1.6094379124341003; + eps_y = 1.791759469228055; + h_i = 0.05073361230901422; + h_p = 0.9492663876909857; + j_B = 0.029421382957178336; + k_e = 5.8340735579187815; + l_i = 0.9443833160158907; + l_id = 0.9443833160158907; + l_p = 0.7675095494290946; + l_pd = 0.7675095494290946; + lam_e = 10.072438696056208; + lam_i = 6.783926256047107; + lam_p = 1.1201910293136885; + m_e = -1.0498221244986778; + m_i = -0.35667494393873245; + mk_be = 0.41698681086954853; + mk_bh = 0.41698681086954853; + mk_d = -0.5216553168586762; + pie = 1.000000000000001; + pie_wi = 1.000000000000001; + pie_wp = 1.000000000000001; + q_h = 32.99703718774283; + q_k = 1.0; + r_be = 0.014655630697388407; + r_bh = 0.014655630697388405; + r_d = 0.005732676254652312; + r_ib = 0.009658494610860022; + r_k = 0.046946406588904616; + rr_e = 0.10788347419371955; + s_e = 0.10632521116506125; + s_i = 0.07161149483936115; + spr_b = 0.008922954442736095; + u = 1.0000000000000149; + w_i = 0.17401119948313096; + w_p = 0.8564494016472878; + x = 1.1999999999999984; + y_e = 1.3146661887342532; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Gali_2015_chapter_3_nonlinear.jl b/test/Gali_2015_chapter_3_nonlinear.jl new file mode 100644 index 000000000..61cd046dd --- /dev/null +++ b/test/Gali_2015_chapter_3_nonlinear.jl @@ -0,0 +1,87 @@ +using MacroModelling + +@model Gali_2015_chapter_3_nonlinear begin + W_real[0] = C[0] ^ sigma * N[0] ^ varphi + + Q[0] = beta * (C[1] / C[0]) ^ (-sigma) * Z[1] / Z[0] / Pi[1] + + R[0] = 1 / Q[0] + + Y[0] = A[0] * (N[0] / S[0]) ^ (1 - alpha) + + R[0] = Pi[1] * realinterest[0] + + R[0] = 1 / beta * Pi[0] ^ phi__p__i * (Y[0] / Y[ss]) ^ phi__y * exp(nu[0]) + + C[0] = Y[0] + + log(A[0]) = rho__a * log(A[-1]) + std_a * eps_a[x] + + log(Z[0]) = rho__z * log(Z[-1]) - std_z * eps_z[x] + + nu[0] = rho__nu * nu[-1] + std_nu * eps_nu[x] + + MC[0] = W_real[0] / ((1 - alpha) * Y[0] * S[0] / N[0]) + + 1 = theta * Pi[0] ^ (epsilon - 1) + (1 - theta) * Pi_star[0] ^ (1 - epsilon) + + S[0] = (1 - theta) * Pi_star[0] ^ (( - epsilon) / (1 - alpha)) + theta * Pi[0] ^ (epsilon / (1 - alpha)) * S[-1] + + Pi_star[0] ^ (1 + alpha * epsilon / (1 - alpha)) = epsilon * x_aux_1[0] / x_aux_2[0] * (1 - tau) / (epsilon - 1) + + x_aux_1[0] = Z[0] * Y[0] * MC[0] * C[0] ^ (-sigma) + beta * theta * Pi[1] ^ (epsilon + alpha * epsilon / (1 - alpha)) * x_aux_1[1] + + x_aux_2[0] = C[0] ^ (-sigma) * Z[0] * Y[0] + beta * theta * Pi[1] ^ (epsilon - 1) * x_aux_2[1] + + log_y[0] = log(Y[0]) + + log_W_real[0] = log(W_real[0]) + + log_N[0] = log(N[0]) + + pi_ann[0] = 4 * log(Pi[0]) + + i_ann[0] = 4 * log(R[0]) + + r_real_ann[0] = 4 * log(realinterest[0]) + + M_real[0] = Y[0] / R[0] ^ eta + +end + + +@parameters Gali_2015_chapter_3_nonlinear begin + sigma = 1.0 + + varphi = 5.0 + + phi__p__i = 1.5 + + phi__y = 0.125 + + theta = 0.75 + + rho__nu = 0.5 + + rho__z = 0.5 + + rho__a = 0.9 + + beta = 0.99 + + eta = 3.77 + + alpha = 0.25 + + epsilon = 9.0 + + tau = 0.0 + + std_a = 0.01 + + std_z = 0.05 + + std_nu = 0.0025 + +end + diff --git a/test/Gali_2015_chapter_3_nonlinear.mod b/test/Gali_2015_chapter_3_nonlinear.mod new file mode 100644 index 000000000..7f150e6de --- /dev/null +++ b/test/Gali_2015_chapter_3_nonlinear.mod @@ -0,0 +1,109 @@ +var +A C MC M_real N Pi Pi_star Q R S W_real Y Z i_ann log_N log_W_real log_y nu pi_ann r_real_ann realinterest x_aux_1 x_aux_2 ; + +varexo +eps_a eps_nu eps_z ; + +parameters +std_a std_nu std_z alpha beta eta theta rho__a rho__z rho__nu sigma tau varphi phi__y phi__p__i epsilon ; + +% Parameter definitions: + sigma = 1.0; + varphi = 5.0; + phi__p__i = 1.5; + phi__y = 0.125; + theta = 0.75; + rho__nu = 0.5; + rho__z = 0.5; + rho__a = 0.9; + beta = 0.99; + eta = 3.77; + alpha = 0.25; + epsilon = 9.0; + tau = 0.0; + std_a = 0.01; + std_z = 0.05; + std_nu = 0.0025; + +model; + W_real(0) = C(0) ^ sigma * N(0) ^ varphi; + + Q(0) = ((beta * (C(1) / C(0)) ^ -sigma * Z(1)) / Z(0)) / Pi(1); + + R(0) = 1 / Q(0); + + Y(0) = A(0) * (N(0) / S(0)) ^ (1 - alpha); + + R(0) = Pi(1) * realinterest(0); + + R(0) = (1 / beta) * Pi(0) ^ phi__p__i * (Y(0) / STEADY_STATE(Y)) ^ phi__y * exp(nu(0)); + + C(0) = Y(0); + + log(A(0)) = rho__a * log(A(-1)) + std_a * eps_a; + + log(Z(0)) = rho__z * log(Z(-1)) - std_z * eps_z; + + nu(0) = rho__nu * nu(-1) + std_nu * eps_nu; + + MC(0) = W_real(0) / ((S(0) * Y(0) * (1 - alpha)) / N(0)); + + 1 = theta * Pi(0) ^ (epsilon - 1) + (1 - theta) * Pi_star(0) ^ (1 - epsilon); + + S(0) = (1 - theta) * Pi_star(0) ^ (-epsilon / (1 - alpha)) + theta * Pi(0) ^ (epsilon / (1 - alpha)) * S(-1); + + Pi_star(0) ^ (1 + (epsilon * alpha) / (1 - alpha)) = (((epsilon * x_aux_1(0)) / x_aux_2(0)) * (1 - tau)) / (epsilon - 1); + + x_aux_1(0) = MC(0) * Y(0) * Z(0) * C(0) ^ -sigma + beta * theta * Pi(1) ^ (epsilon + (alpha * epsilon) / (1 - alpha)) * x_aux_1(1); + + x_aux_2(0) = Y(0) * Z(0) * C(0) ^ -sigma + beta * theta * Pi(1) ^ (epsilon - 1) * x_aux_2(1); + + log_y(0) = log(Y(0)); + + log_W_real(0) = log(W_real(0)); + + log_N(0) = log(N(0)); + + pi_ann(0) = 4 * log(Pi(0)); + + i_ann(0) = 4 * log(R(0)); + + r_real_ann(0) = 4 * log(realinterest(0)); + + M_real(0) = Y(0) / R(0) ^ eta; + +end; + +shocks; +var eps_a = 1; +var eps_nu = 1; +var eps_z = 1; +end; + +initval; + A = 1.0; + C = 0.9505798249541406; + MC = 0.8888888888888884; + M_real = 0.915236383286892; + N = 0.934655265184067; + Pi = 0.9999999999999996; + Pi_star = 0.9999999999999987; + Q = 0.9900000000000004; + R = 1.0101010101010095; + S = 1.0; + W_real = 0.6780252644037242; + Y = 0.9505798249541406; + Z = 1.0; + i_ann = 0.04020134341400339; + log_N = -0.06757751801802749; + log_W_real = -0.3885707286036581; + log_y = -0.050683138513520666; + nu = 0.0; + pi_ann = -1.776356839400251e-15; + r_real_ann = 0.04020134341400514; + realinterest = 1.01010101010101; + x_aux_1 = 3.4519956850053397; + x_aux_2 = 3.8834951456310276; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Gali_Monacelli_2005_CITR.jl b/test/Gali_Monacelli_2005_CITR.jl new file mode 100644 index 000000000..2ab81c849 --- /dev/null +++ b/test/Gali_Monacelli_2005_CITR.jl @@ -0,0 +1,75 @@ +using MacroModelling + +@model Gali_Monacelli_2005_CITR begin + x[0] = x[1] - sigma__a ^ (-1) * (r[0] - pih[1] - rnat[0]) + + pih[0] = pih[1] * beta + x[0] * kappa__a + + rnat[0] = ( - sigma__a) * Gamma * (1 - rho__a) * a[0] + sigma__a * alpha * (Theta + Psi) * (ystar[1] - ystar[0]) + + ynat[0] = Gamma * a[0] + Psi * alpha * ystar[0] + + x[0] = y[0] - ynat[0] + + y[0] = ystar[0] + sigma__a ^ (-1) * s[0] + + pi[0] = pih[0] + alpha * (s[0] - s[-1]) + + s[0] = s[-1] + deprec_rate[0] - pih[0] + + y[0] = a[0] + n[0] + + nx[0] = alpha * s[0] * (omega / sigma - 1) + + y[0] = c[0] + alpha * s[0] * omega / sigma + + real_wage[0] = sigma * c[0] + n[0] * phi + + a[0] = rho__a * a[-1] + epsilon__a[x] + + ystar[0] = rho__y * ystar[-1] + epsilon__star[x] + + r[0] = pi[0] * phi__p__i + +end + + +@parameters Gali_Monacelli_2005_CITR begin + sigma = 1.0 + + eta = 1.0 + + gamma = 1.0 + + phi = 3.0 + + theta = 0.75 + + beta = 0.99 + + alpha = 0.4 + + phi__p__i = 1.5 + + rho__a = 0.9 + + rho__y = 0.86 + + rho = 1 / beta - 1 + + omega = sigma*gamma+(1-alpha)*(sigma*eta-1) + + sigma__a = sigma/(1-alpha+alpha*omega) + + Theta = sigma*gamma+(1-alpha)*(sigma*eta-1)-1 + + lambda = ((1 - beta * theta) * (1 - theta)) / theta + + kappa__a = lambda*(sigma__a+phi) + + Gamma = (1+phi)/(sigma__a+phi) + + Psi = (-sigma__a)*Theta/(sigma__a+phi) + +end + diff --git a/test/Gali_Monacelli_2005_CITR.mod b/test/Gali_Monacelli_2005_CITR.mod new file mode 100644 index 000000000..22befd1e2 --- /dev/null +++ b/test/Gali_Monacelli_2005_CITR.mod @@ -0,0 +1,86 @@ +var +a c deprec_rate n nx pi pih r real_wage rnat s x y ynat ystar ; + +varexo +epsilon__a epsilon__star ; + +parameters +Gamma Theta Psi alpha beta kappa__a rho__y rho__a sigma sigma__a omega phi phi__p__i ; + +% Parameter definitions: + sigma = 1.0; + eta = 1.0; + gamma = 1.0; + phi = 3.0; + theta = 0.75; + beta = 0.99; + alpha = 0.4; + phi__p__i = 1.5; + rho__a = 0.9; + rho__y = 0.86; + rho = 1 / beta - 1; + omega = sigma * gamma + (1 - alpha) * (sigma * eta - 1); + sigma__a = sigma / ((1 - alpha) + alpha * omega); + Theta = ((1 - alpha) * (sigma * eta - 1) + sigma * gamma) - 1; + lambda = ((1 - beta * theta) * (1 - theta)) / theta; + kappa__a = lambda * (sigma__a + phi); + Gamma = (1 + phi) / (sigma__a + phi); + Psi = (-sigma__a * Theta) / (sigma__a + phi); + +model; + x(0) = x(1) - sigma__a ^ -1 * ((r(0) - pih(1)) - rnat(0)); + + pih(0) = pih(1) * beta + x(0) * kappa__a; + + rnat(0) = -sigma__a * Gamma * (1 - rho__a) * a(0) + sigma__a * alpha * (Theta + Psi) * (ystar(1) - ystar(0)); + + ynat(0) = Gamma * a(0) + ystar(0) * alpha * Psi; + + x(0) = y(0) - ynat(0); + + y(0) = ystar(0) + sigma__a ^ -1 * s(0); + + pi(0) = pih(0) + alpha * (s(0) - s(-1)); + + s(0) = (s(-1) + deprec_rate(0)) - pih(0); + + y(0) = a(0) + n(0); + + nx(0) = s(0) * alpha * (omega / sigma - 1); + + y(0) = c(0) + (s(0) * alpha * omega) / sigma; + + real_wage(0) = sigma * c(0) + n(0) * phi; + + a(0) = rho__a * a(-1) + epsilon__a; + + ystar(0) = rho__y * ystar(-1) + epsilon__star; + + r(0) = pi(0) * phi__p__i; + +end; + +shocks; +var epsilon__a = 1; +var epsilon__star = 1; +end; + +initval; + a = 0.0; + c = 0.0; + deprec_rate = 0.0; + n = 0.0; + nx = 0.0; + pi = 0.0; + pih = 0.0; + r = 0.0; + real_wage = 0.0; + rnat = 0.0; + s = 0.0; + x = 0.0; + y = 0.0; + ynat = 0.0; + ystar = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Ghironi_Melitz_2005.jl b/test/Ghironi_Melitz_2005.jl new file mode 100644 index 000000000..4bc0167a1 --- /dev/null +++ b/test/Ghironi_Melitz_2005.jl @@ -0,0 +1,117 @@ +using MacroModelling + +@model Ghironi_Melitz_2005 begin + 1 = Nd[0] * rho_tilde_d[0] ^ (1 - theta) + Nxbar[0] * rho_tilde_xbar[0] ^ (1 - theta) + + 1 = Ndbar[0] * rho_tilde_dbar[0] ^ (1 - theta) + Nx[0] * rho_tilde_x[0] ^ (1 - theta) + + rho_tilde_d[0] = theta / (theta - 1) * w[0] / (Z[0] * ztilde_d) + + rho_tilde_dbar[0] = theta / (theta - 1) * wbar[0] / (Zbar[0] * ztilde_dbar) + + rho_tilde_x[0] = w[0] * theta / (theta - 1) * tau / (Z[0] * ztilde_x[0]) / Q[0] + + rho_tilde_xbar[0] = wbar[0] * tau * theta * Q[0] / (theta - 1) / (Zbar[0] * ztilde_xbar[0]) + + dtilde[0] = dtilde_d[0] + Nx[0] / Nd[0] * dtilde_x[0] + + dtilde_bar[0] = dtilde_dbar[0] + Nxbar[0] / Ndbar[0] * dtilde_xbar[0] + + dtilde_d[0] = rho_tilde_d[0] ^ (1 - theta) / theta * C[0] + + dtilde_dbar[0] = rho_tilde_dbar[0] ^ (1 - theta) / theta * Cbar[0] + + vtilde[0] = w[0] * fe / Z[0] + + vtilde_bar[0] = wbar[0] * febar / Zbar[0] + + dtilde_x[0] = (theta - 1) * w[0] * fx / Z[0] / (k - (theta - 1)) + + dtilde_xbar[0] = wbar[0] * (theta - 1) / (k - (theta - 1)) * fxbar / Zbar[0] + + Nx[0] / Nd[0] = (zmin / ztilde_x[0]) ^ k * (k / (k - (theta - 1))) ^ (k / (theta - 1)) + + Nxbar[0] / Ndbar[0] = (k / (k - (theta - 1))) ^ (k / (theta - 1)) * (zminbar / ztilde_xbar[0]) ^ k + + Nd[0] = (1 - delta) * (Nd[-1] + Ne[-1]) + + Ndbar[0] = (1 - delta) * (Ndbar[-1] + Nebar[-1]) + + C[0] ^ (-gamma) = beta * (1 + r[0]) * C[1] ^ (-gamma) + + Cbar[0] ^ (-gamma) = beta * (1 + rbar[0]) * Cbar[1] ^ (-gamma) + + vtilde[0] = (1 - delta) * beta * (C[1] / C[0]) ^ (-gamma) * (vtilde[1] + dtilde[1]) + + vtilde_bar[0] = (1 - delta) * beta * (Cbar[1] / Cbar[0]) ^ (-gamma) * (vtilde_bar[1] + dtilde_bar[1]) + + C[0] = w[0] * L + Nd[0] * dtilde[0] - vtilde[0] * Ne[0] + + Cbar[0] = wbar[0] * Lbar + Ndbar[0] * dtilde_bar[0] - vtilde_bar[0] * Nebar[0] + + Q[0] = Nxbar[0] * rho_tilde_xbar[0] ^ (1 - theta) * C[0] / (Nx[0] * rho_tilde_x[0] ^ (1 - theta) * Cbar[0]) + + Qtilde[0] = ((Ndbar[0] / (Ndbar[0] + Nx[0]) * TOL[0] ^ (1 - theta) + Nx[0] / (Ndbar[0] + Nx[0]) * (ztilde_d * tau / ztilde_x[0]) ^ (1 - theta)) / (Nd[0] / (Nd[0] + Nxbar[0]) + Nxbar[0] / (Nd[0] + Nxbar[0]) * (ztilde_dbar * tau * TOL[0] / ztilde_xbar[0]) ^ (1 - theta))) ^ (1 / (1 - theta)) + + Qtilde[0] = Q[0] * ((Nd[0] + Nxbar[0]) / (Ndbar[0] + Nx[0])) ^ (( - 1) / (theta - 1)) + + Z[0] = (1 - rho_Z) * 1.0 + rho_Z * Z[-1] + sigma__z * epsilon__z[x] + + Zbar[0] = 1.0 * (1 - rho_Zbar) + rho_Zbar * Zbar[-1] + sigma__z_bar * epsilon__z_bar[x] + + ztilde_x[0] = (theta * fx * (w[0] / Z[0]) ^ theta * (1 + (theta - 1) / (k - (theta - 1))) * Q[0] ^ (-theta) * tau ^ (theta - 1) * (theta / (theta - 1)) ^ (theta - 1) * Cbar[0] ^ (-1)) ^ (1 / (theta - 1)) + + ztilde_xbar[0] = (fxbar * (1 + (theta - 1) / (k - (theta - 1))) * tau ^ (theta - 1) * theta * (theta / (theta - 1)) ^ (theta - 1) * (wbar[0] / Zbar[0]) ^ theta * Q[0] ^ theta * C[0] ^ (-1)) ^ (1 / (theta - 1)) + + zx[0] = ztilde_x[0] / (k / (k - (theta - 1))) ^ (1 / (theta - 1)) + + zxbar[0] = ztilde_xbar[0] / (k / (k - (theta - 1))) ^ (1 / (theta - 1)) + +end + + +@parameters Ghironi_Melitz_2005 begin + sigma__z = 0.01 + + sigma__z_bar = 0.01 + + beta = 0.99 + + gamma = 2.0 + + delta = 0.025 + + theta = 3.8 + + k = 3.4 + + tau = 1.3 + + zmin = 1.0 + + zminbar = 1.0 + + fe = 1.0 + + febar = 1.0 + + L = 1.0 + + Lbar = 1.0 + + rho_Z = 0.9 + + rho_Zbar = 0.9 + + fx_share = 0.235 + + fx = fx_share*(1-beta*(1-delta))/(beta*(1-delta))*fe + + fxbar = fx_share*(1-beta*(1-delta))/(beta*(1-delta))*febar + + ztilde_d = (k/(k-(theta-1)))^(1/(theta-1))*zmin + + ztilde_dbar = (k/(k-(theta-1)))^(1/(theta-1))*zminbar + +end + diff --git a/test/Ghironi_Melitz_2005.mod b/test/Ghironi_Melitz_2005.mod new file mode 100644 index 000000000..f06598159 --- /dev/null +++ b/test/Ghironi_Melitz_2005.mod @@ -0,0 +1,143 @@ +var +C Cbar Nd Ndbar Ne Nx Nxbar Nebar Q Qtilde TOL Z Zbar dtilde dtilde_d dtilde_dbar dtilde_x dtilde_xbar dtilde_bar r rbar w wbar zx zxbar ztilde_x ztilde_xbar rho_tilde_d rho_tilde_dbar rho_tilde_x rho_tilde_xbar vtilde vtilde_bar ; + +varexo +epsilon__z epsilon__z_bar ; + +parameters +L Lbar fe fx fxbar febar k zmin zminbar ztilde_d ztilde_dbar beta gamma delta theta rho_Z rho_Zbar sigma__z sigma__z_bar tau ; + +% Parameter definitions: + sigma__z = 0.01; + sigma__z_bar = 0.01; + beta = 0.99; + gamma = 2.0; + delta = 0.025; + theta = 3.8; + k = 3.4; + tau = 1.3; + zmin = 1.0; + zminbar = 1.0; + fe = 1.0; + febar = 1.0; + L = 1.0; + Lbar = 1.0; + rho_Z = 0.9; + rho_Zbar = 0.9; + fx_share = 0.235; + fx = ((fx_share * (1 - beta * (1 - delta))) / (beta * (1 - delta))) * fe; + fxbar = ((fx_share * (1 - beta * (1 - delta))) / (beta * (1 - delta))) * febar; + ztilde_d = (k / (k - (theta - 1))) ^ (1 / (theta - 1)) * zmin; + ztilde_dbar = (k / (k - (theta - 1))) ^ (1 / (theta - 1)) * zminbar; + +model; + 1 = Nd(0) * rho_tilde_d(0) ^ (1 - theta) + Nxbar(0) * rho_tilde_xbar(0) ^ (1 - theta); + + 1 = Ndbar(0) * rho_tilde_dbar(0) ^ (1 - theta) + Nx(0) * rho_tilde_x(0) ^ (1 - theta); + + rho_tilde_d(0) = ((theta / (theta - 1)) * w(0)) / (Z(0) * ztilde_d); + + rho_tilde_dbar(0) = ((theta / (theta - 1)) * wbar(0)) / (Zbar(0) * ztilde_dbar); + + rho_tilde_x(0) = (((theta / (theta - 1)) * tau * w(0)) / (Z(0) * ztilde_x(0))) / Q(0); + + rho_tilde_xbar(0) = (((Q(0) * theta) / (theta - 1)) * tau * wbar(0)) / (Zbar(0) * ztilde_xbar(0)); + + dtilde(0) = dtilde_d(0) + (Nx(0) / Nd(0)) * dtilde_x(0); + + dtilde_bar(0) = dtilde_dbar(0) + (Nxbar(0) / Ndbar(0)) * dtilde_xbar(0); + + dtilde_d(0) = ((rho_tilde_d(0) ^ (1 - theta) * 1) / theta) * C(0); + + dtilde_dbar(0) = ((rho_tilde_dbar(0) ^ (1 - theta) * 1) / theta) * Cbar(0); + + vtilde(0) = (w(0) * fe) / Z(0); + + vtilde_bar(0) = (wbar(0) * febar) / Zbar(0); + + dtilde_x(0) = (((w(0) * fx) / Z(0)) * (theta - 1)) / (k - (theta - 1)); + + dtilde_xbar(0) = (((theta - 1) / (k - (theta - 1))) * wbar(0) * fxbar) / Zbar(0); + + Nx(0) / Nd(0) = (zmin / ztilde_x(0)) ^ k * (k / (k - (theta - 1))) ^ (k / (theta - 1)); + + Nxbar(0) / Ndbar(0) = (k / (k - (theta - 1))) ^ (k / (theta - 1)) * (zminbar / ztilde_xbar(0)) ^ k; + + Nd(0) = (1 - delta) * (Nd(-1) + Ne(-1)); + + Ndbar(0) = (1 - delta) * (Ndbar(-1) + Nebar(-1)); + + C(0) ^ -gamma = beta * (1 + r(0)) * C(1) ^ -gamma; + + Cbar(0) ^ -gamma = beta * (1 + rbar(0)) * Cbar(1) ^ -gamma; + + vtilde(0) = (1 - delta) * beta * (C(1) / C(0)) ^ -gamma * (vtilde(1) + dtilde(1)); + + vtilde_bar(0) = (1 - delta) * beta * (Cbar(1) / Cbar(0)) ^ -gamma * (vtilde_bar(1) + dtilde_bar(1)); + + C(0) = (w(0) * L + Nd(0) * dtilde(0)) - vtilde(0) * Ne(0); + + Cbar(0) = (wbar(0) * Lbar + Ndbar(0) * dtilde_bar(0)) - vtilde_bar(0) * Nebar(0); + + Q(0) = (Nxbar(0) * rho_tilde_xbar(0) ^ (1 - theta) * C(0)) / (Nx(0) * rho_tilde_x(0) ^ (1 - theta) * Cbar(0)); + + Qtilde(0) = (((Ndbar(0) / (Ndbar(0) + Nx(0))) * TOL(0) ^ (1 - theta) + (Nx(0) / (Ndbar(0) + Nx(0))) * ((tau * ztilde_d) / ztilde_x(0)) ^ (1 - theta)) / (Nd(0) / (Nd(0) + Nxbar(0)) + (Nxbar(0) / (Nd(0) + Nxbar(0))) * ((tau * TOL(0) * ztilde_dbar) / ztilde_xbar(0)) ^ (1 - theta))) ^ (1 / (1 - theta)); + + Qtilde(0) = Q(0) * ((Nd(0) + Nxbar(0)) / (Ndbar(0) + Nx(0))) ^ (-1 / (theta - 1)); + + Z(0) = (1 - rho_Z) * 1.0 + rho_Z * Z(-1) + sigma__z * epsilon__z; + + Zbar(0) = 1.0 * (1 - rho_Zbar) + rho_Zbar * Zbar(-1) + sigma__z_bar * epsilon__z_bar; + + ztilde_x(0) = (theta * fx * (w(0) / Z(0)) ^ theta * (1 + (theta - 1) / (k - (theta - 1))) * Q(0) ^ -theta * tau ^ (theta - 1) * (theta / (theta - 1)) ^ (theta - 1) * Cbar(0) ^ -1) ^ (1 / (theta - 1)); + + ztilde_xbar(0) = ((theta / (theta - 1)) ^ (theta - 1) * theta * tau ^ (theta - 1) * (1 + (theta - 1) / (k - (theta - 1))) * fxbar * (wbar(0) / Zbar(0)) ^ theta * Q(0) ^ theta * C(0) ^ -1) ^ (1 / (theta - 1)); + + zx(0) = ztilde_x(0) / (k / (k - (theta - 1))) ^ (1 / (theta - 1)); + + zxbar(0) = ztilde_xbar(0) / (k / (k - (theta - 1))) ^ (1 / (theta - 1)); + +end; + +shocks; +var epsilon__z = 1; +var epsilon__z_bar = 1; +end; + +initval; + C = 3.386882407731392; + Cbar = 3.38688240773139; + Nd = 7.506952650706411; + Ndbar = 7.506952650706394; + Ne = 0.19248596540272866; + Nx = 1.5798796065733534; + Nxbar = 1.5798796065733551; + Nebar = 0.1924859654027283; + Q = 1.0000000000000002; + Qtilde = 0.9999999999999993; + TOL = 0.9999999999999992; + Z = 1.0; + Zbar = 1.0; + dtilde = 0.11313270650973085; + dtilde_d = 0.0870217286331077; + dtilde_dbar = 0.08702172863310784; + dtilde_x = 0.12406886813900507; + dtilde_xbar = 0.12406886813900495; + dtilde_bar = 0.11313270650973105; + r = 0.01010101010101011; + rbar = 0.01010101010101011; + w = 3.142484747007705; + wbar = 3.142484747007702; + zx = 1.58150472992854; + zxbar = 1.5815047299285385; + ztilde_x = 2.938435014025518; + ztilde_xbar = 2.9384350140255155; + rho_tilde_d = 2.2953723636801207; + rho_tilde_dbar = 2.295372363680119; + rho_tilde_x = 1.8868005996535888; + rho_tilde_xbar = 1.8868005996535897; + vtilde = 3.142484747007705; + vtilde_bar = 3.142484747007702; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/Ireland_2004.jl b/test/Ireland_2004.jl new file mode 100644 index 000000000..9c4ca086f --- /dev/null +++ b/test/Ireland_2004.jl @@ -0,0 +1,51 @@ +using MacroModelling + +@model Ireland_2004 begin + a[0] = rho__a * a[-1] + sigma__a * epsilon__a[x] + + e[0] = rho__e * e[-1] + sigma__e * epsilon__e[x] + + x[0] = alpha__x * x[-1] + (1 - alpha__x) * x[1] - (rhat[0] - pi_hat[1]) + a[0] * (1 - omega) * (1 - rho__a) + + pi_hat[0] = beta * (alpha__p * pi_hat[-1] + pi_hat[1] * (1 - alpha__p)) + x[0] * psi - e[0] + + x[0] = yhat[0] - a[0] * omega + + ghat[0] = yhat[0] + sigma__z * epsilon__z[x] - yhat[-1] + + rhat[0] - rhat[-1] = pi_hat[0] * rho__p + ghat[0] * rho__g + x[0] * rho__x + sigma__r * epsilon__r[x] + +end + + +@parameters Ireland_2004 begin + beta = 0.99 + + psi = 0.1 + + omega = 0.0581 + + alpha__x = 1.0e-5 + + alpha__p = 1.0e-5 + + rho__p = 0.3866 + + rho__g = 0.396 + + rho__x = 0.1654 + + rho__a = 0.9048 + + rho__e = 0.9907 + + sigma__r = 0.0028 + + sigma__a = 0.0302 + + sigma__e = 0.0002 + + sigma__z = 0.0089 + +end + diff --git a/test/Ireland_2004.mod b/test/Ireland_2004.mod new file mode 100644 index 000000000..e47344c96 --- /dev/null +++ b/test/Ireland_2004.mod @@ -0,0 +1,60 @@ +var +a e rhat x ghat yhat pi_hat ; + +varexo +epsilon__r epsilon__a epsilon__e epsilon__z ; + +parameters +alpha__x alpha__p beta rho__x rho__a rho__e rho__g rho__p sigma__r sigma__a sigma__e sigma__z psi omega ; + +% Parameter definitions: + beta = 0.99; + psi = 0.1; + omega = 0.0581; + alpha__x = 1.0e-5; + alpha__p = 1.0e-5; + rho__p = 0.3866; + rho__g = 0.396; + rho__x = 0.1654; + rho__a = 0.9048; + rho__e = 0.9907; + sigma__r = 0.0028; + sigma__a = 0.0302; + sigma__e = 0.0002; + sigma__z = 0.0089; + +model; + a(0) = rho__a * a(-1) + sigma__a * epsilon__a; + + e(0) = rho__e * e(-1) + sigma__e * epsilon__e; + + x(0) = ((alpha__x * x(-1) + (1 - alpha__x) * x(1)) - (rhat(0) - pi_hat(1))) + a(0) * (1 - omega) * (1 - rho__a); + + pi_hat(0) = (beta * (alpha__p * pi_hat(-1) + pi_hat(1) * (1 - alpha__p)) + x(0) * psi) - e(0); + + x(0) = yhat(0) - a(0) * omega; + + ghat(0) = (sigma__z * epsilon__z + yhat(0)) - yhat(-1); + + rhat(0) - rhat(-1) = pi_hat(0) * rho__p + ghat(0) * rho__g + x(0) * rho__x + sigma__r * epsilon__r; + +end; + +shocks; +var epsilon__r = 1; +var epsilon__a = 1; +var epsilon__e = 1; +var epsilon__z = 1; +end; + +initval; + a = 0.0; + e = 0.0; + rhat = 0.0; + x = 0.0; + ghat = 0.0; + yhat = 0.0; + pi_hat = 0.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/JQ_2012_RBC.jl b/test/JQ_2012_RBC.jl new file mode 100644 index 000000000..880f5031f --- /dev/null +++ b/test/JQ_2012_RBC.jl @@ -0,0 +1,71 @@ +using MacroModelling + +@model JQ_2012_RBC begin + w[0] / c[0] ^ sigma = alpha / (1 - n[0]) + + c[0] ^ (-sigma) = beta * (R[0] - tau) / (1 - tau) * c[1] ^ (-sigma) + + w[0] * n[0] + b[-1] - b[0] / R[0] + d[0] = c[0] + + (1 - theta) * z[0] * k[-1] ^ theta * n[0] ^ (-theta) = w[0] / (1 - mu[0] * (1 + 2 * kappa * (d[0] - d[ss]))) + + (1 + 2 * kappa * (d[0] - d[ss])) * beta * (c[0] / c[1]) ^ sigma / (1 + 2 * kappa * (d[1] - d[ss])) * (1 - delta + theta * (1 - (1 + 2 * kappa * (d[1] - d[ss])) * mu[1]) * z[1] * k[0] ^ (theta - 1) * n[1] ^ (1 - theta)) + mu[0] * (1 + 2 * kappa * (d[0] - d[ss])) * xi[0] = 1 + + R[0] * beta * (c[0] / c[1]) ^ sigma * (1 + 2 * kappa * (d[0] - d[ss])) / (1 + 2 * kappa * (d[1] - d[ss])) + (1 - tau) * R[0] * mu[0] * (1 + 2 * kappa * (d[0] - d[ss])) * xi[0] / (R[0] - tau) = 1 + + b[0] / R[0] + k[-1] * (1 - delta) + z[0] * k[-1] ^ theta * n[0] ^ (1 - theta) - w[0] * n[0] - b[-1] - k[0] = d[0] + kappa * (d[0] - d[ss]) ^ 2 + + xi[0] * (k[0] - (1 - tau) * b[0] / (R[0] - tau)) = z[0] * k[-1] ^ theta * n[0] ^ (1 - theta) + + log(z[0] / zbar) = A_1_1 * log(z[-1] / zbar) + A_1__2 * log(xi[-1] / xi_bar) + sigma__z * epsilon__z[x] + + log(xi[0] / xi_bar) = log(z[-1] / zbar) * A_2__1 + log(xi[-1] / xi_bar) * A_2_2 + sigma__x__i * epsilon__x__i[x] + + y[0] = z[0] * k[-1] ^ theta * n[0] ^ (1 - theta) + + k[0] = k[-1] * (1 - delta) + i[0] + + v[0] = d[0] + c[0] * beta / c[1] * v[1] + + 1 + r[0] = (R[0] - tau) / (1 - tau) + +end + + +@parameters JQ_2012_RBC begin + BY_ratio = 3.36 + + nbar = 0.3 + + zbar = 1.0 + + beta = 0.9825 + + sigma = 1.0 + + theta = 0.36 + + delta = 0.025 + + tau = 0.35 + + kappa = 0.146 + + A_1_1 = 0.9457 + + A_1__2 = (-0.0091) + + A_2__1 = 0.0321 + + A_2_2 = 0.9703 + + sigma__z = 0.0045 + + sigma__x__i = 0.0098 + + xi_bar = 0.16337753022030044 + + alpha = 1.8834086344418162 + +end + diff --git a/test/JQ_2012_RBC.mod b/test/JQ_2012_RBC.mod new file mode 100644 index 000000000..849890f90 --- /dev/null +++ b/test/JQ_2012_RBC.mod @@ -0,0 +1,82 @@ +var +R b c d i k n r v w y z mu xi ; + +varexo +epsilon__x__i epsilon__z ; + +parameters +A_2_2 A_2__1 A_1__2 A_1_1 zbar alpha beta delta theta kappa xi_bar sigma sigma__x__i sigma__z tau ; + +% Parameter definitions: + BY_ratio = 3.36; + nbar = 0.3; + zbar = 1.0; + beta = 0.9825; + sigma = 1.0; + theta = 0.36; + delta = 0.025; + tau = 0.35; + kappa = 0.146; + A_1_1 = 0.9457; + A_1__2 = -0.0091; + A_2__1 = 0.0321; + A_2_2 = 0.9703; + sigma__z = 0.0045; + sigma__x__i = 0.0098; + xi_bar = 0.16337753022030044; + alpha = 1.8834086344418162; + +model; + w(0) / c(0) ^ sigma = alpha / (1 - n(0)); + + c(0) ^ -sigma = ((beta * (R(0) - tau)) / (1 - tau)) * c(1) ^ -sigma; + + ((w(0) * n(0) + b(-1)) - b(0) / R(0)) + d(0) = c(0); + + (1 - theta) * z(0) * k(-1) ^ theta * n(0) ^ -theta = w(0) / (1 - mu(0) * (1 + 2 * kappa * (d(0) - STEADY_STATE(d)))); + + ((beta * (c(0) / c(1)) ^ sigma * (1 + 2 * kappa * (d(0) - STEADY_STATE(d)))) / (1 + 2 * kappa * (d(1) - STEADY_STATE(d)))) * ((1 - delta) + theta * (1 - (1 + 2 * kappa * (d(1) - STEADY_STATE(d))) * mu(1)) * z(1) * k(0) ^ (theta - 1) * n(1) ^ (1 - theta)) + (1 + 2 * kappa * (d(0) - STEADY_STATE(d))) * mu(0) * xi(0) = 1; + + ((1 + 2 * kappa * (d(0) - STEADY_STATE(d))) / (1 + 2 * kappa * (d(1) - STEADY_STATE(d)))) * (c(0) / c(1)) ^ sigma * beta * R(0) + ((1 + 2 * kappa * (d(0) - STEADY_STATE(d))) * mu(0) * xi(0) * R(0) * (1 - tau)) / (R(0) - tau) = 1; + + (((b(0) / R(0) + k(-1) * (1 - delta) + z(0) * k(-1) ^ theta * n(0) ^ (1 - theta)) - w(0) * n(0)) - b(-1)) - k(0) = d(0) + kappa * (d(0) - STEADY_STATE(d)) ^ 2; + + xi(0) * (k(0) - (b(0) * (1 - tau)) / (R(0) - tau)) = z(0) * k(-1) ^ theta * n(0) ^ (1 - theta); + + log(z(0) / zbar) = A_1_1 * log(z(-1) / zbar) + A_1__2 * log(xi(-1) / xi_bar) + sigma__z * epsilon__z; + + log(xi(0) / xi_bar) = log(z(-1) / zbar) * A_2__1 + log(xi(-1) / xi_bar) * A_2_2 + sigma__x__i * epsilon__x__i; + + y(0) = z(0) * k(-1) ^ theta * n(0) ^ (1 - theta); + + k(0) = k(-1) * (1 - delta) + i(0); + + v(0) = d(0) + ((c(0) * beta) / c(1)) * v(1); + + 1 + r(0) = (R(0) - tau) / (1 - tau); + +end; + +shocks; +var epsilon__x__i = 1; +var epsilon__z = 1; +end; + +initval; + R = 1.0115776081424936; + b = 3.6358259916618034; + c = 0.8111657946199857; + d = 0.11480054308526633; + i = 0.2519886806204083; + k = 10.07954722481633; + n = 0.3; + r = 0.017811704834605608; + v = 6.5600310334438054; + w = 2.182509516501626; + y = 1.0631544752403934; + z = 1.0; + mu = 0.03772089598850488; + xi = 0.16337753022030047; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/NAWM_EAUS_2008.jl b/test/NAWM_EAUS_2008.jl new file mode 100644 index 000000000..b15a029ef --- /dev/null +++ b/test/NAWM_EAUS_2008.jl @@ -0,0 +1,771 @@ +using MacroModelling + +@model NAWM_EAUS_2008 begin + EA_R[0] ^ 4 - 1 = EA_PHIRR * (EA_R[-1] ^ 4 - 1) + (1 - EA_PHIRR) * (EA_RRSTAR ^ 4 * EA_PI4TARGET - 1 + EA_PHIRPI * (EA_PIC4[0] - EA_PI4TARGET)) + EA_PHIRGY * (EA_Y[0] / EA_Y[-1] - 1) + sigma__EA_R * EA_EPSR[x] + + US_R[0] ^ 4 - 1 = US_PHIRR * (US_R[-1] ^ 4 - 1) + (1 - US_PHIRR) * (US_RRSTAR ^ 4 * US_PI4TARGET - 1 + US_PHIRPI * (US_PIC4[0] - US_PI4TARGET)) + US_PHIRGY * (US_Y[0] / US_Y[-1] - 1) + sigma__US_R * US_EPSR[x] + + EA_UTILI[0] = 1 / (1 - EA_SIGMA) * (EA_CI[0] - EA_KAPPA * EA_CI[-1]) ^ (1 - EA_SIGMA) - 1 / (1 + EA_ZETA) * EA_NI[0] ^ (1 + EA_ZETA) + EA_BETA * EA_UTILI[1] + + EA_LAMBDAI[0] * (1 + EA_TAUC[0] + EA_GAMMAVI[0] + EA_VI[0] * EA_GAMMAVIDER[0]) = (EA_CI[0] - EA_KAPPA * EA_CI[-1]) ^ (-EA_SIGMA) + + EA_R[0] = EA_LAMBDAI[0] * EA_BETA ^ (-1) / EA_LAMBDAI[1] * EA_PIC[1] + + EA_GAMMAVIDER[0] * EA_VI[0] ^ 2 = 1 - EA_BETA * EA_LAMBDAI[1] / (EA_LAMBDAI[0] * EA_PIC[1]) + + EA_VI[0] = EA_CI[0] * (1 + EA_TAUC[0]) / EA_MI[0] + + EA_GAMMAVI[0] = EA_VI[0] * EA_GAMMAV1 + EA_GAMMAV2 / EA_VI[0] - 2 * (EA_GAMMAV1 * EA_GAMMAV2) ^ 0.5 + + EA_GAMMAVIDER[0] = EA_GAMMAV1 - EA_GAMMAV2 * EA_VI[0] ^ (-2) + + EA_KI[0] = (1 - EA_DELTA) * EA_KI[-1] + (1 - EA_GAMMAI[-1]) * EA_II[-1] + + EA_GAMMAI[0] = EA_GAMMAI1 / 2 * (EA_II[0] / EA_II[-1] - 1) ^ 2 + + EA_GAMMAIDER[0] = EA_GAMMAI1 * (EA_II[0] / EA_II[-1] - 1) / EA_II[-1] + + EA_GAMMAU[0] = ((EA_BETA ^ (-1) + EA_DELTA - 1) * EA_QBAR - EA_DELTA * EA_TAUKBAR * EA_PIBAR) / (EA_PIBAR * (1 - EA_TAUKBAR)) * (EA_U[0] - 1) + EA_GAMMAU2 / 2 * (EA_U[0] - 1) ^ 2 + + EA_GAMMAUDER[0] = ((EA_BETA ^ (-1) + EA_DELTA - 1) * EA_QBAR - EA_DELTA * EA_TAUKBAR * EA_PIBAR) / (EA_PIBAR * (1 - EA_TAUKBAR)) + (EA_U[0] - 1) * EA_GAMMAU2 + + EA_RK[0] = EA_GAMMAUDER[0] * EA_PI[0] + + EA_PI[0] = EA_Q[0] * (1 - EA_GAMMAI[0] - EA_II[0] * EA_GAMMAIDER[0]) + EA_BETA * EA_LAMBDAI[1] / EA_LAMBDAI[0] * EA_Q[1] * EA_GAMMAIDER[1] * EA_II[1] ^ 2 / EA_II[0] + + EA_Q[0] = EA_BETA * EA_LAMBDAI[1] / EA_LAMBDAI[0] * ((1 - EA_TAUK[1]) * (EA_RK[1] * EA_U[1] - EA_GAMMAU[1] * EA_PI[1]) + EA_TAUK[1] * EA_DELTA * EA_PI[1] + (1 - EA_DELTA) * EA_Q[1]) + + EA_WITILDE[0] ^ (1 + EA_ZETA * EA_ETAI) = EA_ETAI / (EA_ETAI - 1) * EA_FI[0] / EA_GI[0] + + EA_FI[0] = EA_WI[0] ^ ((1 + EA_ZETA) * EA_ETAI) * EA_NDI[0] ^ (1 + EA_ZETA) + EA_BETA * EA_XII * (EA_PIC[1] / (EA_PIC[0] ^ EA_CHII * EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)))) ^ ((1 + EA_ZETA) * EA_ETAI) * EA_FI[1] + + EA_GI[0] = EA_LAMBDAI[0] * EA_NDI[0] * (1 - EA_TAUN[0] - EA_TAUWH[0]) * EA_WI[0] ^ EA_ETAI + EA_BETA * EA_XII * (EA_PIC[1] / (EA_PIC[0] ^ EA_CHII * EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)))) ^ (EA_ETAI - 1) * EA_GI[1] + + EA_WI[0] ^ (1 - EA_ETAI) = (1 - EA_XII) * EA_WITILDE[0] ^ (1 - EA_ETAI) + EA_XII * EA_WI[-1] ^ (1 - EA_ETAI) * (EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)) * EA_PIC[-1] ^ EA_CHII / EA_PIC[0]) ^ (1 - EA_ETAI) + + EA_UTILJ[0] = 1 / (1 - EA_SIGMA) * (EA_CJ[0] - EA_KAPPA * EA_CJ[-1]) ^ (1 - EA_SIGMA) - 1 / (1 + EA_ZETA) * EA_NJ[0] ^ (1 + EA_ZETA) + EA_BETA * EA_UTILJ[1] + + EA_CJ[0] * (1 + EA_TAUC[0] + EA_GAMMAVJ[0]) + EA_MJ[0] = (1 - EA_TAUN[0] - EA_TAUWH[0]) * EA_NJ[0] * EA_WJ[0] + EA_TRJ[0] - EA_TJ[0] + EA_MJ[-1] * EA_PIC[0] ^ (-1) + + EA_LAMBDAJ[0] * (1 + EA_TAUC[0] + EA_GAMMAVJ[0] + EA_VJ[0] * EA_GAMMAVJDER[0]) = (EA_CJ[0] - EA_KAPPA * EA_CJ[-1]) ^ (-EA_SIGMA) + + EA_GAMMAVJDER[0] * EA_VJ[0] ^ 2 = 1 - EA_BETA * EA_LAMBDAJ[1] / (EA_PIC[1] * EA_LAMBDAJ[0]) + + EA_VJ[0] = (1 + EA_TAUC[0]) * EA_CJ[0] / EA_MJ[0] + + EA_GAMMAVJ[0] = EA_GAMMAV1 * EA_VJ[0] + EA_GAMMAV2 / EA_VJ[0] - 2 * (EA_GAMMAV1 * EA_GAMMAV2) ^ 0.5 + + EA_GAMMAVJDER[0] = EA_GAMMAV1 - EA_GAMMAV2 * EA_VJ[0] ^ (-2) + + EA_WJTILDE[0] ^ (1 + EA_ZETA * EA_ETAJ) = EA_ETAJ / (EA_ETAJ - 1) * EA_FJ[0] / EA_GJ[0] + + EA_FJ[0] = EA_WJ[0] ^ ((1 + EA_ZETA) * EA_ETAJ) * EA_NDJ[0] ^ (1 + EA_ZETA) + EA_BETA * EA_XIJ * (EA_PIC[1] / (EA_PIC[0] ^ EA_CHIJ * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)))) ^ ((1 + EA_ZETA) * EA_ETAJ) * EA_FJ[1] + + EA_GJ[0] = EA_LAMBDAJ[0] * (1 - EA_TAUN[0] - EA_TAUWH[0]) * EA_NDJ[0] * EA_WJ[0] ^ EA_ETAJ + EA_BETA * EA_XIJ * (EA_PIC[1] / (EA_PIC[0] ^ EA_CHIJ * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)))) ^ (EA_ETAJ - 1) * EA_GJ[1] + + EA_WJ[0] ^ (1 - EA_ETAJ) = (1 - EA_XIJ) * EA_WJTILDE[0] ^ (1 - EA_ETAJ) + EA_XIJ * EA_WJ[-1] ^ (1 - EA_ETAJ) * (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)) * EA_PIC[-1] ^ EA_CHIJ / EA_PIC[0]) ^ (1 - EA_ETAJ) + + EA_YS[0] = EA_Z[0] * EA_KD[0] ^ EA_ALPHA * EA_ND[0] ^ (1 - EA_ALPHA) - EA_PSIBAR + + EA_RK[0] = EA_ALPHA * (EA_YS[0] + EA_PSIBAR) / EA_KD[0] * EA_MC[0] + + EA_MC[0] = 1 / (EA_Z[0] * EA_ALPHA ^ EA_ALPHA * (1 - EA_ALPHA) ^ (1 - EA_ALPHA)) * EA_RK[0] ^ EA_ALPHA * ((1 + EA_TAUWF[0]) * EA_W[0]) ^ (1 - EA_ALPHA) + + EA_NDI[0] = EA_ND[0] * (1 - EA_OMEGA) * (EA_WI[0] / EA_W[0]) ^ (-EA_ETA) + + EA_NDJ[0] = EA_ND[0] * EA_OMEGA * (EA_WJ[0] / EA_W[0]) ^ (-EA_ETA) + + EA_ND[0] ^ (1 - 1 / EA_ETA) = (1 - EA_OMEGA) ^ (1 / EA_ETA) * EA_NDI[0] ^ (1 - 1 / EA_ETA) + EA_OMEGA ^ (1 / EA_ETA) * EA_NDJ[0] ^ (1 - 1 / EA_ETA) + + EA_D[0] = EA_Y[0] * EA_PY[0] - EA_RK[0] * EA_KD[0] - EA_W[0] * EA_ND[0] * (1 + EA_TAUWF[0]) + + EA_PHTILDE[0] / EA_PH[0] = EA_THETA / (EA_THETA - 1) * EA_FH[0] / EA_GH[0] + + EA_FH[0] = EA_MC[0] * EA_H[0] + EA_BETA * EA_LAMBDAI[1] * EA_XIH / EA_LAMBDAI[0] * (EA_PIH[1] / (EA_PIH[0] ^ EA_CHIH * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)))) ^ EA_THETA * EA_FH[1] + + EA_GH[0] = EA_PH[0] * EA_H[0] + EA_BETA * EA_LAMBDAI[1] * EA_XIH / EA_LAMBDAI[0] * (EA_PIH[1] / (EA_PIH[0] ^ EA_CHIH * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)))) ^ (EA_THETA - 1) * EA_GH[1] + + EA_PH[0] ^ (1 - EA_THETA) = (1 - EA_XIH) * EA_PHTILDE[0] ^ (1 - EA_THETA) + EA_XIH * (EA_PH[-1] / EA_PIC[0]) ^ (1 - EA_THETA) * (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * EA_PIH[-1] ^ EA_CHIH) ^ (1 - EA_THETA) + + EA_PIH[0] = EA_PIC[0] * EA_PH[0] / EA_PH[-1] + + US_PIMTILDE[0] / US_PIM[0] = EA_THETA / (EA_THETA - 1) * EA_FX[0] / EA_GX[0] + + EA_FX[0] = EA_MC[0] * US_SIZE / EA_SIZE * US_IM[0] + EA_BETA * EA_LAMBDAI[1] * EA_XIX / EA_LAMBDAI[0] * (US_PIIM[1] / (US_PIIM[0] ^ EA_CHIX * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIX)))) ^ EA_THETA * EA_FX[1] + + EA_GX[0] = US_PIM[0] * US_SIZE * US_IM[0] * EAUS_RER[0] / EA_SIZE + EA_BETA * EA_LAMBDAI[1] * EA_XIX / EA_LAMBDAI[0] * (US_PIIM[1] / (US_PIIM[0] ^ EA_CHIX * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIX)))) ^ (EA_THETA - 1) * EA_GX[1] + + US_PIM[0] ^ (1 - EA_THETA) = (1 - EA_XIX) * US_PIMTILDE[0] ^ (1 - EA_THETA) + EA_XIX * (US_PIM[-1] / US_PIC[0]) ^ (1 - EA_THETA) * (US_PIIM[-1] ^ EA_CHIX * US_PI4TARGET ^ (0.25 * (1 - EA_CHIH))) ^ (1 - EA_THETA) + + US_PIIM[0] = US_PIM[0] * US_PIC[0] / US_PIM[-1] + + EAUS_RER[0] = EA_RER[0] / US_RER + + EA_QC[0] ^ ((EA_MUC - 1) / EA_MUC) = EA_NUC ^ (1 / EA_MUC) * EA_HC[0] ^ (1 - 1 / EA_MUC) + (1 - EA_NUC) ^ (1 / EA_MUC) * ((1 - EA_GAMMAIMC[0]) * EA_IMC[0]) ^ (1 - 1 / EA_MUC) + + 1 = EA_NUC * EA_PH[0] ^ (1 - EA_MUC) + (1 - EA_NUC) * (EA_PIM[0] / EA_GAMMAIMCDAG[0]) ^ (1 - EA_MUC) + + EA_HC[0] = EA_QC[0] * EA_NUC * EA_PH[0] ^ (-EA_MUC) + + EA_GAMMAIMC[0] = EA_GAMMAIMC1 / 2 * (EA_IMC[0] / EA_QC[0] / (EA_IMC[-1] / EA_QC[-1]) - 1) ^ 2 + + EA_GAMMAIMCDAG[0] = 1 - EA_GAMMAIMC[0] - (EA_IMC[0] / EA_QC[0] / (EA_IMC[-1] / EA_QC[-1]) - 1) * EA_IMC[0] * EA_GAMMAIMC1 / EA_QC[0] / (EA_IMC[-1] / EA_QC[-1]) + + EA_QI[0] ^ ((EA_MUI - 1) / EA_MUI) = EA_NUI ^ (1 / EA_MUI) * EA_HI[0] ^ (1 - 1 / EA_MUI) + (1 - EA_NUI) ^ (1 / EA_MUI) * ((1 - EA_GAMMAIMI[0]) * EA_IMI[0]) ^ (1 - 1 / EA_MUI) + + EA_PI[0] ^ (1 - EA_MUI) = EA_NUI * EA_PH[0] ^ (1 - EA_MUI) + (1 - EA_NUI) * (EA_PIM[0] / EA_GAMMAIMIDAG[0]) ^ (1 - EA_MUI) + + EA_HI[0] = EA_QI[0] * EA_NUI * (EA_PH[0] / EA_PI[0]) ^ (-EA_MUI) + + EA_GAMMAIMI[0] = EA_GAMMAIMI1 / 2 * (EA_IMI[0] / EA_QI[0] / (EA_IMI[-1] / EA_QI[-1]) - 1) ^ 2 + + EA_GAMMAIMIDAG[0] = 1 - EA_GAMMAIMI[0] - EA_IMI[0] * EA_GAMMAIMI1 * (EA_IMI[0] / EA_QI[0] / (EA_IMI[-1] / EA_QI[0]) - 1) / EA_QI[0] / (EA_IMI[-1] / EA_QI[-1]) + + EA_PH[-1] * EA_G[-1] + EA_TR[-1] + EA_B[-1] * EA_PIC[-1] ^ (-1) + EA_PIC[-1] ^ (-1) * AUX_ENDO_LAG_51_1[-1] = EA_TAUC[-1] * EA_C[-1] + (EA_TAUN[-1] + EA_TAUWH[-1]) * (EA_WI[-1] * EA_NDI[-1] + EA_WJ[-1] * EA_NDJ[-1]) + EA_TAUWF[-1] * EA_W[-1] * EA_ND[-1] + EA_TAUK[-1] * (EA_RK[-1] * EA_U[-1] - (EA_DELTA + EA_GAMMAU[-1]) * EA_PI[-1]) * EA_K[-1] + EA_TAUD[-1] * EA_D[-1] + EA_T[-1] + EA_R[-1] ^ (-1) * EA_B[0] + EA_M[-1] + + EA_PH[0] * EA_G[0] = EA_GY[0] * EA_PYBAR * EA_YBAR + + EA_TR[0] = EA_PYBAR * EA_YBAR * EA_TRY[0] + + EA_T[0] / (EA_PYBAR * EA_YBAR) = EA_PHITB * (EA_B[0] / (EA_PYBAR * EA_YBAR) - EA_BYTARGET) + + EA_TI[0] = EA_T[0] * EA_UPSILONT + + EA_TRI[0] = EA_TR[0] * EA_UPSILONTR + + EA_PIC4[0] = EA_PIC[0] * EA_PIC[-1] * AUX_ENDO_LAG_63_1[-1] * AUX_ENDO_LAG_63_2[-1] + + EA_RR[0] - 1 = EA_R[0] / EA_PIC[1] - 1 + + EA_C[0] = EA_CI[0] * (1 - EA_OMEGA) + EA_CJ[0] * EA_OMEGA + + EA_M[0] = EA_MI[0] * (1 - EA_OMEGA) + EA_MJ[0] * EA_OMEGA + + EA_K[0] = EA_KI[0] * (1 - EA_OMEGA) + + EA_I[0] = EA_II[0] * (1 - EA_OMEGA) + + EA_TRJ[0] = EA_TR[0] / EA_OMEGA - (1 - EA_OMEGA) * EA_TRI[0] / EA_OMEGA + + EA_TJ[0] = EA_T[0] / EA_OMEGA - (1 - EA_OMEGA) * EA_TI[0] / EA_OMEGA + + EA_GAMMAV[0] = (1 - EA_OMEGA) * EA_CI[0] * EA_GAMMAVI[0] + EA_OMEGA * EA_CJ[0] * EA_GAMMAVJ[0] + + EA_NI[0] = EA_NDI[0] * EA_SI[0] + + EA_SI[0] = (1 - EA_XII) * (EA_WITILDE[0] / EA_WI[0]) ^ (-EA_ETAI) + EA_XII * (EA_WI[-1] / EA_WI[0]) ^ (-EA_ETAI) * (EA_PIC[0] / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)) * EA_PIC[-1] ^ EA_CHII)) ^ EA_ETAI * EA_SI[-1] + + EA_NJ[0] = EA_NDJ[0] * EA_SJ[0] + + EA_SJ[0] = (1 - EA_XIJ) * (EA_WJTILDE[0] / EA_WJ[0]) ^ (-EA_ETAJ) + EA_XIJ * (EA_WJ[-1] / EA_WJ[0]) ^ (-EA_ETAJ) * (EA_PIC[0] / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)) * EA_PIC[-1] ^ EA_CHIJ)) ^ EA_ETAJ * EA_SJ[-1] + + EA_U[0] * EA_K[0] = EA_KD[0] + + EA_YS[0] = EA_H[0] * EA_SH[0] + US_SIZE * US_IM[0] * EA_SX[0] / EA_SIZE + + EA_H[0] = EA_HI[0] + EA_HC[0] + EA_G[0] + + EA_IM[0] = EA_IMC[0] + EA_IMI[0] + + EA_SH[0] = (1 - EA_XIH) * (EA_PHTILDE[0] / EA_PH[0]) ^ (-EA_THETA) + EA_XIH * (EA_PIH[0] / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * EA_PIH[-1] ^ EA_CHIH)) ^ EA_THETA * EA_SH[-1] + + EA_SX[0] = (1 - EA_XIX) * (US_PIMTILDE[0] / US_PIM[0]) ^ (-EA_THETA) + EA_XIX * (US_PIIM[0] / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * US_PIIM[-1] ^ EA_CHIX)) ^ EA_THETA * EA_SX[-1] + + EA_QC[0] = EA_C[0] + EA_GAMMAV[0] + + EA_QI[0] = EA_I[0] + EA_GAMMAU[0] * EA_K[0] + + EA_Y[0] * EA_PY[0] = EA_QC[0] + US_PIM[0] * US_SIZE * US_IM[0] * EAUS_RER[0] / EA_SIZE + EA_PH[0] * EA_G[0] + EA_PI[0] * EA_QI[0] - EA_PIM[0] * ((1 - EA_GAMMAIMC[0]) * EA_IMC[0] / EA_GAMMAIMCDAG[0] + (1 - EA_GAMMAIMI[0]) * EA_IMI[0] / EA_GAMMAIMIDAG[0]) + + EA_Y[0] = EA_YS[0] + + log(EA_Z[0]) = (1 - EA_RHOZ) * log(EA_ZBAR) + EA_RHOZ * log(EA_Z[-1]) + sigma__EA_Z * EA_EPSZ[x] + + EA_GY[0] = (1 - EA_RHOG) * EA_GYBAR + EA_RHOG * EA_GY[-1] + sigma__EA_G * EA_EPSG[x] + + EA_TRY[0] = (1 - EA_RHOTR) * EA_TRYBAR + EA_RHOTR * EA_TRY[-1] + sigma__EA_TR * EA_EPSTR[x] + + EA_TAUC[0] = (1 - EA_RHOTAUC) * EA_TAUCBAR + EA_TAUC[-1] * EA_RHOTAUC + sigma__EA_TAUC * EA_EPSTAUC[x] + + EA_TAUD[0] = (1 - EA_RHOTAUD) * EA_TAUDBAR + EA_TAUD[-1] * EA_RHOTAUD + sigma__EA_TAUD * EA_EPSTAUD[x] + + EA_TAUK[0] = EA_TAUKBAR * (1 - EA_RHOTAUK) + EA_TAUK[-1] * EA_RHOTAUK + sigma__EA_TAUK * EA_EPSTAUK[x] + + EA_TAUN[0] = (1 - EA_RHOTAUN) * EA_TAUNBAR + EA_TAUN[-1] * EA_RHOTAUN + sigma__EA_TAUN * EA_EPSTAUN[x] + + EA_TAUWH[0] = (1 - EA_RHOTAUWH) * EA_TAUWHBAR + EA_TAUWH[-1] * EA_RHOTAUWH + sigma__EA_TAUWH * EA_EPSTAUWH[x] + + EA_TAUWF[0] = (1 - EA_RHOTAUWF) * EA_TAUWFBAR + EA_TAUWF[-1] * EA_RHOTAUWF + sigma__EA_TAUWF * EA_EPSTAUWF[x] + + EA_CY[0] = EA_C[0] / (EA_Y[0] * EA_PY[0]) + + EA_IY[0] = EA_PI[0] * EA_I[0] / (EA_Y[0] * EA_PY[0]) + + EA_IMY[0] = EA_PIM[0] * EA_IM[0] / (EA_Y[0] * EA_PY[0]) + + EA_IMCY[0] = EA_IMC[0] * EA_PIM[0] / (EA_Y[0] * EA_PY[0]) + + EA_IMIY[0] = EA_PIM[0] * EA_IMI[0] / (EA_Y[0] * EA_PY[0]) + + EA_BY[0] = EA_B[0] / (EA_PYBAR * EA_YBAR) + + EA_TY[0] = EA_T[0] / (EA_PYBAR * EA_YBAR) + + EA_YGAP[0] = EA_Y[0] / EA_YBAR - 1 + + EA_YGROWTH[0] = EA_Y[0] / EA_Y[-1] + + EA_YSHARE[0] = EA_Y[0] * EA_PY[0] * EA_SIZE / EA_RER[0] / (EA_Y[0] * EA_PY[0] * EA_SIZE / EA_RER[0] + US_Y[0] * US_SIZE * US_PY[0] / US_RER) + + EA_EPSILONM[0] = ( - 0.125) / (EA_R[0] * (EA_R[0] + EA_R[0] * EA_GAMMAV2 - 1)) + + US_UTILI[0] = 1 / (1 - US_SIGMA) * (US_CI[0] - US_KAPPA * US_CI[-1]) ^ (1 - US_SIGMA) - 1 / (1 + US_ZETA) * US_NI[0] ^ (1 + US_ZETA) + US_BETA * US_UTILI[1] + + US_LAMBDAI[0] * (1 + US_TAUC[0] + US_GAMMAVI[0] + US_VI[0] * US_GAMMAVIDER[0]) = (US_CI[0] - US_KAPPA * US_CI[-1]) ^ (-US_SIGMA) + + US_R[0] = US_LAMBDAI[0] * US_BETA ^ (-1) / US_LAMBDAI[1] * US_PIC[1] + + US_GAMMAVIDER[0] * US_VI[0] ^ 2 = 1 - US_BETA * US_LAMBDAI[1] / (US_LAMBDAI[0] * US_PIC[1]) + + US_VI[0] = US_CI[0] * (1 + US_TAUC[0]) / US_MI[0] + + US_GAMMAVI[0] = US_VI[0] * US_GAMMAV1 + US_GAMMAV2 / US_VI[0] - 2 * (US_GAMMAV1 * US_GAMMAV2) ^ 0.5 + + US_GAMMAVIDER[0] = US_GAMMAV1 - US_GAMMAV2 * US_VI[0] ^ (-2) + + US_KI[0] = (1 - US_DELTA) * US_KI[-1] + (1 - US_GAMMAI[-1]) * US_II[-1] + + US_GAMMAI[0] = US_GAMMAI1 / 2 * (US_II[0] / US_II[-1] - 1) ^ 2 + + US_GAMMAIDER[0] = US_GAMMAI1 * (US_II[0] / US_II[-1] - 1) / US_II[-1] + + US_GAMMAU[0] = ((US_BETA ^ (-1) + US_DELTA - 1) * US_QBAR - US_DELTA * US_TAUKBAR * US_PIBAR) / (US_PIBAR * (1 - US_TAUKBAR)) * (US_U[0] - 1) + US_GAMMAU2 / 2 * (US_U[0] - 1) ^ 2 + + US_GAMMAUDER[0] = ((US_BETA ^ (-1) + US_DELTA - 1) * US_QBAR - US_DELTA * US_TAUKBAR * US_PIBAR) / (US_PIBAR * (1 - US_TAUKBAR)) + (US_U[0] - 1) * US_GAMMAU2 + + US_RK[0] = US_GAMMAUDER[0] * US_PI[0] + + US_PI[0] = US_Q[0] * (1 - US_GAMMAI[0] - US_II[0] * US_GAMMAIDER[0]) + US_BETA * US_LAMBDAI[1] / US_LAMBDAI[0] * US_Q[1] * US_GAMMAIDER[1] * US_II[1] ^ 2 / US_II[0] + + US_Q[0] = US_BETA * US_LAMBDAI[1] / US_LAMBDAI[0] * ((1 - US_TAUK[1]) * (US_RK[1] * US_U[1] - US_GAMMAU[1] * US_PI[1]) + US_TAUK[1] * US_DELTA * US_PI[1] + (1 - US_DELTA) * US_Q[1]) + + US_WITILDE[0] ^ (1 + US_ZETA * US_ETAI) = US_ETAI / (US_ETAI - 1) * US_FI[0] / US_GI[0] + + US_FI[0] = US_WI[0] ^ ((1 + US_ZETA) * US_ETAI) * US_NDI[0] ^ (1 + US_ZETA) + US_BETA * US_XII * (US_PIC[1] / (US_PIC[0] ^ US_CHII * US_PI4TARGET ^ (0.25 * (1 - US_CHII)))) ^ ((1 + US_ZETA) * US_ETAI) * US_FI[1] + + US_GI[0] = US_LAMBDAI[0] * US_NDI[0] * (1 - US_TAUN[0] - US_TAUWH[0]) * US_WI[0] ^ US_ETAI + US_BETA * US_XII * (US_PIC[1] / (US_PIC[0] ^ US_CHII * US_PI4TARGET ^ (0.25 * (1 - US_CHII)))) ^ (US_ETAI - 1) * US_GI[1] + + US_WI[0] ^ (1 - US_ETAI) = (1 - US_XII) * US_WITILDE[0] ^ (1 - US_ETAI) + US_XII * US_WI[-1] ^ (1 - US_ETAI) * (US_PI4TARGET ^ (0.25 * (1 - US_CHII)) * US_PIC[-1] ^ US_CHII / US_PIC[0]) ^ (1 - US_ETAI) + + US_UTILJ[0] = 1 / (1 - US_SIGMA) * (US_CJ[0] - US_KAPPA * US_CJ[-1]) ^ (1 - US_SIGMA) - 1 / (1 + US_ZETA) * US_NJ[0] ^ (1 + US_ZETA) + US_BETA * US_UTILJ[1] + + US_CJ[0] * (1 + US_TAUC[0] + US_GAMMAVJ[0]) + US_MJ[0] = (1 - US_TAUN[0] - US_TAUWH[0]) * US_NJ[0] * US_WJ[0] + US_TRJ[0] - US_TJ[0] + US_MJ[-1] * US_PIC[0] ^ (-1) + + US_LAMBDAJ[0] * (1 + US_TAUC[0] + US_GAMMAVJ[0] + US_VJ[0] * US_GAMMAVJDER[0]) = (US_CJ[0] - US_KAPPA * US_CJ[-1]) ^ (-US_SIGMA) + + US_GAMMAVJDER[0] * US_VJ[0] ^ 2 = 1 - US_BETA * US_LAMBDAJ[1] / (US_PIC[1] * US_LAMBDAJ[0]) + + US_VJ[0] = (1 + US_TAUC[0]) * US_CJ[0] / US_MJ[0] + + US_GAMMAVJ[0] = US_GAMMAV1 * US_VJ[0] + US_GAMMAV2 / US_VJ[0] - 2 * (US_GAMMAV1 * US_GAMMAV2) ^ 0.5 + + US_GAMMAVJDER[0] = US_GAMMAV1 - US_GAMMAV2 * US_VJ[0] ^ (-2) + + US_WJTILDE[0] ^ (1 + US_ZETA * US_ETAJ) = US_ETAJ / (US_ETAJ - 1) * US_FJ[0] / US_GJ[0] + + US_FJ[0] = US_WJ[0] ^ ((1 + US_ZETA) * US_ETAJ) * US_NDJ[0] ^ (1 + US_ZETA) + US_BETA * US_XIJ * (US_PIC[1] / (US_PIC[0] ^ US_CHIJ * US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)))) ^ ((1 + US_ZETA) * US_ETAJ) * US_FJ[1] + + US_GJ[0] = US_LAMBDAJ[0] * (1 - US_TAUN[0] - US_TAUWH[0]) * US_NDJ[0] * US_WJ[0] ^ US_ETAJ + US_BETA * US_XIJ * (US_PIC[1] / (US_PIC[0] ^ US_CHIJ * US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)))) ^ (US_ETAJ - 1) * US_GJ[1] + + US_WJ[0] ^ (1 - US_ETAJ) = (1 - US_XIJ) * US_WJTILDE[0] ^ (1 - US_ETAJ) + US_XIJ * US_WJ[-1] ^ (1 - US_ETAJ) * (US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)) * US_PIC[-1] ^ US_CHIJ / US_PIC[0]) ^ (1 - US_ETAJ) + + US_YS[0] = US_Z[0] * US_KD[0] ^ US_ALPHA * US_ND[0] ^ (1 - US_ALPHA) - US_PSIBAR + + US_RK[0] = US_ALPHA * (US_YS[0] + US_PSIBAR) / US_KD[0] * US_MC[0] + + US_MC[0] = 1 / (US_Z[0] * US_ALPHA ^ US_ALPHA * (1 - US_ALPHA) ^ (1 - US_ALPHA)) * US_RK[0] ^ US_ALPHA * ((1 + US_TAUWF[0]) * US_W[0]) ^ (1 - US_ALPHA) + + US_NDI[0] = US_ND[0] * (1 - US_OMEGA) * (US_WI[0] / US_W[0]) ^ (-US_ETA) + + US_NDJ[0] = US_ND[0] * US_OMEGA * (US_WJ[0] / US_W[0]) ^ (-US_ETA) + + US_ND[0] ^ (1 - 1 / US_ETA) = (1 - US_OMEGA) ^ (1 / US_ETA) * US_NDI[0] ^ (1 - 1 / US_ETA) + US_OMEGA ^ (1 / US_ETA) * US_NDJ[0] ^ (1 - 1 / US_ETA) + + US_D[0] = US_Y[0] * US_PY[0] - US_RK[0] * US_KD[0] - US_W[0] * US_ND[0] * (1 + US_TAUWF[0]) + + US_PHTILDE[0] / US_PH[0] = US_THETA / (US_THETA - 1) * US_FH[0] / US_GH[0] + + US_FH[0] = US_MC[0] * US_H[0] + US_BETA * US_LAMBDAI[1] * US_XIH / US_LAMBDAI[0] * (US_PIH[1] / (US_PIH[0] ^ US_CHIH * US_PI4TARGET ^ (0.25 * (1 - US_CHIH)))) ^ US_THETA * US_FH[1] + + US_GH[0] = US_PH[0] * US_H[0] + US_BETA * US_LAMBDAI[1] * US_XIH / US_LAMBDAI[0] * (US_PIH[1] / (US_PIH[0] ^ US_CHIH * US_PI4TARGET ^ (0.25 * (1 - US_CHIH)))) ^ (US_THETA - 1) * US_GH[1] + + US_PH[0] ^ (1 - US_THETA) = (1 - US_XIH) * US_PHTILDE[0] ^ (1 - US_THETA) + US_XIH * (US_PH[-1] / US_PIC[0]) ^ (1 - US_THETA) * (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * US_PIH[-1] ^ US_CHIH) ^ (1 - US_THETA) + + US_PIH[0] = US_PIC[0] * US_PH[0] / US_PH[-1] + + EA_PIMTILDE[0] / EA_PIM[0] = US_THETA / (US_THETA - 1) * US_FX[0] / US_GX[0] + + US_FX[0] = EA_SIZE * EA_IM[0] * US_MC[0] / US_SIZE + US_BETA * US_LAMBDAI[1] * US_XIX / US_LAMBDAI[0] * (EA_PIIM[1] / (EA_PIIM[0] ^ US_CHIX * US_PI4TARGET ^ (0.25 * (1 - US_CHIX)))) ^ US_THETA * US_FX[1] + + US_GX[0] = EA_PIM[0] * EA_SIZE * EA_IM[0] * USEA_RER[0] / US_SIZE + US_BETA * US_LAMBDAI[1] * US_XIX / US_LAMBDAI[0] * (EA_PIIM[1] / (EA_PIIM[0] ^ US_CHIX * US_PI4TARGET ^ (0.25 * (1 - US_CHIX)))) ^ (US_THETA - 1) * US_GX[1] + + EA_PIM[0] ^ (1 - US_THETA) = (1 - US_XIX) * EA_PIMTILDE[0] ^ (1 - US_THETA) + US_XIX * (EA_PIM[-1] / EA_PIC[0]) ^ (1 - US_THETA) * (EA_PIIM[-1] ^ US_CHIX * EA_PI4TARGET ^ (0.25 * (1 - US_CHIH))) ^ (1 - US_THETA) + + EA_PIIM[0] = EA_PIC[0] * EA_PIM[0] / EA_PIM[-1] + + USEA_RER[0] = US_RER / EA_RER[0] + + US_QC[0] ^ ((US_MUC - 1) / US_MUC) = US_NUC ^ (1 / US_MUC) * US_HC[0] ^ (1 - 1 / US_MUC) + (1 - US_NUC) ^ (1 / US_MUC) * ((1 - US_GAMMAIMC[0]) * US_IMC[0]) ^ (1 - 1 / US_MUC) + + 1 = US_NUC * US_PH[0] ^ (1 - US_MUC) + (1 - US_NUC) * (US_PIM[0] / US_GAMMAIMCDAG[0]) ^ (1 - US_MUC) + + US_HC[0] = US_QC[0] * US_NUC * US_PH[0] ^ (-US_MUC) + + US_GAMMAIMC[0] = US_GAMMAIMC1 / 2 * (US_IMC[0] / US_QC[0] / (US_IMC[-1] / US_QC[-1]) - 1) ^ 2 + + US_GAMMAIMCDAG[0] = 1 - US_GAMMAIMC[0] - (US_IMC[0] / US_QC[0] / (US_IMC[-1] / US_QC[-1]) - 1) * US_IMC[0] * US_GAMMAIMC1 / US_QC[0] / (US_IMC[-1] / US_QC[-1]) + + US_QI[0] ^ ((US_MUI - 1) / US_MUI) = US_NUI ^ (1 / US_MUI) * US_HI[0] ^ (1 - 1 / US_MUI) + (1 - US_NUI) ^ (1 / US_MUI) * ((1 - US_GAMMAIMI[0]) * US_IMI[0]) ^ (1 - 1 / US_MUI) + + US_PI[0] ^ (1 - US_MUI) = US_NUI * US_PH[0] ^ (1 - US_MUI) + (1 - US_NUI) * (US_PIM[0] / US_GAMMAIMIDAG[0]) ^ (1 - US_MUI) + + US_HI[0] = US_QI[0] * US_NUI * (US_PH[0] / US_PI[0]) ^ (-US_MUI) + + US_GAMMAIMI[0] = US_GAMMAIMI1 / 2 * (US_IMI[0] / US_QI[0] / (US_IMI[-1] / US_QI[-1]) - 1) ^ 2 + + US_GAMMAIMIDAG[0] = 1 - US_GAMMAIMI[0] - US_IMI[0] * US_GAMMAIMI1 * (US_IMI[0] / US_QI[0] / (US_IMI[-1] / US_QI[0]) - 1) / US_QI[0] / (US_IMI[-1] / US_QI[-1]) + + US_PH[-1] * US_G[-1] + US_TR[-1] + US_B[-1] * US_PIC[-1] ^ (-1) + US_PIC[-1] ^ (-1) * AUX_ENDO_LAG_165_1[-1] = US_TAUC[-1] * US_C[-1] + (US_TAUN[-1] + US_TAUWH[-1]) * (US_WI[-1] * US_NDI[-1] + US_WJ[-1] * US_NDJ[-1]) + US_TAUWF[-1] * US_W[-1] * US_ND[-1] + US_TAUK[-1] * (US_RK[-1] * US_U[-1] - (US_DELTA + US_GAMMAU[-1]) * US_PI[-1]) * US_K[-1] + US_TAUD[-1] * US_D[-1] + US_T[-1] + US_R[-1] ^ (-1) * US_B[0] + US_M[-1] + + US_PH[0] * US_G[0] = US_GY[0] * US_PYBAR * US_YBAR + + US_TR[0] = US_PYBAR * US_YBAR * US_TRY[0] + + US_T[0] / (US_PYBAR * US_YBAR) = US_PHITB * (US_B[0] / (US_PYBAR * US_YBAR) - US_BYTARGET) + + US_TI[0] = US_T[0] * US_UPSILONT + + US_TRI[0] = US_TR[0] * US_UPSILONTR + + US_PIC4[0] = US_PIC[0] * US_PIC[-1] * AUX_ENDO_LAG_177_1[-1] * AUX_ENDO_LAG_177_2[-1] + + US_RR[0] - 1 = US_R[0] / US_PIC[1] - 1 + + US_C[0] = US_CI[0] * (1 - US_OMEGA) + US_CJ[0] * US_OMEGA + + US_M[0] = US_MI[0] * (1 - US_OMEGA) + US_MJ[0] * US_OMEGA + + US_K[0] = US_KI[0] * (1 - US_OMEGA) + + US_I[0] = US_II[0] * (1 - US_OMEGA) + + US_TRJ[0] = US_TR[0] / US_OMEGA - (1 - US_OMEGA) * US_TRI[0] / US_OMEGA + + US_TJ[0] = US_T[0] / US_OMEGA - (1 - US_OMEGA) * US_TI[0] / US_OMEGA + + US_GAMMAV[0] = (1 - US_OMEGA) * US_CI[0] * US_GAMMAVI[0] + US_OMEGA * US_CJ[0] * US_GAMMAVJ[0] + + US_NI[0] = US_NDI[0] * US_SI[0] + + US_SI[0] = (1 - US_XII) * (US_WITILDE[0] / US_WI[0]) ^ (-US_ETAI) + US_XII * (US_WI[-1] / US_WI[0]) ^ (-US_ETAI) * (US_PIC[0] / (US_PI4TARGET ^ (0.25 * (1 - US_CHII)) * US_PIC[-1] ^ US_CHII)) ^ US_ETAI * US_SI[-1] + + US_NJ[0] = US_NDJ[0] * US_SJ[0] + + US_SJ[0] = (1 - US_XIJ) * (US_WJTILDE[0] / US_WJ[0]) ^ (-US_ETAJ) + US_XIJ * (US_WJ[-1] / US_WJ[0]) ^ (-US_ETAJ) * (US_PIC[0] / (US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)) * US_PIC[-1] ^ US_CHIJ)) ^ US_ETAJ * US_SJ[-1] + + US_U[0] * US_K[0] = US_KD[0] + + US_YS[0] = US_H[0] * US_SH[0] + EA_SIZE * EA_IM[0] * US_SX[0] / US_SIZE + + US_H[0] = US_HI[0] + US_HC[0] + US_G[0] + + US_IM[0] = US_IMC[0] + US_IMI[0] + + US_SH[0] = (1 - US_XIH) * (US_PHTILDE[0] / US_PH[0]) ^ (-US_THETA) + US_XIH * (US_PIH[0] / (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * US_PIH[-1] ^ US_CHIH)) ^ US_THETA * US_SH[-1] + + US_SX[0] = (1 - US_XIX) * (EA_PIMTILDE[0] / EA_PIM[0]) ^ (-US_THETA) + US_XIX * (EA_PIIM[0] / (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * EA_PIIM[-1] ^ US_CHIX)) ^ US_THETA * US_SX[-1] + + US_QC[0] = US_C[0] + US_GAMMAV[0] + + US_QI[0] = US_I[0] + US_GAMMAU[0] * US_K[0] + + US_Y[0] * US_PY[0] = US_QC[0] + EA_PIM[0] * EA_SIZE * EA_IM[0] * USEA_RER[0] / US_SIZE + US_PH[0] * US_G[0] + US_PI[0] * US_QI[0] - US_PIM[0] * ((1 - US_GAMMAIMC[0]) * US_IMC[0] / US_GAMMAIMCDAG[0] + (1 - US_GAMMAIMI[0]) * US_IMI[0] / US_GAMMAIMIDAG[0]) + + US_Y[0] = US_YS[0] + + log(US_Z[0]) = (1 - US_RHOZ) * log(US_ZBAR) + US_RHOZ * log(US_Z[-1]) + sigma__US_Z * US_EPSZ[x] + + US_GY[0] = (1 - US_RHOG) * US_GYBAR + US_RHOG * US_GY[-1] + sigma__US_G * US_EPSG[x] + + US_TRY[0] = (1 - US_RHOTR) * US_TRYBAR + US_RHOTR * US_TRY[-1] + sigma__US_TR * US_EPSTR[x] + + US_TAUC[0] = (1 - US_RHOTAUC) * US_TAUCBAR + US_TAUC[-1] * US_RHOTAUC + sigma__US_TAUC * US_EPSTAUC[x] + + US_TAUD[0] = (1 - US_RHOTAUD) * US_TAUDBAR + US_TAUD[-1] * US_RHOTAUD + sigma__US_TAUD * US_EPSTAUD[x] + + US_TAUK[0] = US_TAUKBAR * (1 - US_RHOTAUK) + US_TAUK[-1] * US_RHOTAUK + sigma__US_TAUK * US_EPSTAUK[x] + + US_TAUN[0] = (1 - US_RHOTAUN) * US_TAUNBAR + US_TAUN[-1] * US_RHOTAUN + sigma__US_TAUN * US_EPSTAUN[x] + + US_TAUWH[0] = (1 - US_RHOTAUWH) * US_TAUWHBAR + US_TAUWH[-1] * US_RHOTAUWH + sigma__US_TAUWH * US_EPSTAUWH[x] + + US_TAUWF[0] = (1 - US_RHOTAUWF) * US_TAUWFBAR + US_TAUWF[-1] * US_RHOTAUWF + sigma__US_TAUWF * US_EPSTAUWF[x] + + US_CY[0] = US_C[0] / (US_Y[0] * US_PY[0]) + + US_IY[0] = US_PI[0] * US_I[0] / (US_Y[0] * US_PY[0]) + + US_IMY[0] = US_PIM[0] * US_IM[0] / (US_Y[0] * US_PY[0]) + + US_IMCY[0] = US_PIM[0] * US_IMC[0] / (US_Y[0] * US_PY[0]) + + US_IMIY[0] = US_PIM[0] * US_IMI[0] / (US_Y[0] * US_PY[0]) + + US_BY[0] = US_B[0] / (US_PYBAR * US_YBAR) + + US_TY[0] = US_T[0] / (US_PYBAR * US_YBAR) + + US_YGAP[0] = US_Y[0] / US_YBAR - 1 + + US_YGROWTH[0] = US_Y[0] / US_Y[-1] + + US_YSHARE[0] = US_Y[0] * US_SIZE * US_PY[0] / US_RER / (EA_Y[0] * EA_PY[0] * EA_SIZE / EA_RER[0] + US_Y[0] * US_SIZE * US_PY[0] / US_RER) + + US_EPSILONM[0] = ( - 0.125) / (US_R[0] * (US_R[0] + US_R[0] * US_GAMMAV2 - 1)) + + 1 = US_R[0] * EA_BETA * EA_LAMBDAI[1] * (1 - EA_GAMMAB[0]) / EA_LAMBDAI[0] * EA_RERDEP[1] / US_PIC[1] + + EA_GAMMAB[0] = EA_GAMMAB1 * (exp(EA_RER[0] * EA_BF[0] / US_PIC[0] / (EA_Y[0] * EA_PY[0]) - EA_BFYTARGET) - 1) - EA_RP[0] + + EA_RP[0] = EA_RHORP * EA_RP[-1] + sigma__EA_RP * EA_EPSRP[x] + + EA_RERDEP[0] = EA_RER[0] / EA_RER[-1] + + EA_TOT[0] = EA_PIM[0] / (US_PIM[0] * EA_RER[0]) + + EA_TB[0] = US_PIM[0] * US_SIZE * US_IM[0] * EA_RER[0] / EA_SIZE - EA_PIM[0] * EA_IM[0] + + EA_BF[0] / US_R[-1] = EA_BF[-1] + EA_TB[-1] / EA_RER[-1] + + EA_SIZE * EA_BF[0] + US_SIZE * US_BF[0] = 0 + + AUX_ENDO_LAG_51_1[0] = EA_M[-1] + + AUX_ENDO_LAG_63_1[0] = EA_PIC[-1] + + AUX_ENDO_LAG_63_2[0] = AUX_ENDO_LAG_63_1[-1] + + AUX_ENDO_LAG_165_1[0] = US_M[-1] + + AUX_ENDO_LAG_177_1[0] = US_PIC[-1] + + AUX_ENDO_LAG_177_2[0] = AUX_ENDO_LAG_177_1[-1] + +end + + +@parameters NAWM_EAUS_2008 begin + sigma__EA_R = 1.0 + + sigma__US_R = 1.0 + + sigma__EA_Z = 1.0 + + sigma__EA_G = 1.0 + + sigma__EA_TR = 1.0 + + sigma__EA_TAUC = 1.0 + + sigma__EA_TAUD = 1.0 + + sigma__EA_TAUK = 1.0 + + sigma__EA_TAUN = 1.0 + + sigma__EA_TAUWH = 1.0 + + sigma__EA_TAUWF = 1.0 + + sigma__US_Z = 1.0 + + sigma__US_G = 1.0 + + sigma__US_TR = 1.0 + + sigma__US_TAUC = 1.0 + + sigma__US_TAUD = 1.0 + + sigma__US_TAUK = 1.0 + + sigma__US_TAUN = 1.0 + + sigma__US_TAUWH = 1.0 + + sigma__US_TAUWF = 1.0 + + sigma__EA_RP = 1.0 + + EA_SIZE = 0.4194 + + EA_OMEGA = 0.25 + + EA_BETA = 0.992638 + + EA_SIGMA = 2.0 + + EA_KAPPA = 0.6 + + EA_ZETA = 2.0 + + EA_DELTA = 0.025 + + EA_ETA = 6.0 + + EA_ETAI = 6.0 + + EA_ETAJ = 6.0 + + EA_XII = 0.75 + + EA_XIJ = 0.75 + + EA_CHII = 0.75 + + EA_CHIJ = 0.75 + + EA_ALPHA = 0.3 + + EA_THETA = 6.0 + + EA_XIH = 0.9 + + EA_XIX = 0.3 + + EA_CHIH = 0.5 + + EA_CHIX = 0.5 + + EA_NUC = 0.919622 + + EA_MUC = 1.5 + + EA_NUI = 0.418629 + + EA_MUI = 1.5 + + EA_GAMMAV1 = 0.289073 + + EA_GAMMAV2 = 0.150339 + + EA_GAMMAI1 = 3.0 + + EA_GAMMAU2 = 0.007 + + EA_GAMMAIMC1 = 2.5 + + EA_GAMMAIMI1 = 0.0 + + EA_GAMMAB1 = 0.01 + + EA_BYTARGET = 2.4 + + EA_PHITB = 0.1 + + EA_GYBAR = 0.18 + + EA_TRYBAR = 0.195161 + + EA_TAUCBAR = 0.183 + + EA_TAUKBAR = 0.184123 + + EA_TAUNBAR = 0.122 + + EA_TAUWHBAR = 0.118 + + EA_TAUWFBAR = 0.219 + + EA_UPSILONT = 1.2 + + EA_UPSILONTR = 0.6666666666666666 + + EA_PI4TARGET = 1.02 + + EA_PHIRR = 0.95 + + EA_PHIRPI = 2.0 + + EA_PHIRGY = 0.1 + + EA_BFYTARGET = 0.0 + + EA_RHOZ = 0.9 + + EA_RHOG = 0.9 + + EA_RHOTR = 0.9 + + EA_RHOTAUC = 0.9 + + EA_RHOTAUK = 0.9 + + EA_RHOTAUN = 0.9 + + EA_RHOTAUD = 0.9 + + EA_RHOTAUWH = 0.9 + + EA_RHOTAUWF = 0.9 + + EA_PYBAR = 1.00645740523434 + + EA_YBAR = 3.62698111871356 + + EA_RHORP = 0.9 + + EA_PIBAR = 0.961117319822928 + + EA_PSIBAR = 0.725396223742712 + + EA_QBAR = 0.961117319822928 + + EA_TAUDBAR = 0.0 + + EA_ZBAR = 1.0 + + US_SIZE = 0.5806 + + US_OMEGA = 0.25 + + US_BETA = 0.992638 + + US_SIGMA = 2.0 + + US_KAPPA = 0.6 + + US_ZETA = 2.0 + + US_DELTA = 0.025 + + US_ETA = 6.0 + + US_ETAI = 6.0 + + US_ETAJ = 6.0 + + US_XII = 0.75 + + US_XIJ = 0.75 + + US_CHII = 0.75 + + US_CHIJ = 0.75 + + US_ALPHA = 0.3 + + US_THETA = 6.0 + + US_XIH = 0.9 + + US_XIX = 0.3 + + US_CHIH = 0.5 + + US_CHIX = 0.5 + + US_NUC = 0.899734 + + US_MUC = 1.5 + + US_NUI = 0.673228 + + US_MUI = 1.5 + + US_GAMMAV1 = 0.028706 + + US_GAMMAV2 = 0.150339 + + US_GAMMAI1 = 3.0 + + US_GAMMAU2 = 0.007 + + US_GAMMAIMC1 = 2.5 + + US_GAMMAIMI1 = 0.0 + + US_BYTARGET = 2.4 + + US_PHITB = 0.1 + + US_GYBAR = 0.16 + + US_TRYBAR = 0.079732 + + US_TAUCBAR = 0.077 + + US_TAUKBAR = 0.184123 + + US_TAUNBAR = 0.154 + + US_TAUWHBAR = 0.071 + + US_TAUWFBAR = 0.071 + + US_UPSILONT = 1.2 + + US_UPSILONTR = 0.6666666666666666 + + US_PI4TARGET = 1.02 + + US_PHIRR = 0.95 + + US_PHIRPI = 2.0 + + US_PHIRGY = 0.1 + + US_RHOZ = 0.9 + + US_RHOG = 0.9 + + US_RHOTR = 0.9 + + US_RHOTAUC = 0.9 + + US_RHOTAUK = 0.9 + + US_RHOTAUN = 0.9 + + US_RHOTAUD = 0.9 + + US_RHOTAUWH = 0.9 + + US_RHOTAUWF = 0.9 + + US_PYBAR = 0.992282866960427 + + US_TAUDBAR = 0.0 + + US_YBAR = 3.92445610588497 + + US_PIBAR = 1.01776829477927 + + US_PSIBAR = 0.784891221176995 + + US_QBAR = 1.01776829477927 + + US_ZBAR = 1.0 + + US_RER = 1.0 + + EA_RRSTAR = 1/EA_BETA + + US_RRSTAR = 1/US_BETA + + EA_interest_EXOG = EA_BETA ^ -1 * EA_PI4TARGET ^ (1 / 4) + + US_interest_EXOG = US_BETA ^ -1 * US_PI4TARGET ^ (1 / 4) + +end + diff --git a/test/NAWM_EAUS_2008.mod b/test/NAWM_EAUS_2008.mod new file mode 100644 index 000000000..ea1fdf559 --- /dev/null +++ b/test/NAWM_EAUS_2008.mod @@ -0,0 +1,865 @@ +var +EAUS_RER EA_B EA_BF EA_BY EA_C EA_CI EA_CJ EA_CY EA_D EA_EPSILONM EA_FH EA_FI EA_FJ EA_FX EA_G EA_GAMMAB EA_GAMMAI EA_GAMMAIDER EA_GAMMAIMC EA_GAMMAIMCDAG EA_GAMMAIMI EA_GAMMAIMIDAG EA_GAMMAU EA_GAMMAUDER EA_GAMMAV EA_GAMMAVI EA_GAMMAVIDER EA_GAMMAVJ EA_GAMMAVJDER EA_GH EA_GI EA_GJ EA_GX EA_GY EA_H EA_HC EA_HI EA_I EA_II EA_IM EA_IMC EA_IMCY EA_IMI EA_IMIY EA_IMY EA_IY EA_K EA_KD EA_KI EA_LAMBDAI EA_LAMBDAJ EA_M EA_MC EA_MI EA_MJ EA_ND EA_NDI EA_NDJ EA_NI EA_NJ EA_PH EA_PHTILDE EA_PI EA_PIC EA_PIC4 EA_PIH EA_PIIM EA_PIM EA_PIMTILDE EA_PY EA_Q EA_QC EA_QI EA_R EA_RER EA_RERDEP EA_RK EA_RP EA_RR EA_SH EA_SI EA_SJ EA_SX EA_T EA_TAUC EA_TAUD EA_TAUK EA_TAUN EA_TAUWF EA_TAUWH EA_TB EA_TI EA_TJ EA_TOT EA_TR EA_TRI EA_TRJ EA_TRY EA_TY EA_U EA_UTILI EA_UTILJ EA_VI EA_VJ EA_W EA_WI EA_WITILDE EA_WJ EA_WJTILDE EA_Y EA_YGAP EA_YGROWTH EA_YS EA_YSHARE EA_Z USEA_RER US_B US_BF US_BY US_C US_CI US_CJ US_CY US_D US_EPSILONM US_FH US_FI US_FJ US_FX US_G US_GAMMAI US_GAMMAIDER US_GAMMAIMC US_GAMMAIMCDAG US_GAMMAIMI US_GAMMAIMIDAG US_GAMMAU US_GAMMAUDER US_GAMMAV US_GAMMAVI US_GAMMAVIDER US_GAMMAVJ US_GAMMAVJDER US_GH US_GI US_GJ US_GX US_GY US_H US_HC US_HI US_I US_II US_IM US_IMC US_IMCY US_IMI US_IMIY US_IMY US_IY US_K US_KD US_KI US_LAMBDAI US_LAMBDAJ US_M US_MC US_MI US_MJ US_ND US_NDI US_NDJ US_NI US_NJ US_PH US_PHTILDE US_PI US_PIC US_PIC4 US_PIH US_PIIM US_PIM US_PIMTILDE US_PY US_Q US_QC US_QI US_R US_RK US_RR US_SH US_SI US_SJ US_SX US_T US_TAUC US_TAUD US_TAUK US_TAUN US_TAUWF US_TAUWH US_TI US_TJ US_TR US_TRI US_TRJ US_TRY US_TY US_U US_UTILI US_UTILJ US_VI US_VJ US_W US_WI US_WITILDE US_WJ US_WJTILDE US_Y US_YGAP US_YGROWTH US_YS US_YSHARE US_Z ; + +varexo +EA_EPSG EA_EPSR EA_EPSRP EA_EPSTAUC EA_EPSTAUD EA_EPSTAUK EA_EPSTAUN EA_EPSTAUWF EA_EPSTAUWH EA_EPSTR EA_EPSZ US_EPSG US_EPSR US_EPSTAUC US_EPSTAUD US_EPSTAUK US_EPSTAUN US_EPSTAUWF US_EPSTAUWH US_EPSTR US_EPSZ ; + +parameters +EA_ALPHA EA_BETA EA_BFYTARGET EA_BYTARGET EA_CHIH EA_CHII EA_CHIJ EA_CHIX EA_DELTA EA_ETA EA_ETAI EA_ETAJ EA_GAMMAB1 EA_GAMMAI1 EA_GAMMAIMC1 EA_GAMMAIMI1 EA_GAMMAU2 EA_GAMMAV1 EA_GAMMAV2 EA_GYBAR EA_KAPPA EA_MUC EA_MUI EA_NUC EA_NUI EA_OMEGA EA_PHIRGY EA_PHIRPI EA_PHIRR EA_PHITB EA_PI4TARGET EA_PIBAR EA_PSIBAR EA_PYBAR EA_QBAR EA_RHOG EA_RHORP EA_RHOTAUC EA_RHOTAUD EA_RHOTAUK EA_RHOTAUN EA_RHOTAUWF EA_RHOTAUWH EA_RHOTR EA_RHOZ EA_RRSTAR EA_SIGMA EA_SIZE EA_TAUCBAR EA_TAUDBAR EA_TAUKBAR EA_TAUNBAR EA_TAUWFBAR EA_TAUWHBAR EA_THETA EA_TRYBAR EA_UPSILONT EA_UPSILONTR EA_XIH EA_XII EA_XIJ EA_XIX EA_YBAR EA_ZBAR EA_ZETA US_ALPHA US_BETA US_BYTARGET US_CHIH US_CHII US_CHIJ US_CHIX US_DELTA US_ETA US_ETAI US_ETAJ US_GAMMAI1 US_GAMMAIMC1 US_GAMMAIMI1 US_GAMMAU2 US_GAMMAV1 US_GAMMAV2 US_GYBAR US_KAPPA US_MUC US_MUI US_NUC US_NUI US_OMEGA US_PHIRGY US_PHIRPI US_PHIRR US_PHITB US_PI4TARGET US_PIBAR US_PSIBAR US_PYBAR US_QBAR US_RER US_RHOG US_RHOTAUC US_RHOTAUD US_RHOTAUK US_RHOTAUN US_RHOTAUWF US_RHOTAUWH US_RHOTR US_RHOZ US_RRSTAR US_SIGMA US_SIZE US_TAUCBAR US_TAUDBAR US_TAUKBAR US_TAUNBAR US_TAUWFBAR US_TAUWHBAR US_THETA US_TRYBAR US_UPSILONT US_UPSILONTR US_XIH US_XII US_XIJ US_XIX US_YBAR US_ZBAR US_ZETA sigma__EA_G sigma__EA_R sigma__EA_RP sigma__EA_TAUC sigma__EA_TAUD sigma__EA_TAUK sigma__EA_TAUN sigma__EA_TAUWF sigma__EA_TAUWH sigma__EA_TR sigma__EA_Z sigma__US_G sigma__US_R sigma__US_TAUC sigma__US_TAUD sigma__US_TAUK sigma__US_TAUN sigma__US_TAUWF sigma__US_TAUWH sigma__US_TR sigma__US_Z ; + +% Parameter definitions: + sigma__EA_R = 1.0; + sigma__US_R = 1.0; + sigma__EA_Z = 1.0; + sigma__EA_G = 1.0; + sigma__EA_TR = 1.0; + sigma__EA_TAUC = 1.0; + sigma__EA_TAUD = 1.0; + sigma__EA_TAUK = 1.0; + sigma__EA_TAUN = 1.0; + sigma__EA_TAUWH = 1.0; + sigma__EA_TAUWF = 1.0; + sigma__US_Z = 1.0; + sigma__US_G = 1.0; + sigma__US_TR = 1.0; + sigma__US_TAUC = 1.0; + sigma__US_TAUD = 1.0; + sigma__US_TAUK = 1.0; + sigma__US_TAUN = 1.0; + sigma__US_TAUWH = 1.0; + sigma__US_TAUWF = 1.0; + sigma__EA_RP = 1.0; + EA_SIZE = 0.4194; + EA_OMEGA = 0.25; + EA_BETA = 0.992638; + EA_SIGMA = 2.0; + EA_KAPPA = 0.6; + EA_ZETA = 2.0; + EA_DELTA = 0.025; + EA_ETA = 6.0; + EA_ETAI = 6.0; + EA_ETAJ = 6.0; + EA_XII = 0.75; + EA_XIJ = 0.75; + EA_CHII = 0.75; + EA_CHIJ = 0.75; + EA_ALPHA = 0.3; + EA_THETA = 6.0; + EA_XIH = 0.9; + EA_XIX = 0.3; + EA_CHIH = 0.5; + EA_CHIX = 0.5; + EA_NUC = 0.919622; + EA_MUC = 1.5; + EA_NUI = 0.418629; + EA_MUI = 1.5; + EA_GAMMAV1 = 0.289073; + EA_GAMMAV2 = 0.150339; + EA_GAMMAI1 = 3.0; + EA_GAMMAU2 = 0.007; + EA_GAMMAIMC1 = 2.5; + EA_GAMMAIMI1 = 0.0; + EA_GAMMAB1 = 0.01; + EA_BYTARGET = 2.4; + EA_PHITB = 0.1; + EA_GYBAR = 0.18; + EA_TRYBAR = 0.195161; + EA_TAUCBAR = 0.183; + EA_TAUKBAR = 0.184123; + EA_TAUNBAR = 0.122; + EA_TAUWHBAR = 0.118; + EA_TAUWFBAR = 0.219; + EA_UPSILONT = 1.2; + EA_UPSILONTR = 0.6666666666666666; + EA_PI4TARGET = 1.02; + EA_PHIRR = 0.95; + EA_PHIRPI = 2.0; + EA_PHIRGY = 0.1; + EA_BFYTARGET = 0.0; + EA_RHOZ = 0.9; + EA_RHOG = 0.9; + EA_RHOTR = 0.9; + EA_RHOTAUC = 0.9; + EA_RHOTAUK = 0.9; + EA_RHOTAUN = 0.9; + EA_RHOTAUD = 0.9; + EA_RHOTAUWH = 0.9; + EA_RHOTAUWF = 0.9; + EA_PYBAR = 1.00645740523434; + EA_YBAR = 3.62698111871356; + EA_RHORP = 0.9; + EA_PIBAR = 0.961117319822928; + EA_PSIBAR = 0.725396223742712; + EA_QBAR = 0.961117319822928; + EA_TAUDBAR = 0.0; + EA_ZBAR = 1.0; + US_SIZE = 0.5806; + US_OMEGA = 0.25; + US_BETA = 0.992638; + US_SIGMA = 2.0; + US_KAPPA = 0.6; + US_ZETA = 2.0; + US_DELTA = 0.025; + US_ETA = 6.0; + US_ETAI = 6.0; + US_ETAJ = 6.0; + US_XII = 0.75; + US_XIJ = 0.75; + US_CHII = 0.75; + US_CHIJ = 0.75; + US_ALPHA = 0.3; + US_THETA = 6.0; + US_XIH = 0.9; + US_XIX = 0.3; + US_CHIH = 0.5; + US_CHIX = 0.5; + US_NUC = 0.899734; + US_MUC = 1.5; + US_NUI = 0.673228; + US_MUI = 1.5; + US_GAMMAV1 = 0.028706; + US_GAMMAV2 = 0.150339; + US_GAMMAI1 = 3.0; + US_GAMMAU2 = 0.007; + US_GAMMAIMC1 = 2.5; + US_GAMMAIMI1 = 0.0; + US_BYTARGET = 2.4; + US_PHITB = 0.1; + US_GYBAR = 0.16; + US_TRYBAR = 0.079732; + US_TAUCBAR = 0.077; + US_TAUKBAR = 0.184123; + US_TAUNBAR = 0.154; + US_TAUWHBAR = 0.071; + US_TAUWFBAR = 0.071; + US_UPSILONT = 1.2; + US_UPSILONTR = 0.6666666666666666; + US_PI4TARGET = 1.02; + US_PHIRR = 0.95; + US_PHIRPI = 2.0; + US_PHIRGY = 0.1; + US_RHOZ = 0.9; + US_RHOG = 0.9; + US_RHOTR = 0.9; + US_RHOTAUC = 0.9; + US_RHOTAUK = 0.9; + US_RHOTAUN = 0.9; + US_RHOTAUD = 0.9; + US_RHOTAUWH = 0.9; + US_RHOTAUWF = 0.9; + US_PYBAR = 0.992282866960427; + US_TAUDBAR = 0.0; + US_YBAR = 3.92445610588497; + US_PIBAR = 1.01776829477927; + US_PSIBAR = 0.784891221176995; + US_QBAR = 1.01776829477927; + US_ZBAR = 1.0; + US_RER = 1.0; + EA_RRSTAR = 1 / EA_BETA; + US_RRSTAR = 1 / US_BETA; + EA_interest_EXOG = EA_BETA ^ -1 * EA_PI4TARGET ^ (1 / 4); + US_interest_EXOG = US_BETA ^ -1 * US_PI4TARGET ^ (1 / 4); + +model; + EA_R(0) ^ 4 - 1 = EA_PHIRR * (EA_R(-1) ^ 4 - 1) + (1 - EA_PHIRR) * ((EA_RRSTAR ^ 4 * EA_PI4TARGET - 1) + EA_PHIRPI * (EA_PIC4(0) - EA_PI4TARGET)) + EA_PHIRGY * (EA_Y(0) / EA_Y(-1) - 1) + sigma__EA_R * EA_EPSR; + + US_R(0) ^ 4 - 1 = US_PHIRR * (US_R(-1) ^ 4 - 1) + (1 - US_PHIRR) * ((US_RRSTAR ^ 4 * US_PI4TARGET - 1) + US_PHIRPI * (US_PIC4(0) - US_PI4TARGET)) + US_PHIRGY * (US_Y(0) / US_Y(-1) - 1) + sigma__US_R * US_EPSR; + + EA_UTILI(0) = ((1 / (1 - EA_SIGMA)) * (EA_CI(0) - EA_KAPPA * EA_CI(-1)) ^ (1 - EA_SIGMA) - (1 / (1 + EA_ZETA)) * EA_NI(0) ^ (1 + EA_ZETA)) + EA_BETA * EA_UTILI(1); + + EA_LAMBDAI(0) * (1 + EA_TAUC(0) + EA_GAMMAVI(0) + EA_VI(0) * EA_GAMMAVIDER(0)) = (EA_CI(0) - EA_KAPPA * EA_CI(-1)) ^ -EA_SIGMA; + + EA_R(0) = ((EA_LAMBDAI(0) * EA_BETA ^ -1) / EA_LAMBDAI(1)) * EA_PIC(1); + + EA_GAMMAVIDER(0) * EA_VI(0) ^ 2 = 1 - (EA_BETA * EA_LAMBDAI(1)) / (EA_LAMBDAI(0) * EA_PIC(1)); + + EA_VI(0) = (EA_CI(0) * (1 + EA_TAUC(0))) / EA_MI(0); + + EA_GAMMAVI(0) = (EA_VI(0) * EA_GAMMAV1 + EA_GAMMAV2 / EA_VI(0)) - 2 * (EA_GAMMAV1 * EA_GAMMAV2) ^ 0.5; + + EA_GAMMAVIDER(0) = EA_GAMMAV1 - EA_GAMMAV2 * EA_VI(0) ^ -2; + + EA_KI(0) = (1 - EA_DELTA) * EA_KI(-1) + (1 - EA_GAMMAI(-1)) * EA_II(-1); + + EA_GAMMAI(0) = (EA_GAMMAI1 / 2) * (EA_II(0) / EA_II(-1) - 1) ^ 2; + + EA_GAMMAIDER(0) = (EA_GAMMAI1 * (EA_II(0) / EA_II(-1) - 1)) / EA_II(-1); + + EA_GAMMAU(0) = ((((EA_DELTA + EA_BETA ^ -1) - 1) * EA_QBAR - EA_DELTA * EA_TAUKBAR * EA_PIBAR) / (EA_PIBAR * (1 - EA_TAUKBAR))) * (EA_U(0) - 1) + (EA_GAMMAU2 / 2) * (EA_U(0) - 1) ^ 2; + + EA_GAMMAUDER(0) = (((EA_DELTA + EA_BETA ^ -1) - 1) * EA_QBAR - EA_DELTA * EA_TAUKBAR * EA_PIBAR) / (EA_PIBAR * (1 - EA_TAUKBAR)) + (EA_U(0) - 1) * EA_GAMMAU2; + + EA_RK(0) = EA_GAMMAUDER(0) * EA_PI(0); + + EA_PI(0) = EA_Q(0) * ((1 - EA_GAMMAI(0)) - EA_II(0) * EA_GAMMAIDER(0)) + (((EA_BETA * EA_LAMBDAI(1)) / EA_LAMBDAI(0)) * EA_Q(1) * EA_GAMMAIDER(1) * EA_II(1) ^ 2) / EA_II(0); + + EA_Q(0) = ((EA_BETA * EA_LAMBDAI(1)) / EA_LAMBDAI(0)) * ((1 - EA_TAUK(1)) * (EA_RK(1) * EA_U(1) - EA_GAMMAU(1) * EA_PI(1)) + EA_PI(1) * EA_DELTA * EA_TAUK(1) + (1 - EA_DELTA) * EA_Q(1)); + + EA_WITILDE(0) ^ (1 + EA_ZETA * EA_ETAI) = ((EA_ETAI / (EA_ETAI - 1)) * EA_FI(0)) / EA_GI(0); + + EA_FI(0) = EA_WI(0) ^ ((1 + EA_ZETA) * EA_ETAI) * EA_NDI(0) ^ (1 + EA_ZETA) + EA_BETA * EA_XII * (EA_PIC(1) / (EA_PIC(0) ^ EA_CHII * EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)))) ^ ((1 + EA_ZETA) * EA_ETAI) * EA_FI(1); + + EA_GI(0) = EA_NDI(0) * EA_LAMBDAI(0) * ((1 - EA_TAUN(0)) - EA_TAUWH(0)) * EA_WI(0) ^ EA_ETAI + EA_BETA * EA_XII * (EA_PIC(1) / (EA_PIC(0) ^ EA_CHII * EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)))) ^ (EA_ETAI - 1) * EA_GI(1); + + EA_WI(0) ^ (1 - EA_ETAI) = (1 - EA_XII) * EA_WITILDE(0) ^ (1 - EA_ETAI) + EA_XII * EA_WI(-1) ^ (1 - EA_ETAI) * ((EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)) * EA_PIC(-1) ^ EA_CHII) / EA_PIC(0)) ^ (1 - EA_ETAI); + + EA_UTILJ(0) = ((1 / (1 - EA_SIGMA)) * (EA_CJ(0) - EA_KAPPA * EA_CJ(-1)) ^ (1 - EA_SIGMA) - (1 / (1 + EA_ZETA)) * EA_NJ(0) ^ (1 + EA_ZETA)) + EA_BETA * EA_UTILJ(1); + + EA_CJ(0) * (1 + EA_TAUC(0) + EA_GAMMAVJ(0)) + EA_MJ(0) = ((EA_NJ(0) * ((1 - EA_TAUN(0)) - EA_TAUWH(0)) * EA_WJ(0) + EA_TRJ(0)) - EA_TJ(0)) + EA_MJ(-1) * EA_PIC(0) ^ -1; + + EA_LAMBDAJ(0) * (1 + EA_TAUC(0) + EA_GAMMAVJ(0) + EA_VJ(0) * EA_GAMMAVJDER(0)) = (EA_CJ(0) - EA_KAPPA * EA_CJ(-1)) ^ -EA_SIGMA; + + EA_GAMMAVJDER(0) * EA_VJ(0) ^ 2 = 1 - (EA_BETA * EA_LAMBDAJ(1)) / (EA_PIC(1) * EA_LAMBDAJ(0)); + + EA_VJ(0) = ((1 + EA_TAUC(0)) * EA_CJ(0)) / EA_MJ(0); + + EA_GAMMAVJ(0) = (EA_GAMMAV1 * EA_VJ(0) + EA_GAMMAV2 / EA_VJ(0)) - 2 * (EA_GAMMAV1 * EA_GAMMAV2) ^ 0.5; + + EA_GAMMAVJDER(0) = EA_GAMMAV1 - EA_GAMMAV2 * EA_VJ(0) ^ -2; + + EA_WJTILDE(0) ^ (1 + EA_ZETA * EA_ETAJ) = ((EA_ETAJ / (EA_ETAJ - 1)) * EA_FJ(0)) / EA_GJ(0); + + EA_FJ(0) = EA_WJ(0) ^ ((1 + EA_ZETA) * EA_ETAJ) * EA_NDJ(0) ^ (1 + EA_ZETA) + EA_BETA * EA_XIJ * (EA_PIC(1) / (EA_PIC(0) ^ EA_CHIJ * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)))) ^ ((1 + EA_ZETA) * EA_ETAJ) * EA_FJ(1); + + EA_GJ(0) = EA_NDJ(0) * ((1 - EA_TAUN(0)) - EA_TAUWH(0)) * EA_LAMBDAJ(0) * EA_WJ(0) ^ EA_ETAJ + EA_BETA * EA_XIJ * (EA_PIC(1) / (EA_PIC(0) ^ EA_CHIJ * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)))) ^ (EA_ETAJ - 1) * EA_GJ(1); + + EA_WJ(0) ^ (1 - EA_ETAJ) = (1 - EA_XIJ) * EA_WJTILDE(0) ^ (1 - EA_ETAJ) + EA_XIJ * EA_WJ(-1) ^ (1 - EA_ETAJ) * ((EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)) * EA_PIC(-1) ^ EA_CHIJ) / EA_PIC(0)) ^ (1 - EA_ETAJ); + + EA_YS(0) = EA_Z(0) * EA_KD(0) ^ EA_ALPHA * EA_ND(0) ^ (1 - EA_ALPHA) - EA_PSIBAR; + + EA_RK(0) = ((EA_ALPHA * (EA_YS(0) + EA_PSIBAR)) / EA_KD(0)) * EA_MC(0); + + EA_MC(0) = (1 / (EA_Z(0) * EA_ALPHA ^ EA_ALPHA * (1 - EA_ALPHA) ^ (1 - EA_ALPHA))) * EA_RK(0) ^ EA_ALPHA * ((1 + EA_TAUWF(0)) * EA_W(0)) ^ (1 - EA_ALPHA); + + EA_NDI(0) = EA_ND(0) * (1 - EA_OMEGA) * (EA_WI(0) / EA_W(0)) ^ -EA_ETA; + + EA_NDJ(0) = EA_ND(0) * EA_OMEGA * (EA_WJ(0) / EA_W(0)) ^ -EA_ETA; + + EA_ND(0) ^ (1 - 1 / EA_ETA) = (1 - EA_OMEGA) ^ (1 / EA_ETA) * EA_NDI(0) ^ (1 - 1 / EA_ETA) + EA_OMEGA ^ (1 / EA_ETA) * EA_NDJ(0) ^ (1 - 1 / EA_ETA); + + EA_D(0) = (EA_Y(0) * EA_PY(0) - EA_RK(0) * EA_KD(0)) - EA_ND(0) * (1 + EA_TAUWF(0)) * EA_W(0); + + EA_PHTILDE(0) / EA_PH(0) = ((EA_THETA / (EA_THETA - 1)) * EA_FH(0)) / EA_GH(0); + + EA_FH(0) = EA_MC(0) * EA_H(0) + ((EA_LAMBDAI(1) * EA_BETA * EA_XIH) / EA_LAMBDAI(0)) * (EA_PIH(1) / (EA_PIH(0) ^ EA_CHIH * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)))) ^ EA_THETA * EA_FH(1); + + EA_GH(0) = EA_PH(0) * EA_H(0) + ((EA_LAMBDAI(1) * EA_BETA * EA_XIH) / EA_LAMBDAI(0)) * (EA_PIH(1) / (EA_PIH(0) ^ EA_CHIH * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)))) ^ (EA_THETA - 1) * EA_GH(1); + + EA_PH(0) ^ (1 - EA_THETA) = (1 - EA_XIH) * EA_PHTILDE(0) ^ (1 - EA_THETA) + EA_XIH * (EA_PH(-1) / EA_PIC(0)) ^ (1 - EA_THETA) * (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * EA_PIH(-1) ^ EA_CHIH) ^ (1 - EA_THETA); + + EA_PIH(0) = (EA_PIC(0) * EA_PH(0)) / EA_PH(-1); + + US_PIMTILDE(0) / US_PIM(0) = ((EA_THETA / (EA_THETA - 1)) * EA_FX(0)) / EA_GX(0); + + EA_FX(0) = ((EA_MC(0) * US_SIZE) / EA_SIZE) * US_IM(0) + ((EA_LAMBDAI(1) * EA_BETA * EA_XIX) / EA_LAMBDAI(0)) * (US_PIIM(1) / (US_PIIM(0) ^ EA_CHIX * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIX)))) ^ EA_THETA * EA_FX(1); + + EA_GX(0) = (US_IM(0) * US_SIZE * US_PIM(0) * EAUS_RER(0)) / EA_SIZE + ((EA_LAMBDAI(1) * EA_BETA * EA_XIX) / EA_LAMBDAI(0)) * (US_PIIM(1) / (US_PIIM(0) ^ EA_CHIX * EA_PI4TARGET ^ (0.25 * (1 - EA_CHIX)))) ^ (EA_THETA - 1) * EA_GX(1); + + US_PIM(0) ^ (1 - EA_THETA) = (1 - EA_XIX) * US_PIMTILDE(0) ^ (1 - EA_THETA) + EA_XIX * (US_PIM(-1) / US_PIC(0)) ^ (1 - EA_THETA) * (US_PIIM(-1) ^ EA_CHIX * US_PI4TARGET ^ (0.25 * (1 - EA_CHIH))) ^ (1 - EA_THETA); + + US_PIIM(0) = (US_PIC(0) * US_PIM(0)) / US_PIM(-1); + + EAUS_RER(0) = EA_RER(0) / US_RER; + + EA_QC(0) ^ ((EA_MUC - 1) / EA_MUC) = EA_NUC ^ (1 / EA_MUC) * EA_HC(0) ^ (1 - 1 / EA_MUC) + (1 - EA_NUC) ^ (1 / EA_MUC) * ((1 - EA_GAMMAIMC(0)) * EA_IMC(0)) ^ (1 - 1 / EA_MUC); + + 1 = EA_NUC * EA_PH(0) ^ (1 - EA_MUC) + (1 - EA_NUC) * (EA_PIM(0) / EA_GAMMAIMCDAG(0)) ^ (1 - EA_MUC); + + EA_HC(0) = EA_QC(0) * EA_NUC * EA_PH(0) ^ -EA_MUC; + + EA_GAMMAIMC(0) = (EA_GAMMAIMC1 / 2) * ((EA_IMC(0) / EA_QC(0)) / (EA_IMC(-1) / EA_QC(-1)) - 1) ^ 2; + + EA_GAMMAIMCDAG(0) = (1 - EA_GAMMAIMC(0)) - ((EA_IMC(0) * EA_GAMMAIMC1 * ((EA_IMC(0) / EA_QC(0)) / (EA_IMC(-1) / EA_QC(-1)) - 1)) / EA_QC(0)) / (EA_IMC(-1) / EA_QC(-1)); + + EA_QI(0) ^ ((EA_MUI - 1) / EA_MUI) = EA_NUI ^ (1 / EA_MUI) * EA_HI(0) ^ (1 - 1 / EA_MUI) + (1 - EA_NUI) ^ (1 / EA_MUI) * ((1 - EA_GAMMAIMI(0)) * EA_IMI(0)) ^ (1 - 1 / EA_MUI); + + EA_PI(0) ^ (1 - EA_MUI) = EA_NUI * EA_PH(0) ^ (1 - EA_MUI) + (1 - EA_NUI) * (EA_PIM(0) / EA_GAMMAIMIDAG(0)) ^ (1 - EA_MUI); + + EA_HI(0) = EA_QI(0) * EA_NUI * (EA_PH(0) / EA_PI(0)) ^ -EA_MUI; + + EA_GAMMAIMI(0) = (EA_GAMMAIMI1 / 2) * ((EA_IMI(0) / EA_QI(0)) / (EA_IMI(-1) / EA_QI(-1)) - 1) ^ 2; + + EA_GAMMAIMIDAG(0) = (1 - EA_GAMMAIMI(0)) - ((EA_IMI(0) * EA_GAMMAIMI1 * ((EA_IMI(0) / EA_QI(0)) / (EA_IMI(-1) / EA_QI(0)) - 1)) / EA_QI(0)) / (EA_IMI(-1) / EA_QI(-1)); + + EA_PH(-1) * EA_G(-1) + EA_TR(-1) + EA_B(-1) * EA_PIC(-1) ^ -1 + EA_PIC(-1) ^ -1 * EA_M(-2) = EA_TAUC(-1) * EA_C(-1) + (EA_TAUN(-1) + EA_TAUWH(-1)) * (EA_WI(-1) * EA_NDI(-1) + EA_WJ(-1) * EA_NDJ(-1)) + EA_TAUWF(-1) * EA_W(-1) * EA_ND(-1) + EA_TAUK(-1) * (EA_RK(-1) * EA_U(-1) - (EA_DELTA + EA_GAMMAU(-1)) * EA_PI(-1)) * EA_K(-1) + EA_TAUD(-1) * EA_D(-1) + EA_T(-1) + EA_R(-1) ^ -1 * EA_B(0) + EA_M(-1); + + EA_PH(0) * EA_G(0) = EA_GY(0) * EA_PYBAR * EA_YBAR; + + EA_TR(0) = EA_YBAR * EA_PYBAR * EA_TRY(0); + + EA_T(0) / (EA_PYBAR * EA_YBAR) = EA_PHITB * (EA_B(0) / (EA_PYBAR * EA_YBAR) - EA_BYTARGET); + + EA_TI(0) = EA_T(0) * EA_UPSILONT; + + EA_TRI(0) = EA_TR(0) * EA_UPSILONTR; + + EA_PIC4(0) = EA_PIC(0) * EA_PIC(-1) * EA_PIC(-2) * EA_PIC(-3); + + EA_RR(0) - 1 = EA_R(0) / EA_PIC(1) - 1; + + EA_C(0) = EA_CI(0) * (1 - EA_OMEGA) + EA_CJ(0) * EA_OMEGA; + + EA_M(0) = EA_MI(0) * (1 - EA_OMEGA) + EA_MJ(0) * EA_OMEGA; + + EA_K(0) = EA_KI(0) * (1 - EA_OMEGA); + + EA_I(0) = EA_II(0) * (1 - EA_OMEGA); + + EA_TRJ(0) = (EA_TR(0) * 1) / EA_OMEGA - (EA_TRI(0) * (1 - EA_OMEGA)) / EA_OMEGA; + + EA_TJ(0) = (EA_T(0) * 1) / EA_OMEGA - (EA_TI(0) * (1 - EA_OMEGA)) / EA_OMEGA; + + EA_GAMMAV(0) = EA_GAMMAVI(0) * EA_CI(0) * (1 - EA_OMEGA) + EA_GAMMAVJ(0) * EA_CJ(0) * EA_OMEGA; + + EA_NI(0) = EA_NDI(0) * EA_SI(0); + + EA_SI(0) = (1 - EA_XII) * (EA_WITILDE(0) / EA_WI(0)) ^ -EA_ETAI + EA_XII * (EA_WI(-1) / EA_WI(0)) ^ -EA_ETAI * (EA_PIC(0) / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHII)) * EA_PIC(-1) ^ EA_CHII)) ^ EA_ETAI * EA_SI(-1); + + EA_NJ(0) = EA_NDJ(0) * EA_SJ(0); + + EA_SJ(0) = (1 - EA_XIJ) * (EA_WJTILDE(0) / EA_WJ(0)) ^ -EA_ETAJ + EA_XIJ * (EA_WJ(-1) / EA_WJ(0)) ^ -EA_ETAJ * (EA_PIC(0) / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIJ)) * EA_PIC(-1) ^ EA_CHIJ)) ^ EA_ETAJ * EA_SJ(-1); + + EA_U(0) * EA_K(0) = EA_KD(0); + + EA_YS(0) = EA_H(0) * EA_SH(0) + (US_IM(0) * US_SIZE * EA_SX(0)) / EA_SIZE; + + EA_H(0) = EA_G(0) + EA_HC(0) + EA_HI(0); + + EA_IM(0) = EA_IMC(0) + EA_IMI(0); + + EA_SH(0) = (1 - EA_XIH) * (EA_PHTILDE(0) / EA_PH(0)) ^ -EA_THETA + EA_XIH * (EA_PIH(0) / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * EA_PIH(-1) ^ EA_CHIH)) ^ EA_THETA * EA_SH(-1); + + EA_SX(0) = (1 - EA_XIX) * (US_PIMTILDE(0) / US_PIM(0)) ^ -EA_THETA + EA_XIX * (US_PIIM(0) / (EA_PI4TARGET ^ (0.25 * (1 - EA_CHIH)) * US_PIIM(-1) ^ EA_CHIX)) ^ EA_THETA * EA_SX(-1); + + EA_QC(0) = EA_C(0) + EA_GAMMAV(0); + + EA_QI(0) = EA_I(0) + EA_GAMMAU(0) * EA_K(0); + + EA_Y(0) * EA_PY(0) = ((US_IM(0) * US_SIZE * US_PIM(0) * EAUS_RER(0)) / EA_SIZE + EA_PH(0) * EA_G(0) + EA_QC(0) + EA_PI(0) * EA_QI(0)) - EA_PIM(0) * (((1 - EA_GAMMAIMC(0)) * EA_IMC(0)) / EA_GAMMAIMCDAG(0) + ((1 - EA_GAMMAIMI(0)) * EA_IMI(0)) / EA_GAMMAIMIDAG(0)); + + EA_Y(0) = EA_YS(0); + + log(EA_Z(0)) = (1 - EA_RHOZ) * log(EA_ZBAR) + EA_RHOZ * log(EA_Z(-1)) + sigma__EA_Z * EA_EPSZ; + + EA_GY(0) = (1 - EA_RHOG) * EA_GYBAR + EA_RHOG * EA_GY(-1) + sigma__EA_G * EA_EPSG; + + EA_TRY(0) = (1 - EA_RHOTR) * EA_TRYBAR + EA_RHOTR * EA_TRY(-1) + sigma__EA_TR * EA_EPSTR; + + EA_TAUC(0) = (1 - EA_RHOTAUC) * EA_TAUCBAR + EA_TAUC(-1) * EA_RHOTAUC + sigma__EA_TAUC * EA_EPSTAUC; + + EA_TAUD(0) = (1 - EA_RHOTAUD) * EA_TAUDBAR + EA_TAUD(-1) * EA_RHOTAUD + sigma__EA_TAUD * EA_EPSTAUD; + + EA_TAUK(0) = EA_TAUKBAR * (1 - EA_RHOTAUK) + EA_TAUK(-1) * EA_RHOTAUK + sigma__EA_TAUK * EA_EPSTAUK; + + EA_TAUN(0) = (1 - EA_RHOTAUN) * EA_TAUNBAR + EA_TAUN(-1) * EA_RHOTAUN + sigma__EA_TAUN * EA_EPSTAUN; + + EA_TAUWH(0) = (1 - EA_RHOTAUWH) * EA_TAUWHBAR + EA_TAUWH(-1) * EA_RHOTAUWH + sigma__EA_TAUWH * EA_EPSTAUWH; + + EA_TAUWF(0) = (1 - EA_RHOTAUWF) * EA_TAUWFBAR + EA_TAUWF(-1) * EA_RHOTAUWF + sigma__EA_TAUWF * EA_EPSTAUWF; + + EA_CY(0) = EA_C(0) / (EA_Y(0) * EA_PY(0)); + + EA_IY(0) = (EA_PI(0) * EA_I(0)) / (EA_Y(0) * EA_PY(0)); + + EA_IMY(0) = (EA_PIM(0) * EA_IM(0)) / (EA_Y(0) * EA_PY(0)); + + EA_IMCY(0) = (EA_IMC(0) * EA_PIM(0)) / (EA_Y(0) * EA_PY(0)); + + EA_IMIY(0) = (EA_PIM(0) * EA_IMI(0)) / (EA_Y(0) * EA_PY(0)); + + EA_BY(0) = EA_B(0) / (EA_PYBAR * EA_YBAR); + + EA_TY(0) = EA_T(0) / (EA_PYBAR * EA_YBAR); + + EA_YGAP(0) = EA_Y(0) / EA_YBAR - 1; + + EA_YGROWTH(0) = EA_Y(0) / EA_Y(-1); + + EA_YSHARE(0) = ((EA_Y(0) * EA_PY(0) * EA_SIZE) / EA_RER(0)) / ((EA_Y(0) * EA_PY(0) * EA_SIZE) / EA_RER(0) + (US_Y(0) * US_SIZE * US_PY(0)) / US_RER); + + EA_EPSILONM(0) = -0.125 / (EA_R(0) * ((EA_R(0) + EA_R(0) * EA_GAMMAV2) - 1)); + + US_UTILI(0) = ((1 / (1 - US_SIGMA)) * (US_CI(0) - US_KAPPA * US_CI(-1)) ^ (1 - US_SIGMA) - (1 / (1 + US_ZETA)) * US_NI(0) ^ (1 + US_ZETA)) + US_BETA * US_UTILI(1); + + US_LAMBDAI(0) * (1 + US_TAUC(0) + US_GAMMAVI(0) + US_VI(0) * US_GAMMAVIDER(0)) = (US_CI(0) - US_KAPPA * US_CI(-1)) ^ -US_SIGMA; + + US_R(0) = ((US_LAMBDAI(0) * US_BETA ^ -1) / US_LAMBDAI(1)) * US_PIC(1); + + US_GAMMAVIDER(0) * US_VI(0) ^ 2 = 1 - (US_BETA * US_LAMBDAI(1)) / (US_LAMBDAI(0) * US_PIC(1)); + + US_VI(0) = (US_CI(0) * (1 + US_TAUC(0))) / US_MI(0); + + US_GAMMAVI(0) = (US_VI(0) * US_GAMMAV1 + US_GAMMAV2 / US_VI(0)) - 2 * (US_GAMMAV1 * US_GAMMAV2) ^ 0.5; + + US_GAMMAVIDER(0) = US_GAMMAV1 - US_GAMMAV2 * US_VI(0) ^ -2; + + US_KI(0) = (1 - US_DELTA) * US_KI(-1) + (1 - US_GAMMAI(-1)) * US_II(-1); + + US_GAMMAI(0) = (US_GAMMAI1 / 2) * (US_II(0) / US_II(-1) - 1) ^ 2; + + US_GAMMAIDER(0) = (US_GAMMAI1 * (US_II(0) / US_II(-1) - 1)) / US_II(-1); + + US_GAMMAU(0) = ((((US_DELTA + US_BETA ^ -1) - 1) * US_QBAR - US_DELTA * US_TAUKBAR * US_PIBAR) / (US_PIBAR * (1 - US_TAUKBAR))) * (US_U(0) - 1) + (US_GAMMAU2 / 2) * (US_U(0) - 1) ^ 2; + + US_GAMMAUDER(0) = (((US_DELTA + US_BETA ^ -1) - 1) * US_QBAR - US_DELTA * US_TAUKBAR * US_PIBAR) / (US_PIBAR * (1 - US_TAUKBAR)) + (US_U(0) - 1) * US_GAMMAU2; + + US_RK(0) = US_GAMMAUDER(0) * US_PI(0); + + US_PI(0) = US_Q(0) * ((1 - US_GAMMAI(0)) - US_II(0) * US_GAMMAIDER(0)) + (((US_BETA * US_LAMBDAI(1)) / US_LAMBDAI(0)) * US_Q(1) * US_GAMMAIDER(1) * US_II(1) ^ 2) / US_II(0); + + US_Q(0) = ((US_BETA * US_LAMBDAI(1)) / US_LAMBDAI(0)) * ((1 - US_TAUK(1)) * (US_RK(1) * US_U(1) - US_GAMMAU(1) * US_PI(1)) + US_PI(1) * US_DELTA * US_TAUK(1) + (1 - US_DELTA) * US_Q(1)); + + US_WITILDE(0) ^ (1 + US_ZETA * US_ETAI) = ((US_ETAI / (US_ETAI - 1)) * US_FI(0)) / US_GI(0); + + US_FI(0) = US_WI(0) ^ ((1 + US_ZETA) * US_ETAI) * US_NDI(0) ^ (1 + US_ZETA) + US_BETA * US_XII * (US_PIC(1) / (US_PIC(0) ^ US_CHII * US_PI4TARGET ^ (0.25 * (1 - US_CHII)))) ^ ((1 + US_ZETA) * US_ETAI) * US_FI(1); + + US_GI(0) = US_NDI(0) * US_LAMBDAI(0) * ((1 - US_TAUN(0)) - US_TAUWH(0)) * US_WI(0) ^ US_ETAI + US_BETA * US_XII * (US_PIC(1) / (US_PIC(0) ^ US_CHII * US_PI4TARGET ^ (0.25 * (1 - US_CHII)))) ^ (US_ETAI - 1) * US_GI(1); + + US_WI(0) ^ (1 - US_ETAI) = (1 - US_XII) * US_WITILDE(0) ^ (1 - US_ETAI) + US_XII * US_WI(-1) ^ (1 - US_ETAI) * ((US_PI4TARGET ^ (0.25 * (1 - US_CHII)) * US_PIC(-1) ^ US_CHII) / US_PIC(0)) ^ (1 - US_ETAI); + + US_UTILJ(0) = ((1 / (1 - US_SIGMA)) * (US_CJ(0) - US_KAPPA * US_CJ(-1)) ^ (1 - US_SIGMA) - (1 / (1 + US_ZETA)) * US_NJ(0) ^ (1 + US_ZETA)) + US_BETA * US_UTILJ(1); + + US_CJ(0) * (1 + US_TAUC(0) + US_GAMMAVJ(0)) + US_MJ(0) = ((US_NJ(0) * ((1 - US_TAUN(0)) - US_TAUWH(0)) * US_WJ(0) + US_TRJ(0)) - US_TJ(0)) + US_MJ(-1) * US_PIC(0) ^ -1; + + US_LAMBDAJ(0) * (1 + US_TAUC(0) + US_GAMMAVJ(0) + US_VJ(0) * US_GAMMAVJDER(0)) = (US_CJ(0) - US_KAPPA * US_CJ(-1)) ^ -US_SIGMA; + + US_GAMMAVJDER(0) * US_VJ(0) ^ 2 = 1 - (US_BETA * US_LAMBDAJ(1)) / (US_PIC(1) * US_LAMBDAJ(0)); + + US_VJ(0) = ((1 + US_TAUC(0)) * US_CJ(0)) / US_MJ(0); + + US_GAMMAVJ(0) = (US_GAMMAV1 * US_VJ(0) + US_GAMMAV2 / US_VJ(0)) - 2 * (US_GAMMAV1 * US_GAMMAV2) ^ 0.5; + + US_GAMMAVJDER(0) = US_GAMMAV1 - US_GAMMAV2 * US_VJ(0) ^ -2; + + US_WJTILDE(0) ^ (1 + US_ZETA * US_ETAJ) = ((US_ETAJ / (US_ETAJ - 1)) * US_FJ(0)) / US_GJ(0); + + US_FJ(0) = US_WJ(0) ^ ((1 + US_ZETA) * US_ETAJ) * US_NDJ(0) ^ (1 + US_ZETA) + US_BETA * US_XIJ * (US_PIC(1) / (US_PIC(0) ^ US_CHIJ * US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)))) ^ ((1 + US_ZETA) * US_ETAJ) * US_FJ(1); + + US_GJ(0) = US_NDJ(0) * ((1 - US_TAUN(0)) - US_TAUWH(0)) * US_LAMBDAJ(0) * US_WJ(0) ^ US_ETAJ + US_BETA * US_XIJ * (US_PIC(1) / (US_PIC(0) ^ US_CHIJ * US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)))) ^ (US_ETAJ - 1) * US_GJ(1); + + US_WJ(0) ^ (1 - US_ETAJ) = (1 - US_XIJ) * US_WJTILDE(0) ^ (1 - US_ETAJ) + US_XIJ * US_WJ(-1) ^ (1 - US_ETAJ) * ((US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)) * US_PIC(-1) ^ US_CHIJ) / US_PIC(0)) ^ (1 - US_ETAJ); + + US_YS(0) = US_Z(0) * US_KD(0) ^ US_ALPHA * US_ND(0) ^ (1 - US_ALPHA) - US_PSIBAR; + + US_RK(0) = ((US_ALPHA * (US_YS(0) + US_PSIBAR)) / US_KD(0)) * US_MC(0); + + US_MC(0) = (1 / (US_Z(0) * US_ALPHA ^ US_ALPHA * (1 - US_ALPHA) ^ (1 - US_ALPHA))) * US_RK(0) ^ US_ALPHA * ((1 + US_TAUWF(0)) * US_W(0)) ^ (1 - US_ALPHA); + + US_NDI(0) = US_ND(0) * (1 - US_OMEGA) * (US_WI(0) / US_W(0)) ^ -US_ETA; + + US_NDJ(0) = US_ND(0) * US_OMEGA * (US_WJ(0) / US_W(0)) ^ -US_ETA; + + US_ND(0) ^ (1 - 1 / US_ETA) = (1 - US_OMEGA) ^ (1 / US_ETA) * US_NDI(0) ^ (1 - 1 / US_ETA) + US_OMEGA ^ (1 / US_ETA) * US_NDJ(0) ^ (1 - 1 / US_ETA); + + US_D(0) = (US_Y(0) * US_PY(0) - US_RK(0) * US_KD(0)) - US_ND(0) * (1 + US_TAUWF(0)) * US_W(0); + + US_PHTILDE(0) / US_PH(0) = ((US_THETA / (US_THETA - 1)) * US_FH(0)) / US_GH(0); + + US_FH(0) = US_MC(0) * US_H(0) + ((US_LAMBDAI(1) * US_BETA * US_XIH) / US_LAMBDAI(0)) * (US_PIH(1) / (US_PIH(0) ^ US_CHIH * US_PI4TARGET ^ (0.25 * (1 - US_CHIH)))) ^ US_THETA * US_FH(1); + + US_GH(0) = US_PH(0) * US_H(0) + ((US_LAMBDAI(1) * US_BETA * US_XIH) / US_LAMBDAI(0)) * (US_PIH(1) / (US_PIH(0) ^ US_CHIH * US_PI4TARGET ^ (0.25 * (1 - US_CHIH)))) ^ (US_THETA - 1) * US_GH(1); + + US_PH(0) ^ (1 - US_THETA) = (1 - US_XIH) * US_PHTILDE(0) ^ (1 - US_THETA) + US_XIH * (US_PH(-1) / US_PIC(0)) ^ (1 - US_THETA) * (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * US_PIH(-1) ^ US_CHIH) ^ (1 - US_THETA); + + US_PIH(0) = (US_PIC(0) * US_PH(0)) / US_PH(-1); + + EA_PIMTILDE(0) / EA_PIM(0) = ((US_THETA / (US_THETA - 1)) * US_FX(0)) / US_GX(0); + + US_FX(0) = (US_MC(0) * EA_IM(0) * EA_SIZE) / US_SIZE + ((US_LAMBDAI(1) * US_BETA * US_XIX) / US_LAMBDAI(0)) * (EA_PIIM(1) / (EA_PIIM(0) ^ US_CHIX * US_PI4TARGET ^ (0.25 * (1 - US_CHIX)))) ^ US_THETA * US_FX(1); + + US_GX(0) = (EA_IM(0) * EA_SIZE * EA_PIM(0) * USEA_RER(0)) / US_SIZE + ((US_LAMBDAI(1) * US_BETA * US_XIX) / US_LAMBDAI(0)) * (EA_PIIM(1) / (EA_PIIM(0) ^ US_CHIX * US_PI4TARGET ^ (0.25 * (1 - US_CHIX)))) ^ (US_THETA - 1) * US_GX(1); + + EA_PIM(0) ^ (1 - US_THETA) = (1 - US_XIX) * EA_PIMTILDE(0) ^ (1 - US_THETA) + US_XIX * (EA_PIM(-1) / EA_PIC(0)) ^ (1 - US_THETA) * (EA_PIIM(-1) ^ US_CHIX * EA_PI4TARGET ^ (0.25 * (1 - US_CHIH))) ^ (1 - US_THETA); + + EA_PIIM(0) = (EA_PIC(0) * EA_PIM(0)) / EA_PIM(-1); + + USEA_RER(0) = US_RER / EA_RER(0); + + US_QC(0) ^ ((US_MUC - 1) / US_MUC) = US_NUC ^ (1 / US_MUC) * US_HC(0) ^ (1 - 1 / US_MUC) + (1 - US_NUC) ^ (1 / US_MUC) * ((1 - US_GAMMAIMC(0)) * US_IMC(0)) ^ (1 - 1 / US_MUC); + + 1 = US_NUC * US_PH(0) ^ (1 - US_MUC) + (1 - US_NUC) * (US_PIM(0) / US_GAMMAIMCDAG(0)) ^ (1 - US_MUC); + + US_HC(0) = US_QC(0) * US_NUC * US_PH(0) ^ -US_MUC; + + US_GAMMAIMC(0) = (US_GAMMAIMC1 / 2) * ((US_IMC(0) / US_QC(0)) / (US_IMC(-1) / US_QC(-1)) - 1) ^ 2; + + US_GAMMAIMCDAG(0) = (1 - US_GAMMAIMC(0)) - ((US_IMC(0) * US_GAMMAIMC1 * ((US_IMC(0) / US_QC(0)) / (US_IMC(-1) / US_QC(-1)) - 1)) / US_QC(0)) / (US_IMC(-1) / US_QC(-1)); + + US_QI(0) ^ ((US_MUI - 1) / US_MUI) = US_NUI ^ (1 / US_MUI) * US_HI(0) ^ (1 - 1 / US_MUI) + (1 - US_NUI) ^ (1 / US_MUI) * ((1 - US_GAMMAIMI(0)) * US_IMI(0)) ^ (1 - 1 / US_MUI); + + US_PI(0) ^ (1 - US_MUI) = US_NUI * US_PH(0) ^ (1 - US_MUI) + (1 - US_NUI) * (US_PIM(0) / US_GAMMAIMIDAG(0)) ^ (1 - US_MUI); + + US_HI(0) = US_QI(0) * US_NUI * (US_PH(0) / US_PI(0)) ^ -US_MUI; + + US_GAMMAIMI(0) = (US_GAMMAIMI1 / 2) * ((US_IMI(0) / US_QI(0)) / (US_IMI(-1) / US_QI(-1)) - 1) ^ 2; + + US_GAMMAIMIDAG(0) = (1 - US_GAMMAIMI(0)) - ((US_IMI(0) * US_GAMMAIMI1 * ((US_IMI(0) / US_QI(0)) / (US_IMI(-1) / US_QI(0)) - 1)) / US_QI(0)) / (US_IMI(-1) / US_QI(-1)); + + US_PH(-1) * US_G(-1) + US_TR(-1) + US_B(-1) * US_PIC(-1) ^ -1 + US_PIC(-1) ^ -1 * US_M(-2) = US_TAUC(-1) * US_C(-1) + (US_TAUN(-1) + US_TAUWH(-1)) * (US_WI(-1) * US_NDI(-1) + US_WJ(-1) * US_NDJ(-1)) + US_TAUWF(-1) * US_W(-1) * US_ND(-1) + US_TAUK(-1) * (US_RK(-1) * US_U(-1) - (US_DELTA + US_GAMMAU(-1)) * US_PI(-1)) * US_K(-1) + US_TAUD(-1) * US_D(-1) + US_T(-1) + US_R(-1) ^ -1 * US_B(0) + US_M(-1); + + US_PH(0) * US_G(0) = US_GY(0) * US_PYBAR * US_YBAR; + + US_TR(0) = US_YBAR * US_PYBAR * US_TRY(0); + + US_T(0) / (US_PYBAR * US_YBAR) = US_PHITB * (US_B(0) / (US_PYBAR * US_YBAR) - US_BYTARGET); + + US_TI(0) = US_T(0) * US_UPSILONT; + + US_TRI(0) = US_TR(0) * US_UPSILONTR; + + US_PIC4(0) = US_PIC(0) * US_PIC(-1) * US_PIC(-2) * US_PIC(-3); + + US_RR(0) - 1 = US_R(0) / US_PIC(1) - 1; + + US_C(0) = US_CI(0) * (1 - US_OMEGA) + US_CJ(0) * US_OMEGA; + + US_M(0) = US_MI(0) * (1 - US_OMEGA) + US_MJ(0) * US_OMEGA; + + US_K(0) = US_KI(0) * (1 - US_OMEGA); + + US_I(0) = US_II(0) * (1 - US_OMEGA); + + US_TRJ(0) = (US_TR(0) * 1) / US_OMEGA - (US_TRI(0) * (1 - US_OMEGA)) / US_OMEGA; + + US_TJ(0) = (US_T(0) * 1) / US_OMEGA - (US_TI(0) * (1 - US_OMEGA)) / US_OMEGA; + + US_GAMMAV(0) = US_GAMMAVI(0) * US_CI(0) * (1 - US_OMEGA) + US_GAMMAVJ(0) * US_CJ(0) * US_OMEGA; + + US_NI(0) = US_NDI(0) * US_SI(0); + + US_SI(0) = (1 - US_XII) * (US_WITILDE(0) / US_WI(0)) ^ -US_ETAI + US_XII * (US_WI(-1) / US_WI(0)) ^ -US_ETAI * (US_PIC(0) / (US_PI4TARGET ^ (0.25 * (1 - US_CHII)) * US_PIC(-1) ^ US_CHII)) ^ US_ETAI * US_SI(-1); + + US_NJ(0) = US_NDJ(0) * US_SJ(0); + + US_SJ(0) = (1 - US_XIJ) * (US_WJTILDE(0) / US_WJ(0)) ^ -US_ETAJ + US_XIJ * (US_WJ(-1) / US_WJ(0)) ^ -US_ETAJ * (US_PIC(0) / (US_PI4TARGET ^ (0.25 * (1 - US_CHIJ)) * US_PIC(-1) ^ US_CHIJ)) ^ US_ETAJ * US_SJ(-1); + + US_U(0) * US_K(0) = US_KD(0); + + US_YS(0) = US_H(0) * US_SH(0) + (EA_IM(0) * EA_SIZE * US_SX(0)) / US_SIZE; + + US_H(0) = US_G(0) + US_HC(0) + US_HI(0); + + US_IM(0) = US_IMC(0) + US_IMI(0); + + US_SH(0) = (1 - US_XIH) * (US_PHTILDE(0) / US_PH(0)) ^ -US_THETA + US_XIH * (US_PIH(0) / (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * US_PIH(-1) ^ US_CHIH)) ^ US_THETA * US_SH(-1); + + US_SX(0) = (1 - US_XIX) * (EA_PIMTILDE(0) / EA_PIM(0)) ^ -US_THETA + US_XIX * (EA_PIIM(0) / (US_PI4TARGET ^ (0.25 * (1 - US_CHIH)) * EA_PIIM(-1) ^ US_CHIX)) ^ US_THETA * US_SX(-1); + + US_QC(0) = US_C(0) + US_GAMMAV(0); + + US_QI(0) = US_I(0) + US_GAMMAU(0) * US_K(0); + + US_Y(0) * US_PY(0) = ((EA_IM(0) * EA_SIZE * EA_PIM(0) * USEA_RER(0)) / US_SIZE + US_PH(0) * US_G(0) + US_QC(0) + US_PI(0) * US_QI(0)) - US_PIM(0) * (((1 - US_GAMMAIMC(0)) * US_IMC(0)) / US_GAMMAIMCDAG(0) + ((1 - US_GAMMAIMI(0)) * US_IMI(0)) / US_GAMMAIMIDAG(0)); + + US_Y(0) = US_YS(0); + + log(US_Z(0)) = (1 - US_RHOZ) * log(US_ZBAR) + US_RHOZ * log(US_Z(-1)) + sigma__US_Z * US_EPSZ; + + US_GY(0) = (1 - US_RHOG) * US_GYBAR + US_RHOG * US_GY(-1) + sigma__US_G * US_EPSG; + + US_TRY(0) = (1 - US_RHOTR) * US_TRYBAR + US_RHOTR * US_TRY(-1) + sigma__US_TR * US_EPSTR; + + US_TAUC(0) = (1 - US_RHOTAUC) * US_TAUCBAR + US_TAUC(-1) * US_RHOTAUC + sigma__US_TAUC * US_EPSTAUC; + + US_TAUD(0) = (1 - US_RHOTAUD) * US_TAUDBAR + US_TAUD(-1) * US_RHOTAUD + sigma__US_TAUD * US_EPSTAUD; + + US_TAUK(0) = US_TAUKBAR * (1 - US_RHOTAUK) + US_TAUK(-1) * US_RHOTAUK + sigma__US_TAUK * US_EPSTAUK; + + US_TAUN(0) = (1 - US_RHOTAUN) * US_TAUNBAR + US_TAUN(-1) * US_RHOTAUN + sigma__US_TAUN * US_EPSTAUN; + + US_TAUWH(0) = (1 - US_RHOTAUWH) * US_TAUWHBAR + US_TAUWH(-1) * US_RHOTAUWH + sigma__US_TAUWH * US_EPSTAUWH; + + US_TAUWF(0) = (1 - US_RHOTAUWF) * US_TAUWFBAR + US_TAUWF(-1) * US_RHOTAUWF + sigma__US_TAUWF * US_EPSTAUWF; + + US_CY(0) = US_C(0) / (US_Y(0) * US_PY(0)); + + US_IY(0) = (US_PI(0) * US_I(0)) / (US_Y(0) * US_PY(0)); + + US_IMY(0) = (US_PIM(0) * US_IM(0)) / (US_Y(0) * US_PY(0)); + + US_IMCY(0) = (US_PIM(0) * US_IMC(0)) / (US_Y(0) * US_PY(0)); + + US_IMIY(0) = (US_PIM(0) * US_IMI(0)) / (US_Y(0) * US_PY(0)); + + US_BY(0) = US_B(0) / (US_PYBAR * US_YBAR); + + US_TY(0) = US_T(0) / (US_PYBAR * US_YBAR); + + US_YGAP(0) = US_Y(0) / US_YBAR - 1; + + US_YGROWTH(0) = US_Y(0) / US_Y(-1); + + US_YSHARE(0) = ((US_Y(0) * US_SIZE * US_PY(0)) / US_RER) / ((EA_Y(0) * EA_PY(0) * EA_SIZE) / EA_RER(0) + (US_Y(0) * US_SIZE * US_PY(0)) / US_RER); + + US_EPSILONM(0) = -0.125 / (US_R(0) * ((US_R(0) + US_R(0) * US_GAMMAV2) - 1)); + + 1 = (((EA_LAMBDAI(1) * EA_BETA * US_R(0) * (1 - EA_GAMMAB(0))) / EA_LAMBDAI(0)) * EA_RERDEP(1)) / US_PIC(1); + + EA_GAMMAB(0) = EA_GAMMAB1 * (exp(((EA_RER(0) * EA_BF(0)) / US_PIC(0)) / (EA_Y(0) * EA_PY(0)) - EA_BFYTARGET) - 1) - EA_RP(0); + + EA_RP(0) = EA_RHORP * EA_RP(-1) + sigma__EA_RP * EA_EPSRP; + + EA_RERDEP(0) = EA_RER(0) / EA_RER(-1); + + EA_TOT(0) = EA_PIM(0) / (US_PIM(0) * EA_RER(0)); + + EA_TB(0) = (US_IM(0) * US_SIZE * US_PIM(0) * EA_RER(0)) / EA_SIZE - EA_PIM(0) * EA_IM(0); + + EA_BF(0) / US_R(-1) = EA_BF(-1) + EA_TB(-1) / EA_RER(-1); + + EA_SIZE * EA_BF(0) + US_SIZE * US_BF(0) = 0; + +end; + +shocks; +var EA_EPSG = 1; +var EA_EPSR = 1; +var EA_EPSRP = 1; +var EA_EPSTAUC = 1; +var EA_EPSTAUD = 1; +var EA_EPSTAUK = 1; +var EA_EPSTAUN = 1; +var EA_EPSTAUWF = 1; +var EA_EPSTAUWH = 1; +var EA_EPSTR = 1; +var EA_EPSZ = 1; +var US_EPSG = 1; +var US_EPSR = 1; +var US_EPSTAUC = 1; +var US_EPSTAUD = 1; +var US_EPSTAUK = 1; +var US_EPSTAUN = 1; +var US_EPSTAUWF = 1; +var US_EPSTAUWH = 1; +var US_EPSTR = 1; +var US_EPSZ = 1; +end; + +initval; + EAUS_RER = 0.9375769789091696; + EA_B = 8.760867242540183; + EA_BF = 8.68814729053804e-16; + EA_BY = 2.399973271207332; + EA_C = 2.189547640633434; + EA_CI = 2.306545839791338; + EA_CJ = 1.8385530431597221; + EA_CY = 0.5998056568674031; + EA_D = 4.5281352595307525e-6; + EA_EPSILONM = -0.7500029830947027; + EA_FH = 23.394478807928166; + EA_FI = 130.04215430500957; + EA_FJ = 4.816376085370729; + EA_FX = 0.7797806786396915; + EA_G = 0.6528567290328195; + EA_GAMMAB = 0.0; + EA_GAMMAI = 0.0; + EA_GAMMAIDER = 0.0; + EA_GAMMAIMC = 0.0; + EA_GAMMAIMCDAG = 1.0; + EA_GAMMAIMI = 0.0; + EA_GAMMAIMIDAG = 1.0; + EA_GAMMAU = -2.388954212973115e-17; + EA_GAMMAUDER = 0.03409034206955798; + EA_GAMMAV = 0.0007018360171077989; + EA_GAMMAVI = 0.0003205392767360593; + EA_GAMMAVIDER = 0.021802850332968495; + EA_GAMMAVJ = 0.00032053927673608706; + EA_GAMMAVJDER = 0.021802850332968495; + EA_GH = 28.073374569513827; + EA_GI = 9.363748856147609; + EA_GJ = 4.912475014679153; + EA_GX = 0.9357368143676298; + EA_GY = 0.18; + EA_H = 2.9741413714552603; + EA_HC = 1.9948487156940642; + EA_HI = 0.3264359267283768; + EA_I = 0.8355960885228276; + EA_II = 1.114128118030437; + EA_IM = 0.7062807155588531; + EA_IMC = 0.19618554860387052; + EA_IMCY = 0.04999950252314003; + EA_IMI = 0.5100951669549825; + EA_IMIY = 0.13000195360314123; + EA_IMY = 0.1800014561262813; + EA_IY = 0.2200033863374439; + EA_K = 33.423843540913055; + EA_KD = 33.423843540913026; + EA_KI = 44.565124721217394; + EA_LAMBDAI = 0.9792488685431114; + EA_LAMBDAJ = 1.5412210452587862; + EA_M = 3.4536517528084834; + EA_MC = 0.8387143403535829; + EA_MI = 3.6381971941126205; + EA_MJ = 2.90001542889607; + EA_ND = 1.8167840593397695; + EA_NDI = 0.8775171676928117; + EA_NDJ = 0.9941778258256061; + EA_NI = 0.8775171676928117; + EA_NJ = 0.9941778258256064; + EA_PH = 1.0064572084242995; + EA_PHTILDE = 1.0064572084242986; + EA_PI = 0.9611182172064003; + EA_PIC = 1.0049629315732036; + EA_PIC4 = 1.0200000000000002; + EA_PIH = 1.0049629315732036; + EA_PIIM = 1.0049629315732036; + EA_PIM = 0.9303417520972932; + EA_PIMTILDE = 0.9303417520972932; + EA_PY = 1.0064572084243006; + EA_Q = 0.9611182172064003; + EA_QC = 2.190249476650542; + EA_QI = 0.8355960885228267; + EA_R = 1.01241634067324; + EA_RER = 0.9375769789091696; + EA_RERDEP = 1.0; + EA_RK = 0.03276484879384991; + EA_RP = 0.0; + EA_RR = 1.007416601016685; + EA_SH = 0.9999999999999991; + EA_SI = 1.0; + EA_SJ = 1.0000000000000002; + EA_SX = 0.9999999999999997; + EA_T = -9.757083836240542e-6; + EA_TAUC = 0.183; + EA_TAUD = 0.0; + EA_TAUK = 0.184123; + EA_TAUN = 0.122; + EA_TAUWF = 0.219; + EA_TAUWH = 0.118; + EA_TB = -9.990070930483452e-18; + EA_TI = -1.170850060348865e-5; + EA_TJ = -3.902833534496219e-6; + EA_TOT = 0.9243728837253081; + EA_TR = 0.7124161058099042; + EA_TRI = 0.47494407053993615; + EA_TRJ = 1.4248322116198084; + EA_TRY = 0.195161; + EA_TY = -2.672879266815233e-6; + EA_U = 0.9999999999999993; + EA_UTILI = -177.82013738769632; + EA_UTILJ = -229.19178867999312; + EA_VI = 0.7499988546219211; + EA_VJ = 0.7499988546219211; + EA_W = 1.1538100746815612; + EA_WI = 1.241611764085936; + EA_WITILDE = 1.241611764085936; + EA_WJ = 1.012583550388813; + EA_WJTILDE = 1.0125835503888128; + EA_Y = 3.6270081132159686; + EA_YGAP = 7.442691738654727e-6; + EA_YGROWTH = 1.0; + EA_YS = 3.6270081132159686; + EA_YSHARE = 0.41935437261682923; + EA_Z = 1.0; + USEA_RER = 1.0665790889655342; + US_B = 9.345934392928857; + US_BF = -6.275936916382456e-16; + US_BY = 2.399980755467948; + US_C = 2.4141519006053844; + US_CI = 2.6973552911805156; + US_CJ = 1.5645417288799908; + US_CY = 0.619935215979073; + US_D = 4.831204977808761e-6; + US_EPSILONM = -0.7500029830947027; + US_FH = 26.478512782513477; + US_FI = 828.473190793932; + US_FJ = 30.684192251627056; + US_FX = 0.6007820237399558; + US_G = 0.6279128558129433; + US_GAMMAI = 0.0; + US_GAMMAIDER = 0.0; + US_GAMMAIMC = 0.0; + US_GAMMAIMCDAG = 1.0; + US_GAMMAIMI = 0.0; + US_GAMMAIMIDAG = 1.0; + US_GAMMAU = -3.137919241526777e-16; + US_GAMMAUDER = 0.03409034206955788; + US_GAMMAV = 0.00024385317859487778; + US_GAMMAVI = 0.0001010098737091597; + US_GAMMAVIDER = 0.002165102315533411; + US_GAMMAVJ = 0.0001010098737091597; + US_GAMMAVJDER = 0.002165102315533411; + US_GH = 31.774215339016152; + US_GI = 14.34775917694218; + US_GJ = 14.215588107432922; + US_GX = 0.7209384284879471; + US_GY = 0.16; + US_H = 3.414299076546841; + US_HC = 2.1977041391845056; + US_HI = 0.5886820815493922; + US_I = 0.8417806331020254; + US_II = 1.1223741774693672; + US_IM = 0.4716023277548092; + US_IMC = 0.21766047820999493; + US_IMCY = 0.05999978210567448; + US_IMI = 0.2539418495448143; + US_IMIY = 0.07000102069747807; + US_IMY = 0.13000080280315254; + US_IY = 0.22000338629871627; + US_K = 33.67122532408105; + US_KD = 33.67122532408075; + US_KI = 44.894967098774735; + US_LAMBDAI = 0.7937331553700396; + US_LAMBDAJ = 2.359263937623106; + US_M = 1.0924517669190108; + US_MC = 0.826902548648775; + US_MI = 1.2206069357605713; + US_MJ = 0.7079862603943298; + US_ND = 2.026920760763991; + US_NDI = 0.8427382490620139; + US_NDJ = 1.280400524253441; + US_NI = 0.8427382490620139; + US_NJ = 1.2804005242534415; + US_PH = 0.9922830583785271; + US_PHTILDE = 0.9922830583785277; + US_PI = 1.0177678340581786; + US_PIC = 1.0049629315732036; + US_PIC4 = 1.0200000000000002; + US_PIH = 1.0049629315732036; + US_PIIM = 1.0049629315732036; + US_PIM = 1.0734662124439842; + US_PIMTILDE = 1.0734662124439842; + US_PY = 0.9922830583785355; + US_Q = 1.0177678340581786; + US_QC = 2.41439575378398; + US_QI = 0.8417806331020149; + US_R = 1.01241634067324; + US_RK = 0.03469605361043634; + US_RR = 1.007416601016685; + US_SH = 0.9999999999999912; + US_SI = 1.0; + US_SJ = 1.0000000000000002; + US_SX = 0.9999999999999996; + US_T = -7.494149007856961e-6; + US_TAUC = 0.077; + US_TAUD = 0.0; + US_TAUK = 0.184123; + US_TAUN = 0.154; + US_TAUWF = 0.071; + US_TAUWH = 0.071; + US_TI = -8.992978809428354e-6; + US_TJ = -2.997659603142785e-6; + US_TR = 0.3104900067716212; + US_TRI = 0.20699333784774746; + US_TRJ = 0.6209800135432424; + US_TRY = 0.079732; + US_TY = -1.9244532051363434e-6; + US_U = 0.9999999999999908; + US_UTILI = -152.99377798327683; + US_UTILJ = -312.09169912993883; + US_VI = 2.3800058507706674; + US_VJ = 2.3800058507706674; + US_W = 1.2557105547683036; + US_WI = 1.3854486468764884; + US_WITILDE = 1.3854486468764884; + US_WJ = 1.0759564683399199; + US_WJTILDE = 1.0759564683399199; + US_Y = 3.9244853185471262; + US_YGAP = 7.443747966139748e-6; + US_YGROWTH = 1.0; + US_YS = 3.9244853185471262; + US_YSHARE = 0.5806456273831708; + US_Z = 1.0; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/QUEST3_2009.jl b/test/QUEST3_2009.jl new file mode 100644 index 000000000..1df9da4ec --- /dev/null +++ b/test/QUEST3_2009.jl @@ -0,0 +1,499 @@ +using MacroModelling + +@model QUEST3_2009 begin + interest[0] = ((1 + E_INOM[0]) ^ 4 - interestq_exog ^ 4) / interestq_exog ^ 4 + + inflation[0] = 0.25 * (inflationq[0] + inflationq[-1] + AUX_ENDO_LAG_104_1[-1] + AUX_ENDO_LAG_104_2[-1]) + + inflationq[0] = (1 + 4 * E_PHIC[0] - inflationannual_exog) / inflationannual_exog + + outputgap[0] = E_LYGAP[0] + + E_INOM[0] = ILAGE * E_INOM[-1] + (1 - ILAGE) * (E_EX_R + GP0 + TINFE * (E_PHIC[0] - GP0) + TYE1 * E_LYGAP[-1]) + TYE2 * (E_LYGAP[0] - E_LYGAP[-1]) + E_ZEPS_M[0] + + exp(E_LUCYN[0]) = exp(E_ZEPS_C[0]) * (exp(E_LCNLCSN[0]) * (1 - HABE / (1 + E_GCNLC[0] - GY0))) ^ (-SIGC) * (1 - OMEGE * exp(E_ZEPS_L[0]) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ KAPPAE) ^ (1 - SIGC) + + exp(E_LUCLCYN[0]) = (1 - OMEGE * exp(E_ZEPS_L[0]) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ KAPPAE) ^ (1 - SIGC) * exp(E_LCLCSN[0]) ^ (-SIGC) + + E_VL[0] = exp(E_ZEPS_C[0]) * OMEGE * exp(E_ZEPS_L[0]) * KAPPAE * (exp(E_LCNLCSN[0]) * (1 - HABE / (1 + E_GCNLC[0] - GY0))) ^ (1 - SIGC) * (1 - OMEGE * exp(E_ZEPS_L[0]) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ KAPPAE) ^ (-SIGC) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ (KAPPAE - 1) + + E_VLLC[0] = (1 - OMEGE * exp(E_ZEPS_L[0]) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ KAPPAE) ^ (-SIGC) * KAPPAE * OMEGE * exp(E_ZEPS_L[0]) * (exp(E_LL[0]) - HABLE * exp(E_LL[-1])) ^ (KAPPAE - 1) * exp(E_LCLCSN[0]) ^ (1 - SIGC) + + 1 / BETAE - 1 = E_INOM[0] + E_GUC[1] - E_PHIC[1] + + E_LUCYN[0] - E_LUCYN[-1] = E_GUC[0] + SIGC * (E_GY[0] - GY0 - E_PHIC[0] + E_PHI[0]) + + exp(E_LCLCSN[0]) * (1 + TVAT) = (1 - E_TW[0] - SSC) * E_WS[0] + E_WS[0] * E_TRW[0] - E_TAXYN[0] + + E_WS[0] = exp(E_LL[0] - E_LYWR[0]) + + exp(E_LCSN[0]) = exp(E_LCLCSN[0]) * SLCFLAG * SLC + exp(E_LCNLCSN[0]) * (1 - SLCFLAG * SLC) + + (1 + TVAT) * ((E_VL[0] * (1 - SLC) + E_VLLC[0] * SLC) / (exp(E_LUCYN[0]) * (1 - SLC) + exp(E_LUCLCYN[0]) * SLC)) ^ (1 - WRLAG) * ((1 - E_TW[0] - SSC) / (1 + TVAT) * (THETAE - 1) / THETAE / exp(E_LYWR[-1]) / (1 + E_GY[0] - GY0)) ^ WRLAG = (1 - E_TW[0] - SSC) * (THETAE - 1) / THETAE / exp(E_LYWR[0]) + GAMIFLAG * GAMWE / THETAE / exp(E_LYWR[0]) * (E_WPHI[0] - GP0 - GY0 - (1 - SFWE) * (E_PHI[-1] - GP0)) - GAMIFLAG * BETAE * GAMWE / THETAE / exp(E_LYWR[0]) * (E_WPHI[1] - GP0 - GY0 - (1 - SFWE) * (E_PHI[0] - GP0)) + + (1 + E_ZEPS_W[0]) / exp(E_LYWR[0]) = E_ETA[0] * ALPHAE / exp(E_LL[0]) * (1 + E_LOL[0]) - 1 / exp(E_LYWR[0]) * GAMLE * (E_LL[0] - E_LL[-1]) + GAMLE / exp(E_LYWR[1]) * (1 + E_GY[1] - GY0) / (1 + E_R[0]) * (E_LL[1] - E_LL[0]) + + GAMIE * (exp(E_LIK[0]) - (GY0 + DELTAE + GPOP0 + GPCPI0)) + GAMI2E * (E_GI[0] - GY0 - GPCPI0) - GAMI2E / (1 + E_INOM[0]) * (E_GI[1] - GY0 - GPCPI0) = E_Q[0] - 1 + + E_ETA[0] * (1 - TP) * (1 - ALPHAE) * exp(E_LYKPPI[0]) = E_Q[0] - (1 + GPCPI0 - E_R[0] - DELTAE - RPREMK - E_ZEPS_RPREMK[0] - E_PHIPI[1]) * E_Q[1] + (1 - TP) * (A1E * (E_UCAP[0] - UCAP0) + A2E * (E_UCAP[0] - UCAP0) ^ 2) + + (1 - ALPHAE) * E_ETA[0] * exp(E_LYKPPI[0]) = E_UCAP[0] * (A1E + A2E * 2 * (E_UCAP[0] - UCAP0)) + + E_ETA[0] = 1 - (TAUE + E_ZEPS_ETA[0]) - GAMIFLAG * GAMPE * (BETAE * (SFPE * E_PHI[1] + E_PHI[-1] * (1 - SFPE) - GP0) - (E_PHI[0] - GP0)) + + E_MRY[0] = (1 + E_INOM[0]) ^ (-ZETE) + + E_GK[0] - (GY0 + GPCPI0) = exp(E_LIK[0]) - (GY0 + DELTAE + GPOP0 + GPCPI0) + + E_GKG[0] - (GY0 + GPCPI0) = exp(E_LIKG[0]) - (GPOP0 + GY0 + GPCPI0 + DELTAGE) + + E_LISN[0] = E_LIK[0] + GY0 + GPCPI0 - E_LYKPPI[0] - E_GK[0] + + E_GY[0] = (1 - ALPHAE) * (E_GK[0] + E_GUCAP[0]) + ALPHAE * (E_GTFP[0] + E_GL[0] * (1 + LOL)) + E_GKG[0] * (1 - ALPHAGE) + + E_R[0] = E_INOM[0] - E_PHI[1] + + E_LYGAP[0] = (1 - ALPHAE) * (log(E_UCAP[0]) - log(E_UCAP0[0])) + ALPHAE * (E_LL[0] - E_LL0[0]) + + E_LL0[0] = RHOL0 * E_LL0[-1] + E_LL[0] * (1 - RHOL0) + + E_UCAP0[0] = RHOUCAP0 * E_UCAP0[-1] + E_UCAP[0] * (1 - RHOUCAP0) + + exp(E_LPCP[0]) = (SE + (1 - SE) * exp(E_LPMP[0]) ^ (1 - SIGIME)) ^ (1 / (1 - SIGIME)) + + exp(E_LPMP[0]) = (1 + E_ZEPS_ETAM[0] + GAMIFLAG * GAMPME * (GP0 + BETAE * (SFPME * E_PHIM[1] + (1 - SFPME) * E_PHIM[-1] - GP0) - E_PHIM[0])) * exp(E_LER[0]) ^ ALPHAX + + exp(E_LPXP[0]) = 1 + E_ZEPS_ETAX[0] + GAMIFLAG * GAMPXE * (GP0 + BETAE * (SFPXE * E_PHIX[1] + (1 - SFPXE) * E_PHIX[-1] - GP0) - E_PHIX[0]) + + 1 = exp(E_LCSN[0]) + exp(E_LISN[0]) + exp(E_LIGSN[0]) + exp(E_LGSN[0]) + E_TBYN[0] + + E_TBYN[0] = exp(E_LEXYN[0]) - exp(E_LIMYN[0]) + E_ZEPS_EX[0] + + exp(E_LIMYN[0]) = (1 - SE) * (exp(E_LCSN[0]) + exp(E_LISN[0]) + exp(E_LIGSN[0]) + exp(E_LGSN[0])) * exp(RHOPCPM * (E_LPCP[-1] - E_LPMP[-1]) + (1 - RHOPCPM) * (E_LPCP[0] - E_LPMP[0])) ^ SIGIME * exp(E_LPMP[0] - E_LPCP[0]) + + exp(E_LEXYN[0]) = (1 - SE) * exp(E_LPXP[0]) * exp(RHOPWPX * (ALPHAX * SE * E_LER[-1] - E_LPXP[-1]) + (1 - RHOPWPX) * (ALPHAX * SE * E_LER[0] - E_LPXP[0])) ^ SIGEXE * exp(E_LYWY[0]) ^ ALPHAX + + E_INOM[0] = E_INOMW[0] + E_GE[1] - RPREME * E_BWRY[0] + E_ZEPS_RPREME[0] + + E_BWRY[0] = E_TBYN[0] + (1 + E_INOM[0] - E_PHI[1] - E_GY[0] - GPOP0) * E_BWRY[-1] + + exp(E_LBGYN[0]) = exp(E_LIGSN[0]) + exp(E_LGSN[0]) + (1 + E_R[0] - E_GY[0] - GPOP0) * exp(E_LBGYN[-1]) + E_TRW[0] * exp(E_LL[0] - E_LYWR[0]) - E_WS[0] * (E_TW[0] + SSC) - TP * (1 - E_WS[0]) - TVAT * exp(E_LCSN[0]) - E_TAXYN[0] + + E_GG[0] - GY0 = GSLAG * (E_GG[-1] - GY0) + GFLAG * GVECM * (E_LGSN[-1] - log(GSN)) + (E_LYGAP[0] - E_LYGAP[-1]) * GFLAG * G1E + GFLAG * GEXOFLAG * E_ZEPS_G[0] + (1 - GFLAG) * (E_ZEPS_G[0] - E_ZEPS_G[-1]) + + E_GIG[0] - GY0 - GPCPI0 = IGSLAG * (E_GIG[-1] - GY0 - GPCPI0) + IGFLAG * IGVECM * (E_LIGSN[-1] - log(IGSN)) + (E_LYGAP[0] - E_LYGAP[-1]) * IGFLAG * IG1E + IGFLAG * IGEXOFLAG * E_ZEPS_IG[0] + (1 - IGFLAG) * (E_ZEPS_IG[0] - E_ZEPS_IG[-1]) + + E_GIG[0] - E_GI[0] = E_LIGSN[0] - E_LISN[0] - E_LIGSN[-1] + E_LISN[-1] + + E_TRW[0] = TRSN + TRFLAG * TR1E * (1 - exp(E_LL[0]) - (1 - L0)) + E_ZEPS_TR[0] + + E_TAXYN[0] - E_TAXYN[-1] = BGADJ1 * (exp(E_LBGYN[-1]) - BGTAR) + BGADJ2 * (exp(E_LBGYN[0]) - exp(E_LBGYN[-1])) + + E_TW[0] = TW0 * (1 + E_LYGAP[0] * TW1 * TWFLAG) + + E_TRYN[0] = E_TRW[0] * exp(E_LL[0] - E_LYWR[0]) + + E_TRTAXYN[0] = E_TRW[0] * exp(E_LL[0] - E_LYWR[0]) - E_TAXYN[0] + + E_WSW[0] = (1 - E_TW[0] - SSC) * E_WS[0] + + E_INOMW[0] = (1 - RII) * E_EX_INOMW + RII * E_INOMW[-1] + RIP * (E_PHIW[-1] - GPW0) + RIX * (E_GYW[-1] - GYW0) + STD_EPS_INOMW * E_EPS_INOMW[x] + + E_PHIW[0] - GPW0 = RPI * (E_INOMW[-1] - E_EX_INOMW) + (E_PHIW[-1] - GPW0) * RPP + (E_GYW[-1] - GYW0) * RPX + STD_EPS_PW * E_EPS_PW[x] + + E_GYW[0] - GYW0 = (E_INOMW[-1] - E_EX_INOMW) * RXI + (E_PHIW[-1] - GPW0) * RXP + (E_GYW[-1] - GYW0) * RXX + RXY * (E_LYWY[-1] - LYWY0) + STD_EPS_YW * E_EPS_YW[x] + + E_LYWY[0] - E_LYWY[-1] = E_GYW[0] - E_GY[0] + + E_GTFP[0] - GTFP0 = STD_EPS_Y * E_EPS_Y[x] + + E_LOL[0] - LOL = RHOLOL * (E_LOL[-1] - LOL) + STD_EPS_LOL * E_EPS_LOL[x] + + E_PHIPI[0] = GPCPI0 + E_ZEPS_PPI[0] + + E_ZEPS_C[0] = RHOCE * E_ZEPS_C[-1] + STD_EPS_C * E_EPS_C[x] + + E_ZEPS_ETA[0] = RHOETA * E_ZEPS_ETA[-1] + STD_EPS_ETA * E_EPS_ETA[x] + + E_ZEPS_ETAM[0] = RHOETAM * E_ZEPS_ETAM[-1] + STD_EPS_ETAM * E_EPS_ETAM[x] + + E_ZEPS_ETAX[0] = RHOETAX * E_ZEPS_ETAX[-1] + STD_EPS_ETAX * E_EPS_ETAX[x] + + E_ZEPS_EX[0] = RHOEXE * E_ZEPS_EX[-1] + STD_EPS_EX * E_EPS_EX[x] + + E_ZEPS_G[0] = E_ZEPS_G[-1] * RHOGE + STD_EPS_G * E_EPS_G[x] + + E_ZEPS_IG[0] = E_ZEPS_IG[-1] * RHOIG + IGEXOFLAG * STD_EPS_IG * E_EPS_IG[x] + + E_ZEPS_L[0] = RHOLE * E_ZEPS_L[-1] + STD_EPS_L * E_EPS_L[x] + + E_ZEPS_M[0] = STD_EPS_M * E_EPS_M[x] + + E_ZEPS_PPI[0] = STD_EPS_PPI * E_EPS_PPI[x] + RHOPPI1 * E_ZEPS_PPI[-1] + RHOPPI2 * AUX_ENDO_LAG_98_1[-1] + RHOPPI3 * AUX_ENDO_LAG_98_2[-1] + RHOPPI4 * AUX_ENDO_LAG_98_3[-1] + + E_ZEPS_RPREME[0] = RHORPE * E_ZEPS_RPREME[-1] + STD_EPS_RPREME * E_EPS_RPREME[x] + + E_ZEPS_RPREMK[0] = RHORPK * E_ZEPS_RPREMK[-1] + STD_EPS_RPREMK * E_EPS_RPREMK[x] + + E_ZEPS_W[0] = STD_EPS_W * E_EPS_W[x] + + E_ZEPS_TR[0] = RHOTR * E_ZEPS_TR[-1] + TREXOFLAG * STD_EPS_TR * E_EPS_TR[x] + + E_PHIC[0] + E_GC[0] - E_GY[0] - E_PHI[0] = E_LCSN[0] - E_LCSN[-1] + + E_PHIC[0] + E_GCLC[0] - E_GY[0] - E_PHI[0] = E_LCLCSN[0] - E_LCLCSN[-1] + + E_PHIC[0] + E_GCNLC[0] - E_GY[0] - E_PHI[0] = E_LCNLCSN[0] - E_LCNLCSN[-1] + + E_PHIW[0] + E_GE[0] - E_PHI[0] = E_LER[0] - E_LER[-1] + + E_PHIX[0] + E_GEX[0] - E_GY[0] - E_PHI[0] = E_LEXYN[0] - E_LEXYN[-1] + + E_PHIC[0] + E_GG[0] - E_GY[0] - E_PHI[0] = E_LGSN[0] - E_LGSN[-1] + + E_GI[0] - E_GK[-1] = E_LIK[0] - E_LIK[-1] + + E_GIG[0] - E_GKG[-1] = E_LIKG[0] - E_LIKG[-1] + + E_PHIM[0] + E_GIM[0] - E_GY[0] - E_PHI[0] = E_LIMYN[0] - E_LIMYN[-1] + + E_GY[0] + E_PHIPI[0] - E_GK[0] = E_LYKPPI[0] - E_LYKPPI[-1] + + E_GL[0] = E_LL[0] - E_LL[-1] + + E_GTAX[0] - E_GY[0] - E_PHI[0] = log(E_TAXYN[0] / E_TAXYN[-1]) + + E_GTFPUCAP[0] = (1 - ALPHAE) * E_GUCAP[0] + ALPHAE * E_GTFP[0] + + E_GTR[0] - E_GL[0] - E_WRPHI[0] = log(E_TRW[0] / E_TRW[-1]) + + E_GUCAP[0] = log(E_UCAP[0] / E_UCAP[-1]) + + E_GWRY[0] = E_LYWR[-1] - E_LYWR[0] + + E_GY[0] - E_GYPOT[0] = E_LYGAP[0] - E_LYGAP[-1] + + E_BGYN[0] = exp(E_LBGYN[0]) + + E_DBGYN[0] = E_BGYN[0] - E_BGYN[-1] + + E_CLCSN[0] = exp(E_LCLCSN[0]) + + E_GSN[0] = exp(E_LGSN[0]) + + E_LTRYN[0] = log(E_TRYN[0]) + + E_PHIC[0] - E_PHI[0] = E_LPCP[0] - E_LPCP[-1] + + E_PHIM[0] - E_PHI[0] = E_LPMP[0] - E_LPMP[-1] + + E_PHIX[0] - E_PHI[0] = E_LPXP[0] - E_LPXP[-1] + + E_GY[0] + E_PHI[0] - E_WPHI[0] = E_LYWR[0] - E_LYWR[-1] + + E_WPHI[0] = E_PHI[0] + E_WRPHI[0] + + E_GYL[0] = E_GY[0] + GPOP0 + + E_GCL[0] = GPOP0 + E_GC[0] + + E_GIL[0] = GPOP0 + E_GI[0] + + E_GGL[0] = GPOP0 + E_GG[0] + + E_GEXL[0] = GPOP0 + E_GEX[0] + DGEX + + E_GIML[0] = GPOP0 + E_GIM[0] + DGIM + + E_PHIML[0] = E_PHIM[0] + DGPM + + E_PHIXL[0] = E_PHIX[0] + DGPX + + E_LCY[0] = E_LCSN[0] - E_LPCP[0] + + E_LGY[0] = E_LGSN[0] - E_LPCP[0] + + E_LWS[0] = E_LL[0] - E_LYWR[0] + + AUX_ENDO_LAG_104_1[0] = inflationq[-1] + + AUX_ENDO_LAG_104_2[0] = AUX_ENDO_LAG_104_1[-1] + + AUX_ENDO_LAG_98_1[0] = E_ZEPS_PPI[-1] + + AUX_ENDO_LAG_98_2[0] = AUX_ENDO_LAG_98_1[-1] + + AUX_ENDO_LAG_98_3[0] = AUX_ENDO_LAG_98_2[-1] + +end + + +@parameters QUEST3_2009 begin + STD_EPS_INOMW = 0.0023 + + STD_EPS_PW = 0.0029 + + STD_EPS_YW = 0.0044 + + STD_EPS_PPI = 0.00312216772065 + + STD_EPS_C = 0.0597 + + STD_EPS_ETA = 0.15 + + STD_EPS_ETAM = 0.0202 + + STD_EPS_ETAX = 0.0648 + + STD_EPS_EX = 0.0044 + + STD_EPS_G = 0.0048 + + STD_EPS_IG = 0.0056 + + STD_EPS_L = 0.0283 + + STD_EPS_LOL = 0.0048 + + STD_EPS_M = 0.0013 + + STD_EPS_RPREME = 0.0017 + + STD_EPS_RPREMK = 0.007 + + STD_EPS_TR = 0.0022 + + STD_EPS_W = 0.0437 + + STD_EPS_Y = 0.0121 + + A2E = 0.0453 + + G1E = (-0.0754) + + GAMIE = 76.0366 + + GAMI2E = 1.1216 + + GAMLE = 58.2083 + + GAMPE = 61.4415 + + GAMPME = 1.6782 + + GAMPXE = 26.1294 + + GAMWE = 1.2919 + + GSLAG = (-0.4227) + + GVECM = (-0.1567) + + HABE = 0.5634 + + HABLE = 0.8089 + + IG1E = 0.1497 + + IGSLAG = 0.4475 + + IGVECM = (-0.1222) + + ILAGE = 0.9009 + + KAPPAE = 1.9224 + + RHOCE = 0.9144 + + RHOETA = 0.1095 + + RHOETAM = 0.9557 + + RHOETAX = 0.8109 + + RHOGE = 0.2983 + + RHOIG = 0.853 + + RHOLE = 0.975 + + RHOL0 = 0.9334 + + RHOPCPM = 0.6652 + + RHOPWPX = 0.2159 + + RHORPE = 0.9842 + + RHORPK = 0.9148 + + RHOUCAP0 = 0.9517 + + RPREME = 0.02 + + RPREMK = 0.0245 + + SE = 0.8588 + + SFPE = 0.8714 + + SFPME = 0.7361 + + SFPXE = 0.918 + + SFWE = 0.7736 + + SIGC = 4.0962 + + SIGEXE = 2.5358 + + SIGIME = 1.1724 + + SLC = 0.3507 + + TINFE = 1.959 + + TR1E = 0.9183 + + RHOTR = 0.8636 + + TYE1 = 0.4274 + + TYE2 = 0.0783 + + WRLAG = 0.2653 + + ALPHAX = 0.5 + + BETAE = 0.996 + + ALPHAE = 0.52 + + ALPHAGE = 0.9 + + BGADJ2 = 0.004 + + BGTAR = 2.4 + + DELTAE = 0.025 + + DELTAGE = 0.0125 + + DGIM = 0.00738619107021 + + DGEX = 0.00738619107021 + + DGPM = (-0.00396650612294) + + DGPX = (-0.00396650612294) + + LOL = 0.0 + + RHOEXE = 0.975 + + RHOLOL = 0.99 + + SSC = 0.2 + + TAUE = 0.1 + + THETAE = 1.6 + + TP = 0.2 + + TRSN = 0.36 + + TVAT = 0.2 + + TW0 = 0.2 + + TW1 = 0.8 + + ZETE = 0.4 + + GFLAG = 1.0 + + IGFLAG = 1.0 + + TRFLAG = 1.0 + + TWFLAG = 1.0 + + GEXOFLAG = 1.0 + + IGEXOFLAG = 1.0 + + TREXOFLAG = 1.0 + + SLCFLAG = 1.0 + + GAMIFLAG = 1.0 + + A1E = 0.0669 + + OMEGE = 1.4836 + + GSN = 0.203 + + IGSN = 0.025 + + GPCPI0 = 0.0 + + GP0 = 0.005 + + GPOP0 = 0.00113377677398 + + GY0 = 0.003 + + UCAP0 = 1.0 + + L0 = 0.65 + + LYWY0 = 0.0 + + RXY = (-0.0001) + + RII = 0.887131978334279 + + RIP = 0.147455589872832 + + RIX = 0.120095599681076 + + RPI = 0.112067224979767 + + RPP = 0.502758194258928 + + RPX = 0.082535400836409 + + RXI = 0.073730131176521 + + RXP = (-0.302655015002645) + + RXX = 0.49500024655355 + + RHOPPI1 = 0.24797097628284 + + RHOPPI2 = 0.13739098460472 + + RHOPPI3 = 0.10483962746747 + + RHOPPI4 = 0.09282876044442 + + interestq_exog = 1.00901606 + + inflationannual_exog = 1.02 + + BGADJ1 = 0.001*BGADJ2 + + GPW0 = GP0 + + GTFP0 = (ALPHAE+ALPHAGE-1)/ALPHAE*GY0-(2-ALPHAE-ALPHAGE)/ALPHAE*GPCPI0 + + GYW0 = GY0 + + E_EX_R = 1/BETAE-1 + + E_EX_INOMW = GP0+E_EX_R + +end + diff --git a/test/QUEST3_2009.mod b/test/QUEST3_2009.mod new file mode 100644 index 000000000..e8ecdf474 --- /dev/null +++ b/test/QUEST3_2009.mod @@ -0,0 +1,494 @@ +var +E_BGYN E_BWRY E_CLCSN E_DBGYN E_ETA E_GC E_GCL E_GCLC E_GCNLC E_GE E_GEX E_GEXL E_GG E_GGL E_GI E_GIG E_GIL E_GIM E_GIML E_GK E_GKG E_GL E_GSN E_GTAX E_GTFP E_GTFPUCAP E_GTR E_GUC E_GUCAP E_GWRY E_GY E_GYL E_GYPOT E_GYW E_INOM E_INOMW E_LBGYN E_LCLCSN E_LCNLCSN E_LCSN E_LCY E_LER E_LEXYN E_LGSN E_LGY E_LIGSN E_LIK E_LIKG E_LIMYN E_LISN E_LL E_LL0 E_LOL E_LPCP E_LPMP E_LPXP E_LTRYN E_LUCLCYN E_LUCYN E_LWS E_LYGAP E_LYKPPI E_LYWR E_LYWY E_MRY E_PHI E_PHIC E_PHIM E_PHIML E_PHIPI E_PHIW E_PHIX E_PHIXL E_Q E_R E_TAXYN E_TBYN E_TRTAXYN E_TRW E_TRYN E_TW E_UCAP E_UCAP0 E_VL E_VLLC E_WPHI E_WRPHI E_WS E_WSW E_ZEPS_C E_ZEPS_ETA E_ZEPS_ETAM E_ZEPS_ETAX E_ZEPS_EX E_ZEPS_G E_ZEPS_IG E_ZEPS_L E_ZEPS_M E_ZEPS_PPI E_ZEPS_RPREME E_ZEPS_RPREMK E_ZEPS_TR E_ZEPS_W inflation inflationq interest outputgap ; + +varexo +E_EPS_C E_EPS_ETA E_EPS_ETAM E_EPS_ETAX E_EPS_EX E_EPS_G E_EPS_IG E_EPS_INOMW E_EPS_L E_EPS_LOL E_EPS_M E_EPS_PPI E_EPS_PW E_EPS_RPREME E_EPS_RPREMK E_EPS_TR E_EPS_W E_EPS_Y E_EPS_YW ; + +parameters +A1E A2E ALPHAE ALPHAGE ALPHAX BETAE BGADJ1 BGADJ2 BGTAR DELTAE DELTAGE DGEX DGIM DGPM DGPX E_EX_INOMW E_EX_R G1E GAMI2E GAMIE GAMIFLAG GAMLE GAMPE GAMPME GAMPXE GAMWE GEXOFLAG GFLAG GP0 GPCPI0 GPOP0 GPW0 GSLAG GSN GTFP0 GVECM GY0 GYW0 HABE HABLE IG1E IGEXOFLAG IGFLAG IGSLAG IGSN IGVECM ILAGE KAPPAE L0 LOL LYWY0 OMEGE RHOCE RHOETA RHOETAM RHOETAX RHOEXE RHOGE RHOIG RHOL0 RHOLE RHOLOL RHOPCPM RHOPPI1 RHOPPI2 RHOPPI3 RHOPPI4 RHOPWPX RHORPE RHORPK RHOTR RHOUCAP0 RII RIP RIX RPI RPP RPREME RPREMK RPX RXI RXP RXX RXY SE SFPE SFPME SFPXE SFWE SIGC SIGEXE SIGIME SLC SLCFLAG SSC STD_EPS_C STD_EPS_ETA STD_EPS_ETAM STD_EPS_ETAX STD_EPS_EX STD_EPS_G STD_EPS_IG STD_EPS_INOMW STD_EPS_L STD_EPS_LOL STD_EPS_M STD_EPS_PPI STD_EPS_PW STD_EPS_RPREME STD_EPS_RPREMK STD_EPS_TR STD_EPS_W STD_EPS_Y STD_EPS_YW TAUE THETAE TINFE TP TR1E TREXOFLAG TRFLAG TRSN TVAT TW0 TW1 TWFLAG TYE1 TYE2 UCAP0 WRLAG ZETE inflationannual_exog interestq_exog ; + +% Parameter definitions: + STD_EPS_INOMW = 0.0023; + STD_EPS_PW = 0.0029; + STD_EPS_YW = 0.0044; + STD_EPS_PPI = 0.00312216772065; + STD_EPS_C = 0.0597; + STD_EPS_ETA = 0.15; + STD_EPS_ETAM = 0.0202; + STD_EPS_ETAX = 0.0648; + STD_EPS_EX = 0.0044; + STD_EPS_G = 0.0048; + STD_EPS_IG = 0.0056; + STD_EPS_L = 0.0283; + STD_EPS_LOL = 0.0048; + STD_EPS_M = 0.0013; + STD_EPS_RPREME = 0.0017; + STD_EPS_RPREMK = 0.007; + STD_EPS_TR = 0.0022; + STD_EPS_W = 0.0437; + STD_EPS_Y = 0.0121; + A2E = 0.0453; + G1E = -0.0754; + GAMIE = 76.0366; + GAMI2E = 1.1216; + GAMLE = 58.2083; + GAMPE = 61.4415; + GAMPME = 1.6782; + GAMPXE = 26.1294; + GAMWE = 1.2919; + GSLAG = -0.4227; + GVECM = -0.1567; + HABE = 0.5634; + HABLE = 0.8089; + IG1E = 0.1497; + IGSLAG = 0.4475; + IGVECM = -0.1222; + ILAGE = 0.9009; + KAPPAE = 1.9224; + RHOCE = 0.9144; + RHOETA = 0.1095; + RHOETAM = 0.9557; + RHOETAX = 0.8109; + RHOGE = 0.2983; + RHOIG = 0.853; + RHOLE = 0.975; + RHOL0 = 0.9334; + RHOPCPM = 0.6652; + RHOPWPX = 0.2159; + RHORPE = 0.9842; + RHORPK = 0.9148; + RHOUCAP0 = 0.9517; + RPREME = 0.02; + RPREMK = 0.0245; + SE = 0.8588; + SFPE = 0.8714; + SFPME = 0.7361; + SFPXE = 0.918; + SFWE = 0.7736; + SIGC = 4.0962; + SIGEXE = 2.5358; + SIGIME = 1.1724; + SLC = 0.3507; + TINFE = 1.959; + TR1E = 0.9183; + RHOTR = 0.8636; + TYE1 = 0.4274; + TYE2 = 0.0783; + WRLAG = 0.2653; + ALPHAX = 0.5; + BETAE = 0.996; + ALPHAE = 0.52; + ALPHAGE = 0.9; + BGADJ2 = 0.004; + BGTAR = 2.4; + DELTAE = 0.025; + DELTAGE = 0.0125; + DGIM = 0.00738619107021; + DGEX = 0.00738619107021; + DGPM = -0.00396650612294; + DGPX = -0.00396650612294; + LOL = 0.0; + RHOEXE = 0.975; + RHOLOL = 0.99; + SSC = 0.2; + TAUE = 0.1; + THETAE = 1.6; + TP = 0.2; + TRSN = 0.36; + TVAT = 0.2; + TW0 = 0.2; + TW1 = 0.8; + ZETE = 0.4; + GFLAG = 1.0; + IGFLAG = 1.0; + TRFLAG = 1.0; + TWFLAG = 1.0; + GEXOFLAG = 1.0; + IGEXOFLAG = 1.0; + TREXOFLAG = 1.0; + SLCFLAG = 1.0; + GAMIFLAG = 1.0; + A1E = 0.0669; + OMEGE = 1.4836; + GSN = 0.203; + IGSN = 0.025; + GPCPI0 = 0.0; + GP0 = 0.005; + GPOP0 = 0.00113377677398; + GY0 = 0.003; + UCAP0 = 1.0; + L0 = 0.65; + LYWY0 = 0.0; + RXY = -0.0001; + RII = 0.887131978334279; + RIP = 0.147455589872832; + RIX = 0.120095599681076; + RPI = 0.112067224979767; + RPP = 0.502758194258928; + RPX = 0.082535400836409; + RXI = 0.073730131176521; + RXP = -0.302655015002645; + RXX = 0.49500024655355; + RHOPPI1 = 0.24797097628284; + RHOPPI2 = 0.13739098460472; + RHOPPI3 = 0.10483962746747; + RHOPPI4 = 0.09282876044442; + interestq_exog = 1.00901606; + inflationannual_exog = 1.02; + BGADJ1 = 0.001*BGADJ2; + GPW0 = GP0; + GTFP0 = (((ALPHAE + ALPHAGE) - 1) / ALPHAE) * GY0 - (((2 - ALPHAE) - ALPHAGE) / ALPHAE) * GPCPI0; + GYW0 = GY0; + E_EX_R = 1 / BETAE - 1; + E_EX_INOMW = GP0 + E_EX_R; + +model; + interest(0) = ((1 + E_INOM(0)) ^ 4 - interestq_exog ^ 4) / interestq_exog ^ 4; + + inflation(0) = 0.25 * (inflationq(0) + inflationq(-1) + inflationq(-2) + inflationq(-3)); + + inflationq(0) = ((1 + 4 * E_PHIC(0)) - inflationannual_exog) / inflationannual_exog; + + outputgap(0) = E_LYGAP(0); + + E_INOM(0) = ILAGE * E_INOM(-1) + (1 - ILAGE) * (E_EX_R + GP0 + TINFE * (E_PHIC(0) - GP0) + TYE1 * E_LYGAP(-1)) + TYE2 * (E_LYGAP(0) - E_LYGAP(-1)) + E_ZEPS_M(0); + + exp(E_LUCYN(0)) = exp(E_ZEPS_C(0)) * (exp(E_LCNLCSN(0)) * (1 - HABE / ((1 + E_GCNLC(0)) - GY0))) ^ -SIGC * (1 - OMEGE * exp(E_ZEPS_L(0)) * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ KAPPAE) ^ (1 - SIGC); + + exp(E_LUCLCYN(0)) = (1 - OMEGE * exp(E_ZEPS_L(0)) * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ KAPPAE) ^ (1 - SIGC) * exp(E_LCLCSN(0)) ^ -SIGC; + + E_VL(0) = exp(E_ZEPS_L(0)) * OMEGE * KAPPAE * exp(E_ZEPS_C(0)) * (exp(E_LCNLCSN(0)) * (1 - HABE / ((1 + E_GCNLC(0)) - GY0))) ^ (1 - SIGC) * (1 - OMEGE * exp(E_ZEPS_L(0)) * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ KAPPAE) ^ -SIGC * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ (KAPPAE - 1); + + E_VLLC(0) = exp(E_ZEPS_L(0)) * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ (KAPPAE - 1) * OMEGE * KAPPAE * (1 - OMEGE * exp(E_ZEPS_L(0)) * (exp(E_LL(0)) - HABLE * exp(E_LL(-1))) ^ KAPPAE) ^ -SIGC * exp(E_LCLCSN(0)) ^ (1 - SIGC); + + 1 / BETAE - 1 = (E_INOM(0) + E_GUC(1)) - E_PHIC(1); + + E_LUCYN(0) - E_LUCYN(-1) = E_GUC(0) + SIGC * (((E_GY(0) - GY0) - E_PHIC(0)) + E_PHI(0)); + + exp(E_LCLCSN(0)) * (1 + TVAT) = (((1 - E_TW(0)) - SSC) * E_WS(0) + E_WS(0) * E_TRW(0)) - E_TAXYN(0); + + E_WS(0) = exp(E_LL(0) - E_LYWR(0)); + + exp(E_LCSN(0)) = exp(E_LCLCSN(0)) * SLCFLAG * SLC + exp(E_LCNLCSN(0)) * (1 - SLCFLAG * SLC); + + (1 + TVAT) * ((E_VL(0) * (1 - SLC) + E_VLLC(0) * SLC) / (exp(E_LUCYN(0)) * (1 - SLC) + exp(E_LUCLCYN(0)) * SLC)) ^ (1 - WRLAG) * (((((((1 - E_TW(0)) - SSC) / (1 + TVAT)) * (THETAE - 1)) / THETAE) / exp(E_LYWR(-1))) / ((1 + E_GY(0)) - GY0)) ^ WRLAG = (((((1 - E_TW(0)) - SSC) * (THETAE - 1)) / THETAE) / exp(E_LYWR(0)) + (((GAMIFLAG * GAMWE) / THETAE) / exp(E_LYWR(0))) * (((E_WPHI(0) - GP0) - GY0) - (1 - SFWE) * (E_PHI(-1) - GP0))) - (((GAMWE * BETAE * GAMIFLAG) / THETAE) / exp(E_LYWR(0))) * (((E_WPHI(1) - GP0) - GY0) - (1 - SFWE) * (E_PHI(0) - GP0)); + + (1 + E_ZEPS_W(0)) / exp(E_LYWR(0)) = (((E_ETA(0) * ALPHAE) / exp(E_LL(0))) * (1 + E_LOL(0)) - (1 / exp(E_LYWR(0))) * GAMLE * (E_LL(0) - E_LL(-1))) + ((((GAMLE * 1) / exp(E_LYWR(1))) * ((1 + E_GY(1)) - GY0)) / (1 + E_R(0))) * (E_LL(1) - E_LL(0)); + + (GAMIE * (exp(E_LIK(0)) - (GY0 + DELTAE + GPOP0 + GPCPI0)) + GAMI2E * ((E_GI(0) - GY0) - GPCPI0)) - (GAMI2E / (1 + E_INOM(0))) * ((E_GI(1) - GY0) - GPCPI0) = E_Q(0) - 1; + + E_ETA(0) * (1 - TP) * (1 - ALPHAE) * exp(E_LYKPPI(0)) = (E_Q(0) - ((((((GPCPI0 + 1) - E_R(0)) - DELTAE) - RPREMK) - E_ZEPS_RPREMK(0)) - E_PHIPI(1)) * E_Q(1)) + (1 - TP) * (A1E * (E_UCAP(0) - UCAP0) + A2E * (E_UCAP(0) - UCAP0) ^ 2); + + exp(E_LYKPPI(0)) * E_ETA(0) * (1 - ALPHAE) = E_UCAP(0) * (A1E + (E_UCAP(0) - UCAP0) * 2 * A2E); + + E_ETA(0) = (1 - (TAUE + E_ZEPS_ETA(0))) - GAMIFLAG * GAMPE * (BETAE * ((SFPE * E_PHI(1) + E_PHI(-1) * (1 - SFPE)) - GP0) - (E_PHI(0) - GP0)); + + E_MRY(0) = (1 + E_INOM(0)) ^ -ZETE; + + E_GK(0) - (GY0 + GPCPI0) = exp(E_LIK(0)) - (GY0 + DELTAE + GPOP0 + GPCPI0); + + E_GKG(0) - (GY0 + GPCPI0) = exp(E_LIKG(0)) - (GPCPI0 + GY0 + GPOP0 + DELTAGE); + + E_LISN(0) = ((GPCPI0 + GY0 + E_LIK(0)) - E_LYKPPI(0)) - E_GK(0); + + E_GY(0) = (1 - ALPHAE) * (E_GK(0) + E_GUCAP(0)) + ALPHAE * (E_GTFP(0) + E_GL(0) * (1 + LOL)) + E_GKG(0) * (1 - ALPHAGE); + + E_R(0) = E_INOM(0) - E_PHI(1); + + E_LYGAP(0) = (1 - ALPHAE) * (log(E_UCAP(0)) - log(E_UCAP0(0))) + ALPHAE * (E_LL(0) - E_LL0(0)); + + E_LL0(0) = RHOL0 * E_LL0(-1) + E_LL(0) * (1 - RHOL0); + + E_UCAP0(0) = RHOUCAP0 * E_UCAP0(-1) + E_UCAP(0) * (1 - RHOUCAP0); + + exp(E_LPCP(0)) = (SE + (1 - SE) * exp(E_LPMP(0)) ^ (1 - SIGIME)) ^ (1 / (1 - SIGIME)); + + exp(E_LPMP(0)) = (1 + E_ZEPS_ETAM(0) + GAMIFLAG * GAMPME * ((GP0 + BETAE * ((SFPME * E_PHIM(1) + (1 - SFPME) * E_PHIM(-1)) - GP0)) - E_PHIM(0))) * exp(E_LER(0)) ^ ALPHAX; + + exp(E_LPXP(0)) = 1 + E_ZEPS_ETAX(0) + GAMIFLAG * GAMPXE * ((GP0 + BETAE * ((SFPXE * E_PHIX(1) + (1 - SFPXE) * E_PHIX(-1)) - GP0)) - E_PHIX(0)); + + 1 = exp(E_LCSN(0)) + exp(E_LISN(0)) + exp(E_LIGSN(0)) + exp(E_LGSN(0)) + E_TBYN(0); + + E_TBYN(0) = (exp(E_LEXYN(0)) - exp(E_LIMYN(0))) + E_ZEPS_EX(0); + + exp(E_LIMYN(0)) = (exp(E_LCSN(0)) + exp(E_LISN(0)) + exp(E_LIGSN(0)) + exp(E_LGSN(0))) * (1 - SE) * exp(RHOPCPM * (E_LPCP(-1) - E_LPMP(-1)) + (1 - RHOPCPM) * (E_LPCP(0) - E_LPMP(0))) ^ SIGIME * exp(E_LPMP(0) - E_LPCP(0)); + + exp(E_LEXYN(0)) = exp(E_LPXP(0)) * (1 - SE) * exp(RHOPWPX * (E_LER(-1) * SE * ALPHAX - E_LPXP(-1)) + (1 - RHOPWPX) * (E_LER(0) * SE * ALPHAX - E_LPXP(0))) ^ SIGEXE * exp(E_LYWY(0)) ^ ALPHAX; + + E_INOM(0) = ((E_INOMW(0) + E_GE(1)) - RPREME * E_BWRY(0)) + E_ZEPS_RPREME(0); + + E_BWRY(0) = E_TBYN(0) + ((((1 + E_INOM(0)) - E_PHI(1)) - E_GY(0)) - GPOP0) * E_BWRY(-1); + + exp(E_LBGYN(0)) = ((((exp(E_LIGSN(0)) + exp(E_LGSN(0)) + (((1 + E_R(0)) - E_GY(0)) - GPOP0) * exp(E_LBGYN(-1)) + E_TRW(0) * exp(E_LL(0) - E_LYWR(0))) - E_WS(0) * (E_TW(0) + SSC)) - TP * (1 - E_WS(0))) - TVAT * exp(E_LCSN(0))) - E_TAXYN(0); + + E_GG(0) - GY0 = GSLAG * (E_GG(-1) - GY0) + GFLAG * GVECM * (E_LGSN(-1) - log(GSN)) + (E_LYGAP(0) - E_LYGAP(-1)) * GFLAG * G1E + GFLAG * GEXOFLAG * E_ZEPS_G(0) + (1 - GFLAG) * (E_ZEPS_G(0) - E_ZEPS_G(-1)); + + (E_GIG(0) - GY0) - GPCPI0 = IGSLAG * ((E_GIG(-1) - GY0) - GPCPI0) + IGFLAG * IGVECM * (E_LIGSN(-1) - log(IGSN)) + (E_LYGAP(0) - E_LYGAP(-1)) * IGFLAG * IG1E + IGFLAG * IGEXOFLAG * E_ZEPS_IG(0) + (1 - IGFLAG) * (E_ZEPS_IG(0) - E_ZEPS_IG(-1)); + + E_GIG(0) - E_GI(0) = ((E_LIGSN(0) - E_LISN(0)) - E_LIGSN(-1)) + E_LISN(-1); + + E_TRW(0) = TRSN + TRFLAG * TR1E * ((1 - exp(E_LL(0))) - (1 - L0)) + E_ZEPS_TR(0); + + E_TAXYN(0) - E_TAXYN(-1) = BGADJ1 * (exp(E_LBGYN(-1)) - BGTAR) + BGADJ2 * (exp(E_LBGYN(0)) - exp(E_LBGYN(-1))); + + E_TW(0) = TW0 * (1 + E_LYGAP(0) * TW1 * TWFLAG); + + E_TRYN(0) = E_TRW(0) * exp(E_LL(0) - E_LYWR(0)); + + E_TRTAXYN(0) = E_TRW(0) * exp(E_LL(0) - E_LYWR(0)) - E_TAXYN(0); + + E_WSW(0) = ((1 - E_TW(0)) - SSC) * E_WS(0); + + E_INOMW(0) = (1 - RII) * E_EX_INOMW + RII * E_INOMW(-1) + RIP * (E_PHIW(-1) - GPW0) + RIX * (E_GYW(-1) - GYW0) + STD_EPS_INOMW * E_EPS_INOMW; + + E_PHIW(0) - GPW0 = RPI * (E_INOMW(-1) - E_EX_INOMW) + (E_PHIW(-1) - GPW0) * RPP + (E_GYW(-1) - GYW0) * RPX + STD_EPS_PW * E_EPS_PW; + + E_GYW(0) - GYW0 = (E_INOMW(-1) - E_EX_INOMW) * RXI + (E_PHIW(-1) - GPW0) * RXP + (E_GYW(-1) - GYW0) * RXX + RXY * (E_LYWY(-1) - LYWY0) + STD_EPS_YW * E_EPS_YW; + + E_LYWY(0) - E_LYWY(-1) = E_GYW(0) - E_GY(0); + + E_GTFP(0) - GTFP0 = STD_EPS_Y * E_EPS_Y; + + E_LOL(0) - LOL = RHOLOL * (E_LOL(-1) - LOL) + STD_EPS_LOL * E_EPS_LOL; + + E_PHIPI(0) = GPCPI0 + E_ZEPS_PPI(0); + + E_ZEPS_C(0) = RHOCE * E_ZEPS_C(-1) + STD_EPS_C * E_EPS_C; + + E_ZEPS_ETA(0) = RHOETA * E_ZEPS_ETA(-1) + STD_EPS_ETA * E_EPS_ETA; + + E_ZEPS_ETAM(0) = RHOETAM * E_ZEPS_ETAM(-1) + STD_EPS_ETAM * E_EPS_ETAM; + + E_ZEPS_ETAX(0) = RHOETAX * E_ZEPS_ETAX(-1) + STD_EPS_ETAX * E_EPS_ETAX; + + E_ZEPS_EX(0) = RHOEXE * E_ZEPS_EX(-1) + STD_EPS_EX * E_EPS_EX; + + E_ZEPS_G(0) = E_ZEPS_G(-1) * RHOGE + STD_EPS_G * E_EPS_G; + + E_ZEPS_IG(0) = E_ZEPS_IG(-1) * RHOIG + IGEXOFLAG * STD_EPS_IG * E_EPS_IG; + + E_ZEPS_L(0) = RHOLE * E_ZEPS_L(-1) + STD_EPS_L * E_EPS_L; + + E_ZEPS_M(0) = STD_EPS_M * E_EPS_M; + + E_ZEPS_PPI(0) = STD_EPS_PPI * E_EPS_PPI + RHOPPI1 * E_ZEPS_PPI(-1) + RHOPPI2 * E_ZEPS_PPI(-2) + RHOPPI3 * E_ZEPS_PPI(-3) + RHOPPI4 * E_ZEPS_PPI(-4); + + E_ZEPS_RPREME(0) = RHORPE * E_ZEPS_RPREME(-1) + STD_EPS_RPREME * E_EPS_RPREME; + + E_ZEPS_RPREMK(0) = RHORPK * E_ZEPS_RPREMK(-1) + STD_EPS_RPREMK * E_EPS_RPREMK; + + E_ZEPS_W(0) = STD_EPS_W * E_EPS_W; + + E_ZEPS_TR(0) = RHOTR * E_ZEPS_TR(-1) + TREXOFLAG * STD_EPS_TR * E_EPS_TR; + + ((E_PHIC(0) + E_GC(0)) - E_GY(0)) - E_PHI(0) = E_LCSN(0) - E_LCSN(-1); + + ((E_PHIC(0) + E_GCLC(0)) - E_GY(0)) - E_PHI(0) = E_LCLCSN(0) - E_LCLCSN(-1); + + ((E_PHIC(0) + E_GCNLC(0)) - E_GY(0)) - E_PHI(0) = E_LCNLCSN(0) - E_LCNLCSN(-1); + + (E_PHIW(0) + E_GE(0)) - E_PHI(0) = E_LER(0) - E_LER(-1); + + ((E_PHIX(0) + E_GEX(0)) - E_GY(0)) - E_PHI(0) = E_LEXYN(0) - E_LEXYN(-1); + + ((E_PHIC(0) + E_GG(0)) - E_GY(0)) - E_PHI(0) = E_LGSN(0) - E_LGSN(-1); + + E_GI(0) - E_GK(-1) = E_LIK(0) - E_LIK(-1); + + E_GIG(0) - E_GKG(-1) = E_LIKG(0) - E_LIKG(-1); + + ((E_PHIM(0) + E_GIM(0)) - E_GY(0)) - E_PHI(0) = E_LIMYN(0) - E_LIMYN(-1); + + (E_PHIPI(0) + E_GY(0)) - E_GK(0) = E_LYKPPI(0) - E_LYKPPI(-1); + + E_GL(0) = E_LL(0) - E_LL(-1); + + (E_GTAX(0) - E_GY(0)) - E_PHI(0) = log(E_TAXYN(0) / E_TAXYN(-1)); + + E_GTFPUCAP(0) = (1 - ALPHAE) * E_GUCAP(0) + ALPHAE * E_GTFP(0); + + (E_GTR(0) - E_GL(0)) - E_WRPHI(0) = log(E_TRW(0) / E_TRW(-1)); + + E_GUCAP(0) = log(E_UCAP(0) / E_UCAP(-1)); + + E_GWRY(0) = E_LYWR(-1) - E_LYWR(0); + + E_GY(0) - E_GYPOT(0) = E_LYGAP(0) - E_LYGAP(-1); + + E_BGYN(0) = exp(E_LBGYN(0)); + + E_DBGYN(0) = E_BGYN(0) - E_BGYN(-1); + + E_CLCSN(0) = exp(E_LCLCSN(0)); + + E_GSN(0) = exp(E_LGSN(0)); + + E_LTRYN(0) = log(E_TRYN(0)); + + E_PHIC(0) - E_PHI(0) = E_LPCP(0) - E_LPCP(-1); + + E_PHIM(0) - E_PHI(0) = E_LPMP(0) - E_LPMP(-1); + + E_PHIX(0) - E_PHI(0) = E_LPXP(0) - E_LPXP(-1); + + (E_PHI(0) + E_GY(0)) - E_WPHI(0) = E_LYWR(0) - E_LYWR(-1); + + E_WPHI(0) = E_PHI(0) + E_WRPHI(0); + + E_GYL(0) = E_GY(0) + GPOP0; + + E_GCL(0) = GPOP0 + E_GC(0); + + E_GIL(0) = GPOP0 + E_GI(0); + + E_GGL(0) = GPOP0 + E_GG(0); + + E_GEXL(0) = GPOP0 + E_GEX(0) + DGEX; + + E_GIML(0) = GPOP0 + E_GIM(0) + DGIM; + + E_PHIML(0) = E_PHIM(0) + DGPM; + + E_PHIXL(0) = E_PHIX(0) + DGPX; + + E_LCY(0) = E_LCSN(0) - E_LPCP(0); + + E_LGY(0) = E_LGSN(0) - E_LPCP(0); + + E_LWS(0) = E_LL(0) - E_LYWR(0); + +end; + +shocks; +var E_EPS_C = 1; +var E_EPS_ETA = 1; +var E_EPS_ETAM = 1; +var E_EPS_ETAX = 1; +var E_EPS_EX = 1; +var E_EPS_G = 1; +var E_EPS_IG = 1; +var E_EPS_INOMW = 1; +var E_EPS_L = 1; +var E_EPS_LOL = 1; +var E_EPS_M = 1; +var E_EPS_PPI = 1; +var E_EPS_PW = 1; +var E_EPS_RPREME = 1; +var E_EPS_RPREMK = 1; +var E_EPS_TR = 1; +var E_EPS_W = 1; +var E_EPS_Y = 1; +var E_EPS_YW = 1; +end; + +initval; + E_BGYN = 2.4; + E_BWRY = 3.5136036355069597e-13; + E_CLCSN = 0.3862100121811388; + E_DBGYN = 0.0; + E_ETA = 0.8999999999999975; + E_GC = 0.0029999999999974747; + E_GCL = 0.004133776773977475; + E_GCLC = 0.0029999999999974747; + E_GCNLC = 0.0029999999999974747; + E_GE = -8.879205119311964e-15; + E_GEX = 0.0029999999999974747; + E_GEXL = 0.011519967844187476; + E_GG = 0.0029999999999974747; + E_GGL = 0.004133776773977475; + E_GI = 0.0029999999999966243; + E_GIG = 0.002999999999996441; + E_GIL = 0.004133776773976625; + E_GIM = 0.0029999999999974747; + E_GIML = 0.011519967844187476; + E_GK = 0.0029999999999968654; + E_GKG = 0.0029999999999963185; + E_GL = 0.0; + E_GSN = 0.2030000000000047; + E_GTAX = 0.007999999999987071; + E_GTFP = 0.002423076923076923; + E_GTFPUCAP = 0.00126; + E_GTR = 0.0029999999999974743; + E_GUC = 1.034423093305421e-14; + E_GUCAP = 0.0; + E_GWRY = 0.0; + E_GY = 0.0029999999999974747; + E_GYL = 0.004133776773977475; + E_GYPOT = 0.0029999999999974747; + E_GYW = 0.0029999999999974747; + E_INOM = 0.009016064257007416; + E_INOMW = 0.009016064257023322; + E_LBGYN = 0.8754687373538999; + E_LCLCSN = -0.9513739844632308; + E_LCNLCSN = -0.3702009800996817; + E_LCSN = -0.5381154211972545; + E_LCY = -0.5381154211968358; + E_LER = -5.9306333075950735e-12; + E_LEXYN = -1.9575779539225562; + E_LGSN = -1.5945492999403266; + E_LGY = -1.5945492999399078; + E_LIGSN = -3.6888794541139207; + E_LIK = -3.5358570640298757; + E_LIKG = -4.096319905490043; + E_LIMYN = -1.9575779539225562; + E_LISN = -1.670502596381497; + E_LL = -0.4307690924404956; + E_LL0 = -0.43076909244049555; + E_LOL = 0.0; + E_LPCP = -4.187892334820806e-13; + E_LPMP = -2.9653097525568943e-12; + E_LPXP = 1.110223024625156e-15; + E_LTRYN = -1.7809611512085037; + E_LUCLCYN = 3.9814892519114324; + E_LUCYN = 4.9955643106167855; + E_LWS = -0.759286983064493; + E_LYGAP = -8.657831554752552e-16; + E_LYKPPI = -1.8653544676483753; + E_LYWR = 0.3285178906239975; + E_LYWY = 1.379755684705053e-11; + E_MRY = 0.9964161724095931; + E_PHI = 0.004999999999989597; + E_PHIC = 0.004999999999989597; + E_PHIM = 0.004999999999989597; + E_PHIML = 0.001033493877049597; + E_PHIPI = 0.0; + E_PHIW = 0.004999999999998476; + E_PHIX = 0.004999999999989597; + E_PHIXL = 0.001033493877049597; + E_Q = 0.9999999999997612; + E_R = 0.00401606425701782; + E_TAXYN = -0.014175876237826998; + E_TBYN = 4.1359512676761533e-17; + E_TRTAXYN = 0.18265201461736735; + E_TRW = 0.35999174867423256; + E_TRYN = 0.16847613837954037; + E_TW = 0.19999999999999987; + E_UCAP = 0.9999456974357295; + E_UCAP0 = 0.9999456974357311; + E_VL = 19.06927878595193; + E_VLLC = 8.860191438118742; + E_WPHI = 0.007999999999987071; + E_WRPHI = 0.0029999999999974743; + E_WS = 0.4679999999999987; + E_WSW = 0.2807999999999993; + E_ZEPS_C = 0.0; + E_ZEPS_ETA = 0.0; + E_ZEPS_ETAM = 0.0; + E_ZEPS_ETAX = 0.0; + E_ZEPS_EX = 0.0; + E_ZEPS_G = 0.0; + E_ZEPS_IG = 0.0; + E_ZEPS_L = 0.0; + E_ZEPS_M = 0.0; + E_ZEPS_PPI = 0.0; + E_ZEPS_RPREME = 0.0; + E_ZEPS_RPREMK = 0.0; + E_ZEPS_TR = 0.0; + E_ZEPS_W = 0.0; + inflation = -4.081362154557982e-14; + inflationq = -4.081362154557982e-14; + interest = 1.687587602994256e-8; + outputgap = -8.657831554752552e-16; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/SGU_2003_debt_premium.jl b/test/SGU_2003_debt_premium.jl new file mode 100644 index 000000000..938a110eb --- /dev/null +++ b/test/SGU_2003_debt_premium.jl @@ -0,0 +1,57 @@ +using MacroModelling + +@model SGU_2003_debt_premium begin + d[0] = (1 + r[-1]) * d[-1] - y[0] + c[0] + i[0] + phi / 2 * (k[0] - k[-1]) ^ 2 + + y[0] = exp(a[0]) * k[-1] ^ alpha * h[0] ^ (1 - alpha) + + k[0] = i[0] + k[-1] * (1 - delta) + + lambda[0] = beta * (1 + r[0]) * lambda[1] + + (c[0] - h[0] ^ omega / omega) ^ (-gamma) = lambda[0] + + (c[0] - h[0] ^ omega / omega) ^ (-gamma) * h[0] ^ (omega - 1) = lambda[0] * y[0] * (1 - alpha) / h[0] + + lambda[0] * (1 + phi * (k[0] - k[-1])) = beta * lambda[1] * (1 + alpha * y[1] / k[0] - delta + phi * (k[1] - k[0])) + + a[0] = rho * a[-1] + sigma__tfp * e[x] + + r[0] = rbar + riskpremium[0] + + riskpremium[0] = psi__2 * (exp(d[0] - dbar) - 1) + + tb_y[0] = 1 - (i[0] + c[0] + phi / 2 * (k[0] - k[-1]) ^ 2) / y[0] + + ca_y[0] = 1 / y[0] * (d[-1] - d[0]) + + util[0] = ((c[0] - h[0] ^ omega * omega ^ (-1)) ^ (1 - gamma) - 1) / (1 - gamma) + +end + + +@parameters SGU_2003_debt_premium begin + gamma = 2.0 + + omega = 1.455 + + alpha = 0.32 + + phi = 0.028 + + rbar = 0.04 + + delta = 0.1 + + rho = 0.42 + + sigma__tfp = 0.0129 + + psi__2 = 0.000742 + + dbar = 0.7442 + + beta = 1/(1+rbar) + +end + diff --git a/test/SGU_2003_debt_premium.mod b/test/SGU_2003_debt_premium.mod new file mode 100644 index 000000000..aa3edf0c8 --- /dev/null +++ b/test/SGU_2003_debt_premium.mod @@ -0,0 +1,72 @@ +var +a c ca_y d h i k r riskpremium tb_y util y lambda ; + +varexo +e ; + +parameters +dbar rbar alpha beta gamma delta rho sigma__tfp psi__2 omega phi ; + +% Parameter definitions: + gamma = 2.0; + omega = 1.455; + alpha = 0.32; + phi = 0.028; + rbar = 0.04; + delta = 0.1; + rho = 0.42; + sigma__tfp = 0.0129; + psi__2 = 0.000742; + dbar = 0.7442; + beta = 1 / (1 + rbar); + +model; + d(0) = ((1 + r(-1)) * d(-1) - y(0)) + c(0) + i(0) + (phi / 2) * (k(0) - k(-1)) ^ 2; + + y(0) = exp(a(0)) * k(-1) ^ alpha * h(0) ^ (1 - alpha); + + k(0) = i(0) + k(-1) * (1 - delta); + + lambda(0) = beta * (1 + r(0)) * lambda(1); + + (c(0) - h(0) ^ omega / omega) ^ -gamma = lambda(0); + + (c(0) - h(0) ^ omega / omega) ^ -gamma * h(0) ^ (omega - 1) = (y(0) * (1 - alpha) * lambda(0)) / h(0); + + lambda(0) * (1 + phi * (k(0) - k(-1))) = beta * lambda(1) * (((1 + (alpha * y(1)) / k(0)) - delta) + phi * (k(1) - k(0))); + + a(0) = rho * a(-1) + sigma__tfp * e; + + r(0) = rbar + riskpremium(0); + + riskpremium(0) = psi__2 * (exp(d(0) - dbar) - 1); + + tb_y(0) = 1 - ((phi / 2) * (k(0) - k(-1)) ^ 2 + c(0) + i(0)) / y(0); + + ca_y(0) = (1 / y(0)) * (d(-1) - d(0)); + + util(0) = ((c(0) - h(0) ^ omega * omega ^ -1) ^ (1 - gamma) - 1) / (1 - gamma); + +end; + +shocks; +var e = 1; +end; + +initval; + a = 0.0; + c = 1.116950781911716; + ca_y = 0.0; + d = 0.7442000000001217; + h = 1.0074179936054608; + i = 0.3397685279738418; + k = 3.397685279738418; + r = 0.04000000000000009; + riskpremium = 9.020562075079397e-17; + tb_y = 0.020025734361833747; + util = -1.3683490243936811; + y = 1.4864873098855629; + lambda = 5.609077101346496; +end; + +stoch_simul(order = 1, irf = 40); diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index 6b542fb80..5a2f5340f 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -141,6 +141,47 @@ end @test all(isfinite, pf_0) @test abs(kal_BB - Statistics.mean(pf_0)) < 8 + # ---- does the equivalence carry to the states and shocks? ---- + # Theory: with P₁ = BB' the Kalman gain is P C'F⁻¹ = BB'C'(CBB'C')⁻¹ = B Z⁺, + # which is exactly the inversion filter's state recursion, so the two must + # track the same path. The estimates path works in the full nVars basis + # (unlike the likelihood path, which uses union(past, observables)), so the + # prior has to be built there. + Bf = S1[:, nP+1:end] + P1_est = Bf * Bf' + + inv_v = collect(get_estimated_variables(m, data; filter = :inversion)) + kal_v = collect(get_estimated_variables(m, data; filter = :kalman, smooth = true, + initial_covariance = P1_est)) + inv_s = collect(get_estimated_shocks(m, data; filter = :inversion)) + kal_s = collect(get_estimated_shocks(m, data; filter = :kalman, smooth = true, + initial_covariance = P1_est)) + + reldev(a, b) = maximum(abs, a .- b) / max(maximum(abs, b), eps()) + + # states and shocks agree to machine precision — a far sharper check on the + # inversion filter's implementation than any likelihood comparison, since it + # pins the whole path rather than one scalar + @test reldev(inv_v, kal_v) < 1e-8 + @test reldev(inv_s, kal_s) < 1e-8 + + # the shocks also match the Kalman filter's *filtered* estimates; the states + # do not, because a single period's seven observations do not pin all forty + # variables contemporaneously even though the full sample does + @test reldev(inv_s, collect(get_estimated_shocks(m, data; filter = :kalman, smooth = false, + initial_covariance = P1_est))) < 1e-8 + + # ... and "the full sample pins the state exactly" is directly checkable: + # under P₁ = BB' the smoothed dispersion collapses, which is precisely the + # assumption the inversion filter makes. + sd_sm = collect(get_estimated_variable_standard_deviations(m, data; filter = :kalman, + smooth = true, initial_covariance = P1_est)) + @test maximum(sd_sm) < 1e-3 + + # with the ergodic prior the two are far apart, so the agreement above is not + # an artefact of every initial covariance giving the same answer + @test reldev(inv_v, collect(get_estimated_variables(m, data; filter = :kalman, smooth = true))) > 0.1 + # Var(x₀) = BB' ⇒ P₁ = A BB' A' + BB' Bfull = S1[:, nP+1:end] kal_shift = get_loglikelihood(m, data, p; filter = :kalman, From 6344ea7cbedc3c632a66ed288ad92109a3700174 Mon Sep 17 00:00:00 2001 From: thorek1 Date: Mon, 27 Jul 2026 15:01:17 +0200 Subject: [PATCH 22/24] Add higher-order equivalence checks for the particle filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Above first order there is no Kalman filter to compare against, so the higher-order particle machinery — the pruned and non-pruned second- and third-order transitions, their Kronecker scratch, the pruned Vector{Vector} particle layout — had nothing exact to be checked against. Two references close that gap. 1. A *linear* model has no higher-order solution terms, so every perturbation order describes the same system and the exact answer is the Kalman likelihood at every order. Smets-Wouters (2007) log-linearised is such a model, and the tests assert that premise rather than assume it: the inversion filter is deterministic, so it must return an identical value at all five orders (it does, to 1e-10). Deviations from the Kalman value are then -2.7 / -0.3 / -0.3 at first, pruned second and second order — all within Monte-Carlo error and all on the expected downward side. Pruned and non-pruned agree exactly, which is itself a check on the pruning code. 2. On a genuinely nonlinear model the reference is the inversion filter. As H -> 0 the measurement density collapses onto the change of variables y -> eps, so p(y|x) -> N(eps_hat;0,I)/|det Z|, which is the inversion filter's per-period term; a degenerate initial cloud matches its other assumption, that x0 is known. The test asserts the *direction* rather than a tolerance, because the observed gap is non-monotonic (-35.9, -1.6, -3.2 as the measurement-error variance falls through 1e-4, 1e-5, 1e-6). That is not a defect: shrinking H is exactly what degenerates the importance weights, so the approach stalls at a floor set by particle noise. Third order on forty variables costs minutes per evaluation — the first version of this took 19 minutes — so the third-order sweep runs on a small linear model instead, where all five orders finish in seconds. SW07 keeps first and second order, where it is cheap and the dimensionality is the point. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019YdYHujHfAMxP3jt9kwzzT --- docs/src/filters.md | 32 +++++++++++++ test/test_particle_filter.jl | 77 +++++++++++++++++++++++++++++++ test/test_particle_filter_sw07.jl | 57 +++++++++++++++++++++++ 3 files changed, 166 insertions(+) diff --git a/docs/src/filters.md b/docs/src/filters.md index ee01b3f2a..2d4963a09 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -300,6 +300,38 @@ The first row is the sharp statement, and it is the one to remember: **the inver The second row is why the two filters normally *disagree* even on a square system: the default ergodic prior is a genuinely different starting point, and the error decays as ``\delta_t = (I - BZ^{-1}C)A\,\delta_{t-1}``. The spectral radius of that matrix is the **invertibility** (fundamentalness) condition — the "poor man's invertibility condition" of Fernández-Villaverde, Rubio-Ramírez, Sargent & Watson. If it exceeds one the inversion filter's state estimate never converges and its likelihood is wrong at any sample length; if it is close to one (0.98 in the RBC example) convergence is real but slow. +### Equivalences above first order + +Above first order there is no Kalman filter to check against, but two exact references remain. + +**A linear model filtered at a nonlinear order.** If the model's higher-order solution terms vanish, every perturbation order describes the same system, so a particle filter run at `:pruned_second_order` or `:third_order` must still reproduce the *Kalman* likelihood. Smets-Wouters (2007) in its log-linearised form is exactly such a model — the inversion filter returns an identical value at all five orders — which makes it a rare thing: complex enough to be a real test (40 variables, 7 shocks, 184 periods) with a known exact answer, yet exercising the pruned and non-pruned second- and third-order transitions and the pruned particle layout. Deviations from the Kalman value at ``H = 2s_i``, averaged over seeds: + +| order | deviation | +|---|---| +| `first_order` | ``-2.7`` | +| `pruned_second_order` | ``-0.3`` | +| `second_order` | ``-0.3`` | + +All within Monte-Carlo error and on the expected (downward) side of it. Pruned and non-pruned agree *exactly*, as they must when there is nothing to prune. + +Third order on forty variables costs minutes per evaluation, so the package tests it the same way but on a small linear model, where the whole sweep — all five orders against the Kalman value — runs in seconds. The logic is identical; only the model is cheaper. + +**The zero-measurement-error limit.** On a genuinely nonlinear model the reference is the inversion filter. As ``H \to 0`` the measurement density collapses onto the change of variables ``y \mapsto \varepsilon``, + +```math +p(y_t \mid x_{t-1}) \;\longrightarrow\; N(\hat\varepsilon_t; 0, I)\,/\,|\det Z(x_{t-1})|, +``` + +which is exactly the inversion filter's per-period contribution. Give the particle filter a degenerate initial cloud (`initial_covariance = 0`, matching the inversion filter's assumption that ``x_0`` is known) and the two must agree in that limit, at any order. On the package's RBC example at `:pruned_second_order`, against an inversion value of ``386.6``: + +| measurement-error variance | deviation | +|---|---| +| ``10^{-4}`` | ``-35.9`` | +| ``10^{-5}`` | ``-1.6`` | +| ``10^{-6}`` | ``-3.2`` | + +The non-monotonicity is the interesting part and is not a defect: shrinking ``H`` is precisely what makes the importance weights degenerate, so the approach to the inversion filter stalls at a floor set by particle noise rather than continuing to zero. This is the same tension noted earlier — as measurement error vanishes the particle filter degenerates towards the inversion filter's problem, and that is exactly where it needs the most particles. + ### Does the equivalence carry to the states? Yes, and it is a sharper check than the likelihood: a likelihood is one scalar, whereas the states and shocks pin the whole path. diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index 815489018..ca93eef72 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -329,6 +329,83 @@ threw(f) = try; f(); false; catch; true; end @test threw(() -> get_estimated_variable_standard_deviations(RBC_pf, data; filter = :inversion)) end + @testset "Linear model: every perturbation order reproduces the Kalman likelihood" begin + # A *linear* model has no higher-order solution terms, so every + # perturbation order describes the same system and the exact likelihood is + # the Kalman one — at every order. That turns the Kalman filter into a + # reference for the higher-order particle machinery (the pruned and + # non-pruned second- and third-order transitions, their Kronecker scratch, + # and the pruned `Vector{Vector}` particle layout), which otherwise has + # nothing exact to be checked against. Deviations beyond Monte-Carlo error + # are a bug in the transition code, not a property of the model. + @model LIN_pf begin + zs[0] = rho_l * zs[-1] + sig_l * e1[x] + ys[0] = zs[0] + 0 * ys[1] + end + @parameters LIN_pf begin + rho_l = 0.5 + sig_l = 0.01 + end + + Random.seed!(4242) + dlin = simulate(LIN_pf, periods = 60)([:ys], :, :simulate) + plin = LIN_pf.parameter_values + mel = 5e-6 # variance + + # Premise: the model really is linear. The inversion filter is + # deterministic, so it must return the identical value at every order. + inv_lls = [get_loglikelihood(LIN_pf, dlin, plin; filter = :inversion, algorithm = a) + for a in (:first_order, :pruned_second_order, :second_order, + :pruned_third_order, :third_order)] + @test all(isapprox.(inv_lls, inv_lls[1], rtol = 1e-10)) + + kal_lin = get_loglikelihood(LIN_pf, dlin, plin; filter = :kalman, + measurement_error = mel) + @test isfinite(kal_lin) + + for algo in (:first_order, :pruned_second_order, :second_order, + :pruned_third_order, :third_order) + lls = [get_loglikelihood(LIN_pf, dlin, plin; filter = :bootstrap_particle, + algorithm = algo, measurement_error = mel, + n_particles = 20_000, + particle_rng = Random.Xoshiro(600 + s)) for s in 1:3] + @test all(isfinite, lls) + @test abs(kal_lin - Statistics.mean(lls)) < 8.0 + end + end + + @testset "Higher order: particle filter approaches the inversion filter" begin + # At higher order there is no Kalman filter to check against, but there is + # still an exact reference. As H -> 0 the measurement density collapses onto + # the change of variables y -> eps, so + # p(y_t | x_{t-1}) -> N(eps_hat; 0, I) / |det Z(x_{t-1})|, + # which is precisely the inversion filter's per-period contribution. Giving + # the particle filter a degenerate initial cloud (`initial_covariance = 0`) + # matches the inversion filter's other assumption — that x_0 is known + # exactly — so the two must agree in that limit, at any perturbation order. + # + # The limit is numerically hostile: shrinking H is exactly what makes the + # importance weights degenerate, so the approach stalls at a floor set by + # particle noise rather than continuing to zero. The test therefore checks + # the *direction* — a moderate H is far from the inversion value, a small + # one is close — instead of pinning a single tolerance. + nV = length(get_variables(RBC_pf)) + Z0 = zeros(nV, nV) + algo = :pruned_second_order + inv_ll = get_loglikelihood(RBC_pf, data, p; filter = :inversion, algorithm = algo) + @test isfinite(inv_ll) + + pf(h) = Statistics.mean(get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, + algorithm = algo, initial_covariance = Z0, measurement_error = h, + n_particles = 25_000, particle_rng = Random.Xoshiro(900 + s)) for s in 1:2) + + far = pf(1e-4) # too much measurement error: a different problem + close = pf(1e-5) # small enough to approach the zero-measurement-error limit + @test isfinite(far) && isfinite(close) + @test abs(close - inv_ll) < 12 + @test abs(close - inv_ll) < abs(far - inv_ll) + end + @testset "Filter selection and automatic measurement error" begin # `:particle` is an alias for the bootstrap filter: same RNG ⇒ same value @test get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, diff --git a/test/test_particle_filter_sw07.jl b/test/test_particle_filter_sw07.jl index 5a2f5340f..ed4010dd8 100644 --- a/test/test_particle_filter_sw07.jl +++ b/test/test_particle_filter_sw07.jl @@ -194,3 +194,60 @@ end @test all(isfinite, pf_BB) @test abs(kal_shift - Statistics.mean(pf_BB)) < 8 end + +# ----------------------------------------------------------------------------- +# Higher-order equivalence for the particle filters. +# +# Smets-Wouters (2007) in its log-linearised form is *linear*, so every +# perturbation order yields the same solution — asserted below rather than +# assumed. That makes it a rare thing: a model complex enough to be a real test +# (40 variables, 7 shocks, 7 observables, 184 periods) on which the exact +# likelihood is known, yet which exercises the higher-order particle machinery — +# the pruned and non-pruned second-order transitions, their augmented Kronecker +# scratch, and the pruned `Vector{Vector}` particle layout. (Third order is +# covered on a small linear model in `test_particle_filter.jl` — same idea, but +# seconds rather than minutes per evaluation on 40 variables.) +# +# Any deviation from the Kalman value beyond Monte-Carlo error is therefore a +# bug in the higher-order transition code, not a property of the model. +# ----------------------------------------------------------------------------- +@testset "SW07 linear: particle filter at higher order matches Kalman" begin + dat, header = readdlm(joinpath(@__DIR__, "data", "usmodel.csv"), ',', header = true) + dat = Float64.(dat) + csv_names = vec(Symbol.(strip.(header))) + data = KeyedArray(dat', Variable = csv_names, Time = axes(dat, 1)) + data = data([:dy, :dc, :dinve, :labobs, :pinfobs, :dw, :robs], 47:230) + observables = [:dy, :dc, :dinve, :labobs, :pinfobs, :dwobs, :robs] + data = rekey(data, :Variable => observables) + + include("../models/Smets_Wouters_2007_linear.jl") + SS(Smets_Wouters_2007_linear, parameters = [:crhoms => 0.01, :crhopinf => 0.01, :crhow => 0.01, :cmap => 0.01, :cmaw => 0.01]) + m = Smets_Wouters_2007_linear + p = m.parameter_values + + # Premise: the model is linear. The inversion filter is deterministic, so if + # the higher-order solution terms vanish it must return the identical value + # at every order. + inv_lls = [get_loglikelihood(m, data, p; filter = :inversion, algorithm = a) + for a in (:first_order, :pruned_second_order, :second_order, + :pruned_third_order, :third_order)] + @test all(isapprox.(inv_lls, inv_lls[1], rtol = 1e-10)) + + me = 2.0 .* [Statistics.std(collect(data(o))) for o in observables] + kal = get_loglikelihood(m, data, p; filter = :kalman, presample_periods = 4, + initial_covariance = :theoretical, measurement_error = me .^ 2) + + # Second order only: third order on a 40-variable model costs minutes per + # evaluation, which is not worth it here. The third-order transitions are + # covered against the same exact reference on a small linear model in + # `test_particle_filter.jl`, where the whole sweep runs in seconds. + for (algo, N, tol) in ((:pruned_second_order, 20_000, 15.0), + (:second_order, 20_000, 15.0)) + lls = [get_loglikelihood(m, data, p; filter = :bootstrap_particle, algorithm = algo, + presample_periods = 4, initial_covariance = :theoretical, + measurement_error = me .^ 2, n_particles = N, + particle_rng = Random.Xoshiro(700 + s)) for s in 1:2] + @test all(isfinite, lls) + @test abs(kal - Statistics.mean(lls)) < tol + end +end From 20ba81d8b947ccc7a4d4160cf9490b40feb2410c Mon Sep 17 00:00:00 2001 From: Thore Kockerols Date: Wed, 29 Jul 2026 12:55:49 +0000 Subject: [PATCH 23/24] add nsss function to sw07 --- test/models/SW07_nonlinear.jl | 142 ++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/test/models/SW07_nonlinear.jl b/test/models/SW07_nonlinear.jl index 10f21d8a0..4a7efb378 100644 --- a/test/models/SW07_nonlinear.jl +++ b/test/models/SW07_nonlinear.jl @@ -207,3 +207,145 @@ end end + +""" + SW07_nonlinear_steady_state!(out, parameters) + +Non-allocating custom non-stochastic steady state solver for the `SW07_nonlinear` model. + +The analytical steady state is derived from the original Dynare `SWnonlinear_steadystate.m` +implementation. Unlike the reference implementation, all quantities are computed directly in +level space (ratios and products) rather than in logs, so no `log` is taken of intermediate +expressions. This avoids the implicit positivity domain constraints that `log` imposes on +intermediate terms (e.g. the consumption-to-output ratio `1 - inve/y - gy`), which is useful +when the solver or automatic differentiation evaluates the function at parameter values that +would otherwise produce a `DomainError`. + +The function fills `out` in place with the steady state values in the order expected by +`get_NSSS_and_parameters`: the variables in `sort(union(var, exo_past, exo_future))` followed +by the calibrated parameters (`mcflex`, `pinfss`). + +`parameters` is the vector of parameter values in declaration order (as returned by +`get_parameters(SW07_nonlinear)`). + +# Example +```julia +get_steady_state(SW07_nonlinear, steady_state_function = SW07_nonlinear_steady_state!) +``` +""" +function SW07_nonlinear_steady_state!(out::AbstractVector, parameters::AbstractVector) + ctou, cg, clandaw, curvw, crhoa, crhob, crhog, crhoqs, crhoms, crhopinf, + crhow, cmap, cmaw, csadjcost, csigma, chabb, cprobw, csigl, cindw, cindp, + czcap, cfc, crpi, crr, cry, crdy, constepinf, constebeta, ctrend, cgy, + calfa, curvp, cprobp = parameters + + cgamma = 1 + ctrend / 100 + cbeta = 1 / (1 + constebeta / 100) + clandap = cfc + + rk = cbeta^(-1) * cgamma^csigma - (1 - ctou) + mc = 1 / clandap + w = (mc * calfa^calfa * (1 - calfa)^(1 - calfa) * rk^(-calfa))^(1 / (1 - calfa)) + + inve_kp = 1 - (1 - ctou) / cgamma # inve / kp + inve_k = cgamma * inve_kp # inve / k + k_lab = calfa / (1 - calfa) * w / rk # k / lab + k_y = k_lab^(1 - calfa) * cfc # k / y + gy = cg + c_y = 1 - inve_k * k_y - gy # c / y + + lab = (w * k_y / (clandaw * k_lab * c_y * (1 - chabb / cgamma)))^(1 / (1 + csigl)) + k = k_lab * lab + y = k / k_y + c = c_y * y + inve = inve_k * k + kp = cgamma * k + r = (1 + constepinf / 100) / (cbeta * cgamma^(-csigma)) + pinf = 1 + constepinf / 100 + xi = (c * (1 - chabb / cgamma))^(-csigma) * exp((csigma - 1) / (1 + csigl) * lab^(1 + csigl)) + + disc = cbeta * cgamma^(1 - csigma) + denw = 1 - disc * cprobw + denp = 1 - disc * cprobp + gamw1 = lab * w^(clandaw / (clandaw - 1) - curvw) / denw + gamw2 = gamw1 * (1 - chabb / cgamma) * c * lab^csigl + gamw3 = lab / denw + gam1 = y / denp + gam2 = gam1 * mc + gam3 = gam1 + + wnew = w + pk = 1.0 + afuncD = rk + + out[1] = 1.0 # Pratio + out[2] = 0.0 # Sfunc + out[3] = 0.0 # SfuncD + out[4] = 0.0 # SfuncDflex + out[5] = 0.0 # Sfuncflex + out[6] = 1.0 # a + out[7] = 0.0 # afunc + out[8] = afuncD # afuncD + out[9] = afuncD # afuncDflex + out[10] = 0.0 # afuncflex + out[11] = 1.0 # b + out[12] = c # c + out[13] = c # cflex + out[14] = ctrend # dc + out[15] = ctrend # dinve + out[16] = 1.0 # dp + out[17] = wnew # dw + out[18] = ctrend # dwobs + out[19] = ctrend # dy + out[20] = 0.0 # epinfma + out[21] = 0.0 # ewma + out[22] = gam1 # gam1 + out[23] = gam2 # gam2 + out[24] = gam3 # gam3 + out[25] = gamw1 # gamw1 + out[26] = gamw2 # gamw2 + out[27] = gamw3 # gamw3 + out[28] = gy # gy + out[29] = inve # inve + out[30] = inve # inveflex + out[31] = k # k + out[32] = k # kflex + out[33] = kp # kp + out[34] = kp # kpflex + out[35] = lab # lab + out[36] = lab # labflex + out[37] = 0.0 # labobs + out[38] = mc # mc + out[39] = 1.0 # ms + out[40] = 1.0 # pdot + out[41] = 1.0 # pdotl + out[42] = pinf # pinf + out[43] = constepinf # pinfobs + out[44] = pk # pk + out[45] = pk # pkflex + out[46] = 1.0 # qs + out[47] = 1.0 # qsaux + out[48] = r # r + out[49] = rk # rk + out[50] = rk # rkflex + out[51] = 100 * (r - 1) # robs + out[52] = r / pinf # rrflex + out[53] = 1.0 # spinf + out[54] = 1.0 # sw + out[55] = w # w + out[56] = 1.0 # wdot + out[57] = 1.0 # wdotl + out[58] = w # wflex + out[59] = wnew # wnew + out[60] = xi # xi + out[61] = xi # xiflex + out[62] = y # y + out[63] = y # yflex + out[64] = 0.0 # ygap + out[65] = 1.0 # zcap + out[66] = 1.0 # zcapflex + out[67] = mc # mcflex (calibrated parameter) + out[68] = 1 + constepinf / 100 # pinfss (calibrated parameter) + + return nothing +end From 8ef66184c145be89e80adc207d1837fd32e52727 Mon Sep 17 00:00:00 2001 From: Thore Kockerols Date: Sun, 2 Aug 2026 18:33:52 +0200 Subject: [PATCH 24/24] Keep higher-order state updates compressed (#312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Keep higher-order state updates compressed * Cache higher-order power contractions * Fix compressed higher-order CI regressions * Cache invariant compressed selector maps * Fix compressed higher-order review paths * Correct review verification count * Fix ForwardDiff higher-order gradient paths * Rewrite the particle filters and add a guided (conditionally optimal) variant The estimates path was ~500x more allocating than the likelihood path and no particle filter gave shock estimates that survived a change of RNG seed on a Smets-Wouters-sized problem. Both are addressed here. Performance. The filters are rebuilt around a batched particle cloud: nVars x N matrices, one per pruned state component, so a period costs a handful of BLAS gemm calls instead of N gemv calls, and the estimates path shares that machinery instead of duplicating it in a closure that dispatched dynamically per particle. The block loop is split across Julia's threads. On SW07 at pruned second order get_estimated_shocks went from 12.06 s / 4900 MiB / 319M allocations to 0.35 s / 19 MiB / 36k for the bootstrap filter, and 92.8 s to 8.4 s for the tempered one. src/filter/particle.jl is shorter than before. Correctness. Batched compressed-Kronecker kernels match the reference vector kernels; the batched transition matches the model's own state_update at all five perturbation orders; on a linear model every order and every variant reproduces the Kalman likelihood, ordered by efficiency. Missing data, smoothing and both shock decompositions verified. Tempered filter. The Metropolis mutation is now preconditioned by the stage's own Gaussian covariance and its scale adapts towards 25% acceptance; a fixed isotropic step cannot suit a target that contracts as phi rises and is anisotropic across shocks. Its defaults move to tempering_target_ratio 1.5 and 4 mutation steps, which measurably improve both the estimates and the likelihood per unit of compute (see the comments in default_options.jl for the numbers). New filter :guided_particle. With about as many shocks as observables and a small measurement error, the observation nearly determines the shock given the ancestor, and that conditional is available in closed form. Drawing from it makes the importance weight identically constant when the transition is linear in the shock. The proposal's centre is refined by Gauss-Newton steps against the true residual, and the filter then anneals from the proposal to the exact conditional along q^(1-beta) pi^beta, which costs 1.2 stages per period on average and only spends more where the proposal is poor. On the euro-area data it needs eight to twelve times less compute than the tempered filter for equal accuracy or better, on both the estimates and the likelihood, at first and pruned second order. :particle now aliases :guided_particle rather than :bootstrap_particle, and tempering_mh_steps defaults to 2 for the guided filter and 4 for the others via a filter selector. This changes results for existing callers passing :particle. Also: the filters now warn instead of silently returning noise when the cloud has degenerated, and both AGENT_PROGRESS.md and tasks/todo.md record the measurements behind each choice, including the approaches that were tried and rejected (full adaptation, proposal over-dispersion, deferred within-period resampling). Verified with test/test_particle_filter.jl (142/142; its plotting testset needs a libgobject this container lacks) and a focused check covering all four variants at every perturbation order. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Fa11GzkuyTSc6xjoEP17br * delete Agent progress * Fix higher-order inversion-filter pullbacks against finite differences The compressed-Kronecker migration left several reverse-mode paths on the uncompressed convention, or reusing buffers that were still live. Weights. d/dx compressed_kron²_power(aug(x)) is 2·compressed_kron²(aug, ∂aug/∂x) and the cubic analogue carries a 3, so the 1/2 and 1/6 in front of the 𝐒₂ and 𝐒₃ terms cancel down to 1 and 1/2 in the pullback. The non-pruned stochastic steady-state pullbacks kept the forward weights, which is the main regression here: the SSS correction is itself second-order small, so this read as 0.15 % on ∂state but cancelled up to ~390 % in the likelihood gradient, and it also reached get_steady_state's parameter Jacobian. The same off-by-a-factor appears in the cubic third-order warmup term, in accumulate_cubic_kron_jacobian_pullback! (whose helper is already the exact VJP), and in the second-order warmup, where a pair VJP was double-counting the 1/2. Aliasing. compressed_kron{²,³}_power_vjp! overwrite their output while the identity variants accumulate; three call sites passed a shared accumulator and so dropped the terms added before them. Separately, the dense pruned third-order path used the ∂kronstate¹⁻_vol cotangent as forward scratch, and the third-order with-missing backward loop read a workspace buffer still holding the last forward step's value. KKT blocks. The dense pruned second- and third-order shock-solver cotangents were assembled by uncompressed ℒ.kron!, which crashed on any model with nExo ≠ 2. Rewritten in the compressed pair/triple bases. Forward side. third_order_warmup_observation_and_jacobian still built its shock-state mixed terms uncompressed, so it disagreed with its own pullback; compressed_pair_hessian! sets both triangles, hence the factor 2 the shock solver's Newton step was missing; the ForwardDiff Dual overloads needed the same compressed treatment. FS2000 parameter gradients, Mooncake against central_fdm(5,1), worst relative error over T = 1/5/40 and all five algorithms: 8.5e-08, against 3.9 before. test_inversion_filter_gradients 264/264, test_higher_order_1 7869/7869, test_missing_data 128/128, test_rrule_robustness 57/57. Co-Authored-By: Claude Opus 5 * Address particle-filter review comments Naming. The bridging controls were named after tempering but are shared with the guided filter, so tempering_* becomes particle_* and the two per-variant values keep their variant's name (DEFAULT_GUIDED_MH_STEPS, DEFAULT_TEMPERED_MH_STEPS). The convention is now stated at the top of the defaults block. Magic numbers that were inline in particle.jl — scratch budget, block sizes, Metropolis adaptation targets and bounds, the low-ESS warning threshold — move to named defaults. propagate_block! loses its algorithm if/elseif chain in favour of dispatch on the perturbation order. build_guided_proposal reuses its factorisation across the Gauss-Newton refinement instead of re-solving. Comments that leaned on jargon are rewritten to say what the code does, and the defaults' commentary is cut back to the conclusions with the supporting tables dropped. Statistics was declared as a test dependency without a compat entry, which Aqua flags. JET could not correlate get_loglikelihood's measurement-error guard with the separate `if` that uses it, so the narrowing moves to the use site. Deletes the tasks/ scratch files, which were working notes rather than sources. test_particle_filter, Aqua and the JET hot-path suite all pass. Co-Authored-By: Claude Opus 5 * Correct the particle-filter guidance in the filters guide The page told readers to fall back to :tempered_particle "when the model is strongly nonlinear", which its own pruned-second-order benchmark contradicts, and to prefer it for estimates. Both were right when written, before the guided filter gained its own Metropolis sweeps; neither is now. Replaces the vague trigger with the assumption that actually binds — the guided proposal is built from the first-order shock impact on observables, so it wants the observation roughly linear in the shock — with the diagnostic that fires when it fails (the post-bridge ESS warning) and the price of switching (~10x). The estimates recommendation flips to :guided_particle, which the page's own table has at 0.078 seed dispersion against 0.093 at an eighth of the cost. The "estimates versus likelihoods" section now attributes the fix to mutation rather than to tempering, since both filters mutate. Also repairs a broken @ref. Co-Authored-By: Claude Opus 5 * Address review comments on the compressed higher-order work Correctness questions raised in review, both answered with finite differences and now pinned by tests. The compressed-power derivative identities are d/dx compressed_kron²_power(x) = 2·compressed_kron²(x, dx) and d/dx compressed_kron³_power(x) = 3·compressed_kron³(x, x, dx), so the forward Taylor weights 1/2 and 1/6 become 1 and 1/2 in every Jacobian built on them. That is why the third-order warmup recursion divides the cubic term by 2 where the uncompressed form divided by 6, and where the factor-of-3 change in occasionally_binding_constraints comes from. Checked against central differences at 1e-10; the alternatives are off by exactly 2 and 3. The OBC Jacobian was also checked end to end against differences of its own output (7.9e-10 at second order, 3.9e-10 at third). Structure. The Aumann-Shapley historical shock decomposition belongs to neither filter that uses it, so it moves to src/filter/decomposition.jl. The particle filter's three column-wise Kronecker kernels were a second implementation of what perturbation/solution.jl already does per vector; they become compressed_kron{²,³}_power_columns!/compressed_kron²_columns! next to the kernels they delegate to. The compressed_kron test set was orphaned — no CI job ever ran it — and now runs as part of basic. Performance, each measured rather than assumed. - The cubic shock/state index sets and their row maps were rebuilt per call (a sort plus a binary search per row) though they depend only on the model's dimensions. Memoised on those dimensions. - The KKT pair-block cotangents read the target column back on the right of a `.+=`, which materialises it: 240 B per call at nExo = 2 up to 328 kB at nExo = 40, and 1.6-5.8x slower than two `ger!`s. Rewritten as preallocated rank-1 updates. End to end it is a small share (0.5 MB of 150 MB on Smets-Wouters at nExo = 7) but it grows with nExo. - `A_mat .-= 2 .* Matrix(I(n_x))` in the ForwardDiff KKT blocks materialised a dense identity for a diagonal update. Now a loop. - `@simd` on the compressed kernels was tried and is 1.6-2.6x *slower* — the body's branch is perfectly predicted and `@simd` trades it for a masked store. Recorded so nobody retries it. - Densifying 𝐒₂/𝐒₃ for the particle filters was questioned. In the compressed basis they are 28-87% dense on every model measured, and dense `gemm` beats sparse over all of that range (3.2-7.9x with one BLAS thread, up to 26x with four); the crossover is near 1% density. Measurements are in the comment. The `filtered` keyword on solve_stochastic_steady_state_newton had no caller passing `false` — both live call sites pre-slice and pass `true` — so it and its dead branch are gone from all four methods. Documentation and naming. The filters guide now maps every knob to the algorithm step it controls, for the tempered and the guided filter alike. The bridging options are shared by both filters, not tempered-only as their docstrings implied, and now say so; `filter`'s docstring notes that the inversion filter is the fastest nonlinear option, that it can fail outright rather than degrade, and that its smoothed and filtered estimates coincide. The guided filter's internal `particle_mh_steps` fallback said 4 where the public selector gives it 2. Semicolon-joined statements are split one per line, cryptic locals in the particle filters are named for what they hold, and the de-indented lines the compressed migration left in inversion.jl are re-indented. Comments answering the rest: how the Laplace approximation feeds the guided proposal and why the reported likelihood does not depend on it, how the guided bridge differs from the tempered one, why the mutating filters keep last period's cloud when the others do not, and why the Gauss-Newton step is fine with fewer observables than shocks. test_compressed_kron 239/239 (including the new argument-order, derivative-weight and column-wise sets), test_inversion_filter_gradients 264/264, test_jet_hot_paths 302/302, test_filter_free_gradients 247/247, test_initial_state 187/187, test_particle_filter 146/146, test_missing_data 128/128, test_rrule_robustness 57/57, test_inversion_filter_likelihood 7/7. test_higher_order_1 was not run to completion locally; CI covers it. Co-Authored-By: Claude Opus 5 * Take the cubic index sets from the model's constants The model already cached these. `third_order_indices` carries shock_state_state_idxs/_rows and shock_shock_state_idxs/_rows, filled once by ensure_conditional_forecast_constants!, and the inversion filter loops already read them; the memoised compressed_cubic_shock_maps added in the previous commit was a second, global, dict-and-lock cache of the same four vectors built a different way. It is gone. What is left is one construction — compressed_shock_state_state_index_map and its shock-shock sibling — used both to fill the model's constants and by the few pullback entry points that can be called without the model in scope, so the two cannot drift. The joint-warmup solver and its observation/Jacobian routine now take the sets as keyword arguments, mirroring what the rrules already did, and all ten call sites in the filters and their pullbacks pass the model's cached vectors down. The remaining fallback is lazy, where the old code hit the dict even when the caller had supplied everything. Dead code the sweep turned up, all of it left behind by the compressed migration: - second_order_warmup_observation_and_jacobian built the *cubic* index sets and never read them. That predates the memoisation, which only made the dead work cheap. - third_order_indices.I_exo2, a sparse I(nExo^2) built for every third-order model and read by nothing. The compressed shock pair is nExo(nExo+1)/2. - `II = I(n_exo_pair)` in all six inversion filter functions, assigned and never used, two of them allocating a sparse identity. The compressed path replaced kron(II, state_vol) with compressed_triple_state_to_pair!. `II` is still live in rrules.jl, where the uses are real. - compressed_shock_shock_state_indices, which had no callers. Also answered in a comment above DEFAULT_GUIDED_NEWTON_STEPS: how the guided proposal's Gauss-Newton step relates to the inversion filter. It is the same problem with the shock prior left in — the inversion filter solves r(eps) = 0, this maximises -0.5||eps||^2 - 0.5 r(eps)' H^-1 r(eps) — and the two coincide as the measurement error vanishes, since M = I + Bo'H^-1 Bo -> Bo'H^-1 Bo and K -> pinv(Bo). Checked: ||K r - pinv(Bo) r|| falls as O(sigma^2), 2e-1 / 2e-3 / 2e-7 at sigma^2 = 1e-2 / 1e-4 / 1e-8, for square Bo and both rectangular shapes. The comment also records why it does not inherit the inversion filter's hard failure (M is positive definite by construction, so the solve succeeds at any shape; the filter's own bail-out is cloud degeneracy, a different condition) and why running the inversion filter per particle instead would give up the shared factorisation that makes the proposal cheap. New testset pins the two builders against the sorted set plus the binary-search row map at four shapes. test_inversion_filter_gradients 264/264 (this is the suite that drives the third-order joint-warmup pullbacks), test_inversion_filter_likelihood 7/7, test_compressed_kron clean including the new set. Function-name diff on inversion.jl against the previous commit: nothing missing, nothing added. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Thore Kockerols Co-authored-by: Claude Opus 5 --- Project.toml | 4 +- docs/src/filters.md | 235 +- ext/ForwardDiffExt.jl | 151 +- ext/StatsPlotsExt.jl | 24 +- src/MacroModelling.jl | 49 +- src/common_docstrings.jl | 20 +- src/default_options.jl | 91 +- src/filter/decomposition.jl | 653 ++++ src/filter/find_shocks.jl | 154 +- src/filter/inversion.jl | 1245 ++----- src/filter/particle.jl | 3638 ++++++++++++------- src/get_functions.jl | 114 +- src/occasionally_binding_constraints.jl | 67 +- src/options_and_caches.jl | 211 +- src/perturbation/solution.jl | 1078 ++++++ src/rrules.jl | 2151 ++++++----- src/steady_state/stochastic_steady_state.jl | 177 +- src/structures.jl | 60 +- test/runtests.jl | 1 + test/test_compressed_kron.jl | 369 ++ test/test_particle_filter.jl | 8 +- 21 files changed, 6833 insertions(+), 3667 deletions(-) create mode 100644 src/filter/decomposition.jl create mode 100644 test/test_compressed_kron.jl diff --git a/Project.toml b/Project.toml index 52583e4ac..54104f8fe 100644 --- a/Project.toml +++ b/Project.toml @@ -103,6 +103,7 @@ RuntimeGeneratedFunctions = "0.5" Showoff = "1" SparseArrays = "1" SpecialFunctions = "2" +Statistics = "1" StatsPlots = "0.15" Subscripts = "0.1.3" Suppressor = "0.2" @@ -135,10 +136,11 @@ Optim = "429524aa-4258-5aef-a3af-852621145aeb" Pigeons = "0eb8d820-af6a-4919-95ae-11206f830c31" Preferences = "21216c6a-2e73-6563-6e65-726566657250" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Turing = "fce5fe82-541a-59a6-adf8-730c64b5f9a0" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" [targets] -test = ["ADTypes", "Aqua", "BenchmarkTools", "CondaPkg", "PythonCall", "JET", "Dates", "DelimitedFiles", "DifferentiationInterface", "DynamicPPL", "ForwardDiff", "Mooncake", "FlexiChains", "MCMCChains", "LineSearches", "Optim", "Test", "Turing", "Pigeons", "FiniteDifferences", "StatsPlots", "Preferences", "Zygote"] +test = ["ADTypes", "Aqua", "BenchmarkTools", "CondaPkg", "PythonCall", "JET", "Dates", "DelimitedFiles", "Statistics", "DifferentiationInterface", "DynamicPPL", "ForwardDiff", "Mooncake", "FlexiChains", "MCMCChains", "LineSearches", "Optim", "Test", "Turing", "Pigeons", "FiniteDifferences", "StatsPlots", "Preferences", "Zygote"] diff --git a/docs/src/filters.md b/docs/src/filters.md index 2d4963a09..68028c866 100644 --- a/docs/src/filters.md +++ b/docs/src/filters.md @@ -30,6 +30,7 @@ Two inputs cut across all of them and are covered separately below: `measurement |---|---|---|---|---|---|---| | `:kalman` | linear (`:first_order`) | exact | yes | optional (incl. correlated) | yes (Durbin–Koopman) | 1× | | `:inversion` | linear and nonlinear | exact given the shocks | yes | not available | n/a (filtered = smoothed) | ~1–10× | +| `:guided_particle` (`:particle`) | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~2× bootstrap | | `:bootstrap_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~10³× | | `:auxiliary_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~2× bootstrap | | `:tempered_particle` | linear and nonlinear | stochastic, unbiased | no | required (incl. correlated) | yes (genealogy) | ~5–10× bootstrap | @@ -38,8 +39,9 @@ A short decision rule: - **Linear model?** Use `:kalman`. It is exact, fast and differentiable, so gradient-based samplers (NUTS/HMC) work. There is no reason to use anything else. - **Nonlinear model, at least as many shocks as observables, no measurement error?** Use `:inversion` (the default at higher order). It is exact and differentiable. -- **Nonlinear model with measurement error, or fewer shocks than observables?** Use a particle filter. Start with `:tempered_particle` if the observation is informative (small measurement error, many observables), otherwise `:bootstrap_particle`. +- **Nonlinear model with measurement error, or fewer shocks than observables?** Use a particle filter. `filter = :particle` gives you `:guided_particle`, which is the right default: it draws the shock from its own conditional rather than blindly, and measures both cheaper and more accurate than `:tempered_particle` on every comparison in [Guided (`:guided_particle`, which `:particle` selects)](@ref). It buys that with one assumption — that the observation is close to linear in the shock, which is what the proposal is built from. Fall back to `:tempered_particle`, which anneals from the prior and assumes nothing, when that assumption fails badly enough that the guided filter's own bridge cannot repair it. You do not have to guess when: the filter warns if the post-bridge effective sample size averages under 5 % of `n_particles`. Expect to pay roughly ten times the compute. `:bootstrap_particle` and `:auxiliary_particle` are baselines, not recommendations. - Particle-filter likelihoods are noisy and non-differentiable: pair them with gradient-free samplers such as slice sampling (Pigeons.jl) or nested sampling. +- **Want *estimates* (`get_estimated_shocks`, `get_estimated_variables`, the estimate plots) rather than a likelihood?** The choice of filter matters more here than it does for the likelihood — see [Estimates versus likelihoods](@ref). Stay with `:guided_particle`: what estimates need is a filter that *moves* particles onto the observation, and of the two that do, it is the more accurate and the cheaper. Use `:bootstrap_particle` and `:auxiliary_particle` as diagnostics only. By default the package picks `:kalman` for `:first_order` and `:inversion` for the nonlinear algorithms. @@ -59,7 +61,8 @@ Every knob discussed on this page has a default, and the defaults are not neutra | `n_particles` | `10_000` | | | `particle_resampling` | `:systematic`, threshold `0.5` | resample only when the effective sample size halves | | `particle_initial_state_scaling` | `1.0` | the initial cloud has exactly the ergodic spread | -| tempering | ratio `2.0`, 1 MH step, ≤100 stages, scale `0.3` | only used by `:tempered_particle` | +| `particle_mh_steps` | `2` for `:guided_particle`, `4` otherwise | the guided filter bridges from a proposal already close to the target and needs far less mutation; the tempered filter bridges from the prior and needs it badly (0.221 against 0.106 at one step versus four) | +| tempering, other | ratio `1.5`, ≤100 stages, starting scale `1.0` | the ratio is set more aggressively than Herbst & Schorfheide's own value because for the tempered filter the mutation, not the particle count, limits accuracy; the scale adapts during the run | Three consequences are worth internalising, because they surprise people: @@ -159,6 +162,10 @@ The crucial property is that ``\widehat{p}(y_{1:T})`` is an **unbiased** estimat Because the estimate is random, a repeated evaluation at the same parameters gives a different number unless you fix the stream: pass a seeded generator via `particle_rng` (e.g. `particle_rng = Random.Xoshiro(1)`). Inside a sampler, reuse the same seed across parameter draws only if you deliberately want common random numbers; otherwise let the sampler see fresh noise, which is what pseudo-marginal correctness assumes. Cost scales roughly linearly in `n_particles` and in the sample length, and the number of particles needed grows quickly with the number of observables. +The whole swarm is propagated at once — the perturbation transition becomes a handful of `gemm` calls per period rather than one matrix-vector product per particle — and those calls are split across Julia's threads, so starting Julia with `-t auto` is worth roughly a factor of seven on a many-core machine. The split is by fixed column blocks and every random draw is made outside it, so the parallelism introduces no *statistical* dependence on the thread count. + +It does not deliver bitwise determinism across machines, though, and the reason is worth knowing if you are comparing runs. Within one process a seed reproduces exactly. Across processes or thread counts the block partitioning and the vectorised reductions reassociate the same sums differently, which perturbs results at the 1e-15 level — and a particle filter can amplify that, because one flipped accept/reject or resampling boundary sends the cloud down a different (equally valid) path. **Compare seeds within a single session**, and treat a number computed on a different machine as a different draw rather than a bug. + ### Why measurement error is required Without measurement error the observation equation is a deterministic function of the state. A particle would have to reproduce ``y_t`` *exactly* to get non-zero weight, which happens with probability zero — every weight collapses to zero and the filter dies. Measurement error smears the observation density and gives particles something to score against. @@ -187,12 +194,234 @@ Helps most when the signal is informative *and* the one-step-ahead state is well Instead of confronting the particles with the full observation in one step, the tempered filter introduces the information gradually. Within each period it walks a bridging sequence ``0 = \phi_0 < \phi_1 < \dots < \phi_N = 1``, at each stage using an inflated measurement covariance ``H/\phi``: early stages are nearly uninformative and easy to match, later stages sharpen towards the true density. At every stage the particles are reweighted by the incremental density, resampled, and then **mutated** by a few random-walk Metropolis steps on their shocks that target the stage's tempered posterior. The stage contributions telescope back to the period's likelihood. -The mutation is what makes this powerful: it *moves* particles towards the data rather than merely reweighting the ones that happen to be well placed, so the cloud does not degenerate even when the observation is sharp. The bridging schedule is chosen adaptively to hit a target inefficiency ratio (`tempering_target_ratio`), so hard periods automatically get more stages than easy ones. +The mutation is what makes this powerful: it *moves* particles towards the data rather than merely reweighting the ones that happen to be well placed, so the cloud does not degenerate even when the observation is sharp. The bridging schedule is chosen adaptively to hit a target inefficiency ratio (`particle_target_ratio`), so hard periods automatically get more stages than easy ones. + +The mutation only earns that if its steps are the right size, and the right size is neither known in advance nor constant. The stage-``\phi`` target on the shocks is + +```math +\pi_\phi(\varepsilon) \;\propto\; N(\varepsilon; 0, I)\,\exp\!\left(-\tfrac{\phi}{2}\, e(\varepsilon)' H^{-1} e(\varepsilon)\right), +``` + +which contracts as ``\phi`` rises and is strongly anisotropic — the observables pin some shocks far more tightly than others. Two things keep the sweep well scaled. First, the proposal is **preconditioned**: linearising ``e(\varepsilon) \approx e(0) - B_o \varepsilon`` with ``B_o`` the first-order impact of the shocks on the observables makes ``\pi_\phi`` Gaussian with covariance ``(I + \phi\, B_o' H^{-1} B_o)^{-1}``, and the step is drawn from that shape (an ``n_\varepsilon \times n_\varepsilon`` Cholesky per stage — negligible next to a transition evaluation). Second, the overall step **scale adapts** during the run towards a 25 % acceptance rate. `particle_mh_scale` is therefore only the starting point, expressed in units of the stage's own posterior scale, so a value near one is right whatever the model — on Smets–Wouters it settles at ``\approx 0.92``, essentially the textbook ``2.38/\sqrt{n_\varepsilon}``. In practice this buys a large variance reduction per particle — several times lower standard deviation than the bootstrap filter at the same ``N`` — at several times the cost per particle. It is the right default when the bootstrap filter degenerates. +**The knobs, and which step each one controls.** One period is: *choose ``\phi_{k+1}`` → reweight → resample → mutate*, repeated until ``\phi = 1``. + +| option | step it acts on | what it does | +|---|---|---| +| `particle_target_ratio` | choosing ``\phi_{k+1}`` | The schedule is solved for, not fixed: ``\phi_{k+1}`` is the largest step whose incremental weights stay within this inefficiency target. Lower ⇒ smaller steps ⇒ more of them, each discarding fewer particles at its resampling. | +| `particle_max_stages` | the loop itself | Hard cap on stages per period, so a pathological observation cannot hang the run. Reaching it is a symptom, not a setting to raise. | +| `particle_resampling`, `particle_resampling_threshold` | resample | Which scheme, and how degenerate the weights must get first. The tempered filter resamples at *every* stage regardless (see [What actually limits the precision](@ref)), so the threshold only governs the once-per-period resampling that follows. | +| `particle_mh_steps` | mutate | How many random-walk Metropolis sweeps per stage. Each sweep is one batched transition evaluation, so this is where the compute goes. | +| `particle_mh_scale` | mutate | The *starting* step size, in units of the stage's own posterior scale. The filter adapts it towards the target acceptance rate during the run, so this only sets where the adaptation begins. | +| `n_particles`, `measurement_error`, `particle_rng` | all of them | Cloud size, the ``H`` that defines ``\pi_\phi``, and the random stream. | + +`particle_target_ratio`, `particle_max_stages`, `particle_mh_steps` and `particle_mh_scale` are shared with `:guided_particle`, which runs the same four steps against a different bridge — see below. They have no effect on `:bootstrap_particle` or `:auxiliary_particle`, which do not bridge or mutate at all. + **Reference:** Herbst & Schorfheide (2019), *Tempered Particle Filtering*; see also Herbst & Schorfheide (2015), *Bayesian Estimation of DSGE Models*. +### Estimates versus likelihoods + +The unbiasedness result above is about the *likelihood*. It says nothing about the quality of the **estimates** — ``E[x_t \mid y_{1:t}]`` and ``E[\varepsilon_t \mid y_{1:t}]``, which is what `get_estimated_variables`, `get_estimated_shocks`, `get_model_estimates` and the estimate plots report. Those are weighted averages over the cloud, and a weighted average is only as good as the number of particles actually carrying weight. + +That is where the bootstrap filter's blind proposal bites hardest. With as many observables as shocks and a small ``H`` — the standard DSGE setup — the weights concentrate on a handful of particles, so the effective sample size collapses. The likelihood estimate survives this (it is still unbiased, just noisy, and the noise averages out over a sampler's iterations); a single reported shock path does not. Re-run it with a different `particle_rng` and the numbers move, sometimes enough to flip the sign of the shock the period is being attributed to. + +Mutation fixes this at the source. Both `:guided_particle` and `:tempered_particle` run within-period Metropolis sweeps that *move* particles onto the observation instead of discarding the ones that missed, so the cloud the moments are taken over has many more distinct support points. On the nonlinear Smets–Wouters model at pruned second order (seven observables, seven shocks, the default ``(0.1 s_i)^2`` measurement error), that is the difference between shock estimates that agree across seeds and shock estimates that are essentially noise. + +Between the two, the guided filter is the better default for estimates just as it is for likelihoods. It bridges from the conditional rather than from the prior, so it needs far fewer stages to reach the same place: on that same problem its across-seed spread of the shock estimates is 0.078 against the tempered filter's 0.093, in an eighth of the time (the table under [Guided (`:guided_particle`, which `:particle` selects)](@ref)). Reach for `:tempered_particle` when the guided proposal itself is the problem, which it tells you about. + +If you do use one of the non-mutating variants for estimates, the filter tells you when the cloud has degenerated: it warns when the average effective sample size falls below 5 % of `n_particles`. Take that warning literally — raising `n_particles` shifts the threshold but not the underlying problem, which is the proposal. + +#### What actually limits the precision + +Even with the tempered filter, a period's estimate is not governed by `n_particles` directly, but by how many **distinct ancestors** survive that period. The measurement equation is informative about ``x_{t-1}`` as well as about ``\varepsilon_t``, so the tempering has to concentrate the ancestor cloud, and each of its stages resamples. On the Smets–Wouters problem above that leaves roughly 2 % of the swarm as distinct ancestors — a few hundred out of ten thousand — and the reported moments are averages over those. (Deferring the within-period resampling until the weights degenerate, the usual adaptive-SMC remedy, was tried and measured there: the surviving ancestors rose only from 2.2 % to 2.6 % while the stage count rose from 9.0 to 11.5, so per unit of work it was slightly *worse*. Resampling every stage, as Herbst & Schorfheide do, is kept.) + +More particles push that back, but on the Smets–Wouters problem they stop helping surprisingly early: the across-seed spread of the last periods' shock estimates falls from 0.23 (in units of one shock standard deviation) at ``N = 2\,500`` to 0.16 at ``N = 10\,000``, and then **stops** — ``N = 40\,000`` gives 0.16 as well. + +That plateau is *not* a statement about what the data can identify. It is the mutation running out of mixing: once the cloud is only being nudged rather than genuinely rejuvenated, adding particles adds copies of the same few trajectories. The two controls that fix it are the ones that govern rejuvenation, and both keep paying long after `n_particles` has stopped: + +| `target_ratio` | `mh_steps` | seed sd of the shock estimates | sd of the log likelihood | cost | +|---|---|---|---|---| +| 2.0 | 2 | 0.199 | 147.8 | 1.0× | +| 2.0 | 4 | 0.144 | 85.9 | 1.7× | +| 1.5 | 2 | 0.161 | 67.6 | 1.3× | +| **1.5** | **4** | **0.106** | **39.1** | 2.3× | +| 2.0 | 8 | 0.088 | — | 3.1× | + +(Measured at ``N = 4\,000`` over ten seeds. A lower ratio makes each bridging step gentler, so fewer ancestors are lost at its resampling; more MH steps rejuvenate harder within each step. The two compound, and both improve the likelihood as well as the estimates — which is why both defaults are set above Herbst & Schorfheide's values of 2.0 and 1. Per unit of compute both beat raising `n_particles`, and for the estimates `n_particles` stops helping altogether past a few thousand.) + +The investment-specific shock `eqs` makes the point sharply. At the defaults its seed spread is 0.26 against an estimate of 0.36 — it looks unidentified, and quadrupling the particle count barely moves it. At `particle_mh_steps = 8` the spread falls to 0.068. Nothing about the data changed; the cloud simply started mixing. **A shock that looks unidentified under a particle filter should be retested with harder rejuvenation before that is believed.** + +The practical reading: past `n_particles` of a few thousand, spend the next unit of compute on `particle_mh_steps` (or a lower `particle_target_ratio`), not on more particles. `particle_mh_steps = 8` is worth trying whenever a shock looks unstable. + +**The direct check, and the one worth running:** call the same estimate under two or three different `particle_rng` seeds and compare. That takes seconds now and tells you exactly which of your shock estimates you can lean on: + +```julia +using Random +paths = [get_estimated_shocks(model, data, filter = :particle, + particle_rng = Xoshiro(s)) for s in 1:3] +maximum(abs, paths[1] .- paths[2]) # per-shock, per-period disagreement +``` + +### Guided (`:guided_particle`, which `:particle` selects) + +This is the filter `filter = :particle` gives you, and the one to reach for first. + +**The idea.** The other three variants all draw the shock without looking at ``y_t`` and then repair the damage — the bootstrap filter by discarding whatever missed, the auxiliary filter by preselecting ancestors, the tempered filter by running an MCMC inside every period. This one uses the observation to draw the shock in the first place. + +It can do that because of a structural feature most DSGEs share: about as many structural shocks as observables, and a measurement error that is small next to the data. Given the ancestor ``x_{t-1}``, the observation then very nearly *determines* ``\varepsilon_t``. Linearising the observed transition in the shock, ``C\,g(x_{t-1},\varepsilon) \approx m_p + B_o\varepsilon`` with ``B_o`` the first-order impact of the shocks on the observables, makes that conditional exactly Gaussian: + +```math +p(\varepsilon_t \mid x_{t-1}, y_t) = N(\mu_p, M^{-1}), +\qquad M = I + B_o' H^{-1} B_o, +\qquad \mu_p = M^{-1} B_o' H^{-1} r_p , +``` + +where ``r_p = y_t - m_p`` is the residual left by the zero-shock prediction. Note what ``M`` does *not* depend on: the particle. It is a property of the model and of ``H`` alone, so it is factorised once per missing-data pattern — once for the whole sample when the data has no holes — and each particle's conditional mean is then one small matrix product away from its own residual. + +**One period, step by step.** + +1. Push the whole cloud forward with ``\varepsilon = 0`` and read off each particle's residual ``r_p``. One batched transition. +2. Form ``\mu_p = M^{-1}B_o'H^{-1}r_p``, then refine it with two Gauss–Newton steps, ``\varepsilon \leftarrow \varepsilon + M^{-1}(B_o'H^{-1}r(\varepsilon) - \varepsilon)``, which use the *true* residual rather than the linearisation. Two more transitions. +3. Draw ``\varepsilon_j = \mu_j + U^{-1}z_j`` with ``z_j`` standard normal and ``U'U = M``, push the cloud forward again, and weight. +4. Bridge from the proposal towards the exact conditional if the weights call for it (below), resample if the effective sample size has fallen, and move on. + +**Why the weights nearly vanish.** Writing out the importance weight of step 3, + +```math +\log w_j = \log Z - \tfrac{1}{2}\left(\|\varepsilon_j\|^2 + r_j'H^{-1}r_j - \|z_j\|^2\right), +``` + +every ``\varepsilon``-dependent term cancels when the transition is *linear* in the shock. The weights are then identically constant: the filter has no conditional weight variance at all, and the only Monte-Carlo error left is the irreducible one across ancestors. That is the classical optimal-importance-function result of Doucet, Godsill & Andrieu (2000), and it is why the filter is so much stronger at first order. At higher order the cancellation is no longer exact, and what survives is precisely the curvature the linearisation misses — which the perturbation itself treats as small. + +**Why the mode is refined.** Step 2 is not decoration. ``\mu_p = M^{-1}B_o'H^{-1}r_p`` is the mode only when the observed transition is linear in the shock; at pruned second order it is not, and a mis-centred proposal in seven dimensions against a target this tight is expensive. The Gauss–Newton steps lift the effective sample size of the importance weights from 0.17 to 0.48 of the cloud. Two steps is measured to be the right number: cutting it does not even save time, because a worse centre makes the bridge below take more stages and a stage costs the same transition a Newton step does. (Solving for the shock that explains the observation and then sampling around it is the implicit particle filter of Chorin, Morzfeld & Tu, 2010, from geophysical data assimilation.) + +**What it costs and buys.** Six or seven batched transition evaluations per period, against the tempered filter's tens. Measured on Smets & Wouters (2007) with seven euro-area observables over 215 quarters, at the shipped defaults, over 16 paired seeds: + +| | | `:tempered_particle` | `:guided_particle` | +|---|---|---|---| +| first order | seed sd of the shock estimates | 0.147 (47.7 s) | **0.075** (4.1 s) | +| pruned second order | seed sd of the shock estimates | 0.093 (90.9 s) | **0.078** (11.2 s) | +| first order | sd of the log likelihood | 76.5 (15.1 s) | **69.4** (1.1 s) | +| pruned second order | sd of the log likelihood | 72.3 (43.4 s) | **55.1** (3.8 s) | + +That is between eight and twelve times less compute for the same accuracy or better, on every one of the four measurements. On a linear model, where the proposal is exact, the gap is wider still: the log-likelihood's standard deviation is a few thousandths against the tempered filter's 0.079 and the bootstrap filter's 0.185, and the mean sits on the Kalman value. + +**Reaching the truth gradually.** The proposal is only as good as its linearisation, and in a period the model can barely explain — a crisis observation ten or more measurement-error units out — it is centred somewhere the conditional's mass is not. Weighting in a single step then gives heavy-tailed weights: measured on the pruned second-order problem, the worst period's effective sample size was *two to four particles whatever `n_particles` was*, and widening the proposal did not help (the mass is not where the Gaussian is, at any width). + +So the filter does not do it in one step. It bridges + +```math +\gamma_\beta(\varepsilon) \;\propto\; q(\varepsilon)^{1-\beta}\,\tilde\pi(\varepsilon)^{\beta}, \qquad \beta: 0 \to 1, +``` + +reweighting, resampling and mutating along the way — annealed importance sampling (Neal, 2001) started from the Laplace approximation rather than from the prior. With ``L(\varepsilon) = \log\tilde\pi - \log q`` the incremental weight is ``\exp((\beta'-\beta)L)``, so the same inefficiency-targeting schedule the tempered filter uses picks the steps, and the Metropolis acceptance interpolates between targeting ``q`` exactly at ``\beta = 0`` and the tempered filter's own acceptance at ``\beta = 1``. + +The point is that the schedule is adaptive, so this is nearly free: on the euro-area problem it takes **1.2 stages per period on average** (at most 6–8, in exactly the periods that need them), and where the proposal is already good it jumps straight to ``\beta = 1`` and reduces to the one-step filter. What it buys is the failure mode: the worst period's effective sample size goes from 0.00025 of the cloud to **0.50**, and the average from 0.24 to 0.92. + +The filter still warns when the average effective sample size falls below 5 % of `n_particles`. If that fires, the model is nonlinear enough in the shock that `:tempered_particle`, which assumes nothing, is worth its extra cost. + +**The knobs, and which step each one controls.** Steps 1–3 above are the proposal; step 4 is the same *choose ``\beta_{k+1}`` → reweight → resample → mutate* loop the tempered filter runs, differing only in where it starts. + +| option | step it acts on | what it does | +|---|---|---| +| `measurement_error` | steps 1–3 | ``H`` enters ``M = I + B_o'H^{-1}B_o`` directly, so it sets both the proposal's width and its centre — not just the weights, as it does for the bootstrap filter. | +| `particle_target_ratio` | choosing ``\beta_{k+1}`` | Same inefficiency target as the tempered filter, applied to ``L = \log\tilde\pi - \log q``. Because ``q`` is already close to the target, the solved step is usually the whole way: ``\beta_1 = 1``, one stage, and the bridge costs nothing. | +| `particle_max_stages` | the bridge loop | Cap on stages. Averages 1.2 here against the tempered filter's ~9, so the cap is far from binding in ordinary periods. | +| `particle_mh_steps` | mutate | Metropolis sweeps per stage, preconditioned by ``M^{-1}`` — the proposal's own covariance, which is the right shape at both ends of the bridge. Defaults to `2` here rather than `4`, because bridging from a good proposal needs less rejuvenation; the *estimates* are flat in this knob and only the likelihood discriminates. | +| `particle_mh_scale` | mutate | Starting step size, adapted during the run exactly as in the tempered filter. | +| `particle_resampling`, `particle_resampling_threshold` | resample | As for the tempered filter. Unlike it, this filter resamples only when the threshold is crossed, which in an ordinary period means once. | +| `n_particles`, `particle_rng` | all of them | Cloud size and the random stream. | + +The proposal's own two settings — two Gauss–Newton refinement steps and a width of one Laplace scale — are deliberately not exposed. Both were swept and both are flat or worse in either direction; the measurements are in [Tuning it: the settings barely matter, and here is the evidence](@ref) and in the comments in `src/filter/particle.jl`. + +#### Does it survive a crisis? COVID on the euro-area data + +The obvious worry about a filter built on a linearisation is what it does when the data stops behaving. The euro-area sample contains the sharpest test available: 2020Q2 sits about 170 measurement-error units away from the sample mean across the seven observables, and 2020Q3 about 149. Four things were checked. + +**It runs.** Every shock estimate is finite in every period under every seed. No failure penalty is triggered. + +**The bridge spends where it is needed.** This is the design working as intended — the schedule is adaptive, so it buys extra stages exactly in the periods that are hard and nowhere else: + +| | stages per period | effective sample size (mean) | (worst) | +|---|---|---|---| +| the 207 ordinary periods | 1.39 | 0.96 | 0.50 | +| the 8 COVID quarters | 2.88 | 0.90 | **0.78** | + +2020Q2, the hardest quarter in the sample, takes six stages and comes out with an effective sample size of 1.00. The *worst* COVID quarter is better defended than the worst ordinary one. + +**The estimates agree with the filter that assumes nothing.** Through the COVID window the guided and tempered filters give the same picture — for 2020Q2, a wage-markup shock of ``4.48 \pm 0.47`` against the tempered filter's ``4.67``, a risk-premium shock of ``-2.53 \pm 0.26`` against ``-2.80``. The inversion filter differs sharply there (``5.78`` for the technology shock where the particle filters say ``0.33``), which is not a disagreement about the filter but about the model: with no measurement error the inversion filter must explain the entire COVID observation with structural shocks, so it produces enormous ones. + +**It does not contaminate what comes after.** The concern with a cloud that has been through an extreme observation is that it collapses and never recovers. Measuring the across-seed dispersion era by era within one sample says otherwise: + +| era | periods | dispersion | +|---|---|---| +| pre-2000 | 117 | 0.167 | +| 2008–09 financial crisis | 8 | 0.108 | +| 2010–19 calm | 40 | 0.100 | +| 2020–21 COVID | 8 | 0.229 | +| **2022 onwards** | 10 | **0.108** | + +The COVID quarters themselves are harder, as they should be. The periods after them return to 0.108 — indistinguishable from the calm decade before and from the financial crisis. (The elevated pre-2000 figure is the filter still forgetting its diffuse initial cloud, not a crisis effect.) + +#### Tuning it: the settings barely matter, and here is the evidence + +Every option was swept on the pruned second-order euro-area problem at ``N = 4\,000`` over 32 *paired* seeds — the same seed set for each configuration, because at 10–16 seeds the same setting measured 0.058 and 0.091 in two runs and nothing can be concluded from that. `sd·√t` is the cost-normalised figure, since a Monte-Carlo error falls like one over the square root of the work. + +| option | values tried | dispersion | cost | best `sd·√t` at | +|---|---|---|---|---| +| `particle_mh_steps` | 0, 1, 2, **4**, 8 | 0.097 – 0.091 | 2.4 – 9.2 s | 1 | +| `particle_target_ratio` | 1.2, **1.5**, 2, 3, 10 | 0.085 – 0.102 | 5.2 – 7.2 s | 1.5 / 10 | +| `particle_resampling_threshold` | 0.25, **0.5**, 0.75 | 0.084 – 0.091 | 5.7 – 5.9 s | 0.25 | +| `particle_resampling` | **`:systematic`**, `:stratified` | 0.091, 0.102 | 5.8 – 5.9 s | `:systematic` | +| Gauss–Newton steps (internal) | 0, 1, **2**, 3 | 0.107 – 0.092 | 6.0 – 8.0 s | 2 | + +Every dispersion in that table lies between 0.084 and 0.107, against a measurement standard error of about 13 %. **No option changes the accuracy by a detectable amount**; they change the cost by a factor of four. The defaults are what they are for reasons that survive that: + +- **Gauss–Newton steps = 2** is a genuine optimum rather than a tie. Cutting it does not even save time: a worse-centred proposal makes the bridge take more stages, and a stage costs the same transition a Newton step does (0 steps → 2.07 stages and 8.0 s; 2 steps → 1.30 stages and 6.0 s). +- **`particle_mh_steps` is resolved per filter** — 2 for `:guided_particle`, 4 otherwise — because the two filters are not remotely equally sensitive to it. The guided filter's estimates are flat in this knob (any value from 0 to 8 lands inside the measurement noise) and only its likelihood discriminates, putting the optimum at 2; `:tempered_particle`, which bridges from the prior, halves its dispersion going from one step to four (0.221 against 0.106). Setting `particle_mh_steps = 1` explicitly saves the guided filter roughly 40 % of its runtime at no measured cost to the estimates. +- The nominally-best `particle_resampling_threshold = 0.25` and `particle_target_ratio = 1.2` beat the defaults by 8 % and 7 %, comfortably inside the noise. Chasing those would be fitting to one sample of one problem. + +#### What does not work: buying accuracy with particles + +The one thing worth knowing before spending anything is that on this problem **more particles do not help**. Quadrupling ``N`` from 4 000 to 16 000 leaves the dispersion where it was, at every mutation setting: + +| `particle_mh_steps` | ``N = 4\,000`` | ``N = 16\,000`` | ratio (2.0 would be textbook) | +|---|---|---|---| +| 0 | 0.099 | 0.108 | 0.91 | +| 1 | 0.094 | 0.104 | 0.90 | +| 4 | 0.090 | 0.099 | 0.92 | + +The importance weights are healthy throughout (effective sample size ~0.9 of the cloud), so this is not the weight degeneracy the bridge fixed. It is the *other* degeneracy: the mutation refreshes ``\varepsilon_t`` but never the ancestor states, so over a long sample the cloud settles onto a trajectory that more particles do not change. Replication does average that away — see above — which is why it, and not `n_particles`, is the lever. + +#### Spend compute on replication, not on particles + +The failure above has an unusual and very useful consequence. The heavy-tailed periods are redrawn afresh from every RNG seed, so the error they cause is *independent across runs* even though it does not shrink within one. Averaging ``K`` independent runs therefore cuts the dispersion by ``\sqrt{K}`` exactly, while raising `n_particles` does nothing — and because the filter is an order of magnitude cheaper than the alternatives, ``K`` can be large. Measured on the pruned second-order euro-area problem, guided at ``N = 4\,000``: + +| ``K`` | across-run sd | ``1/\sqrt{K}`` prediction | cost | +|---|---|---|---| +| 1 | 0.089 | 0.089 | 2.3 s | +| 4 | 0.042 | 0.045 | 9.0 s | +| 8 | 0.030 | 0.032 | 18.0 s | +| 16 | **0.017** | 0.022 | 36.1 s | + +The tempered filter reaches 0.089 in 82 s on the same problem, so sixteen guided replicates are **five times tighter at less than half the cost**. The average is also in the right place: over 48 replicates it differs from the tempered filter's own path by an RMS of 0.054, comfortably inside the 0.085 the tempered reference disagrees with *itself* by across two halves of its seeds, at a correlation of 0.994. So this is variance reduction, not a stable wrong answer. + +```julia +using Statistics +shocks = mean(collect(get_estimated_shocks(model, data; + filter = :guided_particle, algorithm = :pruned_second_order, + n_particles = 4_000, particle_rng = Random.Xoshiro(s))) + for s in 1:16) +``` + +The runs are independent, so this parallelises trivially. It is also the honest way to *report* the uncertainty: the spread across those runs is the Monte-Carlo error of the estimate, and it costs nothing extra to look at. + +One detail worth knowing about the reported shocks. With `particle_mh_steps = 0` the filter reports the conditional mean ``\mu_p`` rather than the shock it drew. Both are consistent for ``E[\varepsilon_t \mid y_{1:t}] = E[\mu(x_{t-1}) \mid y_{1:t}]``, but the conditional mean has already integrated the draw out and so carries none of its variance — a Rao-Blackwellisation, exact to the order the linearisation is. With rejuvenation switched on the particles are draws from the exact conditional instead, so the drawn shock is reported. + +**References:** the conditionally optimal importance function is Doucet, Godsill & Andrieu (2000); building it from a local Gaussian approximation is the "unscented"/optimised particle filter family (van der Merwe, Doucet, de Freitas & Wan, 2000; Andreasen, 2013, for DSGE); solving for the shock that explains the observation before sampling around it is the implicit particle filter of Chorin, Morzfeld & Tu (2010) from geophysical data assimilation. Full adaptation in the sense of Pitt & Shephard (1999) was tried and deliberately *not* kept — see the source comment in `src/filter/particle.jl` for the measurement that rules it out here. + ### Smoothing `smooth = true` returns ``E[x_t \mid y_{1:T}]`` rather than ``E[x_t \mid y_{1:t}]``, i.e. estimates that use the *whole* sample. For the particle filters this is done by **fixed-interval smoothing along the filter's genealogy**: every particle surviving at ``T`` carries the ancestral line that produced it, and those lines are draws from the joint smoothing distribution ``p(x_{1:T} \mid y_{1:T})``, so averaging them with the terminal weights gives the smoothed moments directly. diff --git a/ext/ForwardDiffExt.jl b/ext/ForwardDiffExt.jl index 9f564c994..f2b5ca092 100644 --- a/ext/ForwardDiffExt.jl +++ b/ext/ForwardDiffExt.jl @@ -29,6 +29,11 @@ import MacroModelling: ensure_lyapunov_workspace!, evaluate_custom_steady_state_function, solve_nsss_wrapper, update_ss_counter!, factorize_lu!, solve_lu_left!, get_initial_covariance, find_shocks, normalize_presample_periods, + compressed_kron²!, compressed_kron²_power!, compressed_kron³!, + compressed_kron³_power!, compressed_kron², compressed_kron³, + compressed_kron²_power, compressed_kron³_power, + compressed_pair_hessian!, compressed_triple_hessian!, + ensure_sss_kron_buffers!, # Constants DEFAULT_SOLVER_PARAMETERS, DEFAULT_QME_ALGORITHM @@ -66,19 +71,19 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:second_order # Get cached computational constants constants = initialise_constants!(𝓂) so = constants.second_order + cc = ensure_computational_constants!(constants) ℂ = 𝓂.workspaces.second_order T = constants.post_model_macro - s_in_s⁺ = so.s_in_s⁺ - s_in_s = so.s_in_s I_nPast = T.I_nPast - - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - - A = 𝐒₁̂[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed] - B = 𝐒₂̂[T.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂̂[T.past_not_future_and_mixed_idx,kron_s⁺_s⁺] + + nPast = length(x̂) + n_state_aug = nPast + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + # Pre-sliced by the caller (see the primal method in + # `steady_state/stochastic_steady_state.jl`). + A = 𝐒₁̂ + B = 𝐒₂̂ + B̂ = B # Allocate or reuse workspace for partials and SSS kron buffers. # NOTE: when this overload is called from a higher-level ForwardDiff path, @@ -86,8 +91,7 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:second_order # perturbation solver. Since the SSS Newton iter here is intentionally # carried out on the primal (`S`) values only, we allocate fresh `S`-typed # local buffers whenever the cached ones are not `S`-typed. - nPast = length(x̂) - MacroModelling.ensure_sss_kron_buffers!(ℂ, nPast; third_order=false) + ensure_sss_kron_buffers!(ℂ, nPast; third_order=false) if size(ℂ.∂x_second_order) != (nPast, N) || eltype(ℂ.∂x_second_order) !== S ℂ.∂x_second_order = zeros(S, nPast, N) else @@ -95,24 +99,26 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:second_order end ∂x̄ = ℂ.∂x_second_order n_aug = nPast + 1 - if eltype(ℂ.x_aug_buf) === S + n_aug2 = n_aug * (n_aug + 1) ÷ 2 + if eltype(ℂ.x_aug_buf) === S && length(ℂ.kron_x_aug_xx) == n_aug2 && size(ℂ.kron_x_aug_I) == (n_aug2, nPast) x_aug = ℂ.x_aug_buf kron_x_aug = ℂ.kron_x_aug_xx kron_x_aug_I = ℂ.kron_x_aug_I else x_aug = zeros(S, n_aug) - kron_x_aug = zeros(S, n_aug^2) - kron_x_aug_I = zeros(S, n_aug * nPast, nPast) + kron_x_aug = zeros(S, n_aug2) + kron_x_aug_I = zeros(S, n_aug2, nPast) end + state_identity = @view cc.I_state_vol[:, 1:nPast] x_aug[end] = one(S) max_iters = 100 for i in 1:max_iters copyto!(x_aug, 1, x̂, 1, nPast) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I - I_nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) + compressed_kron²_power!(kron_x_aug, x_aug) Δx = A * x̂ + B̂ * kron_x_aug / 2 - x̂ ∂x_lu = ℒ.lu(∂x, check = false) ℒ.issuccess(∂x_lu) || break @@ -126,8 +132,8 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:second_order end copyto!(x_aug, 1, x̂, 1, nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) + compressed_kron²_power!(kron_x_aug, x_aug) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) solved = isapprox(A * x̂ + B̂ * kron_x_aug / 2, x̂, rtol = tol) if solved @@ -136,8 +142,8 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:second_order ∂𝐒₁ = ℱ.partials.(𝐒₁, i) ∂𝐒₂ = ℱ.partials.(𝐒₂, i) - ∂A = ∂𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - ∂B̂ = ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] + ∂A = ∂𝐒₁ + ∂B̂ = ∂𝐒₂ tmp = ∂A * x̂ + ∂B̂ * kron_x_aug / 2 @@ -169,30 +175,24 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:third_order} so = ensure_computational_constants!(𝓂.constants) T = 𝓂.constants.post_model_macro ℂ = 𝓂.workspaces.third_order - s_in_s⁺ = so.s_in_s⁺ - s_in_s = so.s_in_s I_nPast = T.I_nPast - - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - - kron_s⁺_s⁺_s⁺ = so.kron_s⁺_s⁺_s⁺ - - kron_s_s⁺_s⁺ = so.kron_s_s⁺_s⁺ - - A = 𝐒₁̂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - B = 𝐒₂̂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂̂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] - C = 𝐒₃̂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s_s⁺_s⁺] - Ĉ = 𝐒₃̂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺_s⁺] + + nPast = length(x̂) + n_state_aug = nPast + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + n_state_triple = n_state_aug * (n_state_aug + 1) * (n_state_aug + 2) ÷ 6 + # Pre-sliced by the caller, as at second order. + A = 𝐒₁̂ + B = 𝐒₂̂ + B̂ = B + C = 𝐒₃̂ + Ĉ = C # Allocate or reuse workspace for partials and SSS kron buffers. # See note in the `:second_order` overload above — fall back to fresh # `S`-typed local buffers when the cached workspace got mutated to a # `Dual`-typed one upstream. - nPast = length(x̂) - MacroModelling.ensure_sss_kron_buffers!(ℂ, nPast; third_order=true) + ensure_sss_kron_buffers!(ℂ, nPast; third_order=true) if size(ℂ.∂x_third_order) != (nPast, N) || eltype(ℂ.∂x_third_order) !== S ℂ.∂x_third_order = zeros(S, nPast, N) else @@ -200,7 +200,12 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:third_order} end ∂x̄ = ℂ.∂x_third_order n_aug = nPast + 1 - if eltype(ℂ.x_aug_buf) === S + n_aug2 = n_aug * (n_aug + 1) ÷ 2 + n_aug3 = n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6 + if eltype(ℂ.x_aug_buf) === S && length(ℂ.kron_x_aug_xx) == n_aug2 && + length(ℂ.kron_x_aug_x_kron) == n_aug3 && + size(ℂ.kron_x_aug_I) == (n_aug2, nPast) && + size(ℂ.kron_x_kron_I) == (n_aug3, nPast) x_aug = ℂ.x_aug_buf kron_x_aug = ℂ.kron_x_aug_xx kron_x_kron = ℂ.kron_x_aug_x_kron @@ -208,20 +213,21 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:third_order} kron_x_kron_I = ℂ.kron_x_kron_I else x_aug = zeros(S, n_aug) - kron_x_aug = zeros(S, n_aug^2) - kron_x_kron = zeros(S, n_aug^3) - kron_x_aug_I = zeros(S, n_aug * nPast, nPast) - kron_x_kron_I = zeros(S, n_aug^2 * nPast, nPast) + kron_x_aug = zeros(S, n_aug2) + kron_x_kron = zeros(S, n_aug3) + kron_x_aug_I = zeros(S, n_aug2, nPast) + kron_x_kron_I = zeros(S, n_aug3, nPast) end + state_identity = @view so.I_state_vol[:, 1:nPast] x_aug[end] = one(S) max_iters = 100 for i in 1:max_iters copyto!(x_aug, 1, x̂, 1, nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) - ℒ.kron!(kron_x_kron, x_aug, kron_x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) - ℒ.kron!(kron_x_kron_I, kron_x_aug, I_nPast) + compressed_kron²_power!(kron_x_aug, x_aug) + compressed_kron³_power!(kron_x_kron, x_aug) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) + compressed_kron³!(kron_x_kron_I, x_aug, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I + C * kron_x_kron_I / 2 - I_nPast) Δx = A * x̂ + B̂ * kron_x_aug / 2 + Ĉ * kron_x_kron / 6 - x̂ @@ -237,10 +243,10 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:third_order} end copyto!(x_aug, 1, x̂, 1, nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) - ℒ.kron!(kron_x_kron, x_aug, kron_x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) - ℒ.kron!(kron_x_kron_I, kron_x_aug, I_nPast) + compressed_kron²_power!(kron_x_aug, x_aug) + compressed_kron³_power!(kron_x_kron, x_aug) + compressed_kron²!(kron_x_aug_I, x_aug, @view(so.I_state_vol[:, 1:nPast])) + compressed_kron³!(kron_x_kron_I, x_aug, x_aug, @view(so.I_state_vol[:, 1:nPast])) solved = isapprox(A * x̂ + B̂ * kron_x_aug / 2 + Ĉ * kron_x_kron / 6, x̂, rtol = tol) if solved @@ -250,9 +256,9 @@ function MacroModelling.solve_stochastic_steady_state_newton(::Val{:third_order} ∂𝐒₂ = ℱ.partials.(𝐒₂, i) ∂𝐒₃ = ℱ.partials.(𝐒₃, i) - ∂A = ∂𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - ∂B̂ = ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] - ∂Ĉ = ∂𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺_s⁺] + ∂A = ∂𝐒₁ + ∂B̂ = ∂𝐒₂ + ∂Ĉ = ∂𝐒₃ tmp = ∂A * x̂ + ∂B̂ * kron_x_aug / 2 + ∂Ĉ * kron_x_kron / 6 @@ -1165,13 +1171,22 @@ function MacroModelling.find_shocks(::Val{:LagrangeNewton}, # parameter direction. RHS is differentiation of the KKT residual: # g_x = tmp'·λ - 2x → d g_x = (d_Si + 2·d_Si2e·kron(I,x))' · λ # g_λ = si - Si·x - Si2e·kron(x,x) + # 𝐒ⁱ²ᵉ lives in the compressed shock-pair basis, so the shock kron terms + # must be compressed too (mirrors the primal find_shocks). n_x = length(x_f) n_obs = size(Si_f, 1) - kIx = ℒ.kron(J, x_f) + kIx = compressed_kron²(x_f, J) tmp = Si_f + 2 * Si2e_f * kIx λ = tmp' \ (2 .* x_f) - A_mat = reshape(2 * Si2e_f' * λ, n_x, n_x) - 2 * J - kxx = ℒ.kron(x_f, x_f) + A_mat = zeros(V, n_x, n_x) + compressed_pair_hessian!(A_mat, 2 .* (Si2e_f' * λ)) + # The KKT block's -2I: a loop over the diagonal, not `2 .* Matrix(I(n_x))`, + # which materialised an n_x x n_x dense identity and a second temporary for + # no reason. Nothing about `V` required it — this works for `Dual` too. + @inbounds for i in 1:n_x + A_mat[i, i] -= 2 + end + kxx = compressed_kron²_power(x_f) fXλp = [A_mat tmp'; -tmp zeros(V, n_obs, n_obs)] @@ -1250,16 +1265,26 @@ function MacroModelling.find_shocks(::Val{:LagrangeNewton}, # fXλp = [A tmp'; -tmp 0] with # A = reshape((2·Si2e + 6·Si3e·kron(I,kIx))'·λ, n_x, n_x) - 2I # tmp = Si + 2·Si2e·kron(I,x) + 3·Si3e·kron(I,kron(x,x)) + # 𝐒ⁱ²ᵉ/𝐒ⁱ³ᵉ live in the compressed shock pair/triple bases, so the shock + # kron terms must be compressed too (mirrors the primal find_shocks). n_x = length(x_f) n_obs = size(Si_f, 1) - kxx = ℒ.kron(x_f, x_f) - kxxx = ℒ.kron(x_f, kxx) - kIx = ℒ.kron(J, x_f) - kIxx = ℒ.kron(J, kxx) + kxx = compressed_kron²_power(x_f) + kxxx = compressed_kron³_power(x_f) + kIx = compressed_kron²(x_f, J) + kIxx = compressed_kron³(x_f, x_f, J) tmp = Si_f + 2 * Si2e_f * kIx + 3 * Si3e_f * kIxx λ = tmp' \ (2 .* x_f) - A_mat = reshape((2 * Si2e_f + 6 * Si3e_f * ℒ.kron(J, kIx))' * λ, n_x, n_x) - 2 * J + A_mat = zeros(V, n_x, n_x) + compressed_pair_hessian!(A_mat, 2 .* (Si2e_f' * λ)) + compressed_triple_hessian!(A_mat, 6 .* (Si3e_f' * λ), x_f) + # The KKT block's -2I: a loop over the diagonal, not `2 .* Matrix(I(n_x))`, + # which materialised an n_x x n_x dense identity and a second temporary for + # no reason. Nothing about `V` required it — this works for `Dual` too. + @inbounds for i in 1:n_x + A_mat[i, i] -= 2 + end fXλp = [A_mat tmp'; -tmp zeros(V, n_obs, n_obs)] diff --git a/ext/StatsPlotsExt.jl b/ext/StatsPlotsExt.jl index 282099a54..bb545a5be 100644 --- a/ext/StatsPlotsExt.jl +++ b/ext/StatsPlotsExt.jl @@ -715,10 +715,10 @@ function plot_model_estimates(𝓂::ℳ, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = MacroModelling.DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = MacroModelling.DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = MacroModelling.DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = MacroModelling.DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = MacroModelling.DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = MacroModelling.DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = MacroModelling.DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = MacroModelling.DEFAULT_PARTICLE_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -856,8 +856,8 @@ function plot_model_estimates(𝓂::ℳ, if filter ∈ MacroModelling.PARTICLE_FILTERS extra_kw = merge(extra_kw, (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, - particle_rng, tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale)) + particle_rng, particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale)) end if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = MacroModelling.DEFAULT_MAXLOG @@ -1396,10 +1396,10 @@ function plot_model_estimates!(𝓂::ℳ, particle_resampling_threshold::Real = MacroModelling.DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = MacroModelling.DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = MacroModelling.DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = MacroModelling.DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = MacroModelling.DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = MacroModelling.DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = MacroModelling.DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = MacroModelling.DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = MacroModelling.DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = MacroModelling.DEFAULT_PARTICLE_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, variables::Union{Symbol_input,String_input} = DEFAULT_VARIABLES_EXCLUDING_OBC, shocks::Union{Symbol_input,String_input} = DEFAULT_SHOCK_SELECTION, @@ -1525,8 +1525,8 @@ function plot_model_estimates!(𝓂::ℳ, particle_kw = filter ∈ MacroModelling.PARTICLE_FILTERS ? (; measurement_error = MacroModelling.resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng, - tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale) : NamedTuple() + particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale) : NamedTuple() if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = MacroModelling.DEFAULT_MAXLOG diff --git a/src/MacroModelling.jl b/src/MacroModelling.jl index e52cf0366..6bb3c48b0 100644 --- a/src/MacroModelling.jl +++ b/src/MacroModelling.jl @@ -189,6 +189,7 @@ include("./algorithms/nonlinear_solver.jl") include("./algorithms/quadratic_matrix_equation.jl") include("./filter/find_shocks.jl") +include("./filter/decomposition.jl") include("./filter/inversion.jl") include("./filter/kalman.jl") include("./filter/particle.jl") @@ -1270,20 +1271,6 @@ function get_and_check_observables(T::post_model_macro, data::KeyedArray)::Vecto return observables_symbols end -function x_kron_II!(buffer::Matrix{T}, x::Vector{T}) where T - n = length(x) - m = size(buffer,2) - - # @assert size(buffer, 1) == n^3 "Buffer must have n^2 rows." - # @assert size(buffer, 2) == n^2 "Buffer must have n columns." - - @inbounds for j in 1:m - for i in 1:n - buffer[(j - 1) * n + i, j] = x[i] - end - end -end - # Dead code: bivariate_moment, product_moments, multiplicate, generateSumVectors — never called anywhere # function bivariate_moment(moment::Vector{Int}, rho::Int)::Int # if (moment[1] + moment[2]) % 2 == 1 @@ -2428,7 +2415,7 @@ end function pruned_second_order_state_update(pruned_states::AbstractVector{<:AbstractVector{T}}, shock::AbstractVector{S}, past_idx, n_states::Int, 𝐒₁, 𝐒₂) where {T <: Real, S <: Real} aug_state₁ = [pruned_states[1][past_idx]; 1; shock] aug_state₂ = [pruned_states[2][past_idx]; 0; zero(shock)] - return [𝐒₁ * aug_state₁, 𝐒₁ * aug_state₂ + 𝐒₂ * ℒ.kron(aug_state₁, aug_state₁) / 2] + return [𝐒₁ * aug_state₁, 𝐒₁ * aug_state₂ + 𝐒₂ * compressed_kron²_power(aug_state₁) / 2] end function pruned_second_order_state_update(state::AbstractVector{T}, shock::AbstractVector{S}, past_idx, n_states::Int, 𝐒₁, 𝐒₂) where {T <: Real, S <: Real} @@ -2440,8 +2427,8 @@ function pruned_third_order_state_update(pruned_states::AbstractVector{<:Abstrac aug_state₁̂ = [pruned_states[1][past_idx]; 0; shock] aug_state₂ = [pruned_states[2][past_idx]; 0; zero(shock)] aug_state₃ = [pruned_states[3][past_idx]; 0; zero(shock)] - kron_aug_state₁ = ℒ.kron(aug_state₁, aug_state₁) - return [𝐒₁ * aug_state₁, 𝐒₁ * aug_state₂ + 𝐒₂ * kron_aug_state₁ / 2, 𝐒₁ * aug_state₃ + 𝐒₂ * ℒ.kron(aug_state₁̂, aug_state₂) + 𝐒₃ * ℒ.kron(kron_aug_state₁,aug_state₁) / 6] + kron_aug_state₁ = compressed_kron²_power(aug_state₁) + return [𝐒₁ * aug_state₁, 𝐒₁ * aug_state₂ + 𝐒₂ * kron_aug_state₁ / 2, 𝐒₁ * aug_state₃ + 𝐒₂ * compressed_kron²(aug_state₁̂, aug_state₂) + 𝐒₃ * compressed_kron³_power(aug_state₁) / 6] end function pruned_third_order_state_update(state::AbstractVector{T}, shock::AbstractVector{S}, past_idx, n_states::Int, 𝐒₁, 𝐒₂, 𝐒₃) where {T <: Real, S <: Real} @@ -2465,28 +2452,28 @@ end return Ŝ₁ * aug_state end elseif algorithm ∈ [:second_order, :third_order] - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution Ŝ₁̂ = [Ŝ₁[:,1:nPast] zeros(nVars) Ŝ₁[:,nPast+1:end]] if algorithm == :second_order state_update = function(state::Vector{T}, shock::Vector{S}) where {T,S} aug_state = [state[past_idx]; 1; shock] - return Ŝ₁̂ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + return Ŝ₁̂ * aug_state + 𝐒₂ * compressed_kron²_power(aug_state) / 2 end else # :third_order - 𝐒₃ = 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ + 𝐒₃ = 𝓂.caches.third_order_solution state_update = function(state::Vector{T}, shock::Vector{S}) where {T,S} aug_state = [state[past_idx]; 1; shock] - return Ŝ₁̂ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + 𝐒₃ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 + return Ŝ₁̂ * aug_state + 𝐒₂ * compressed_kron²_power(aug_state) / 2 + 𝐒₃ * compressed_kron³_power(aug_state) / 6 end end elseif algorithm == :pruned_second_order - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution Ŝ₁̂ = [Ŝ₁[:,1:nPast] zeros(nVars) Ŝ₁[:,nPast+1:end]] state_update = (state, shock) -> pruned_second_order_state_update(state, shock, past_idx, nVars, Ŝ₁̂, 𝐒₂) elseif algorithm == :pruned_third_order - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ - 𝐒₃ = 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ + 𝐒₂ = 𝓂.caches.second_order_solution + 𝐒₃ = 𝓂.caches.third_order_solution Ŝ₁̂ = [Ŝ₁[:,1:nPast] zeros(nVars) Ŝ₁[:,nPast+1:end]] state_update = (state, shock) -> pruned_third_order_state_update(state, shock, past_idx, nVars, Ŝ₁̂, 𝐒₂, 𝐒₃) end @@ -2500,30 +2487,30 @@ end elseif algorithm ∈ [:second_order, :third_order] S₁ = 𝓂.caches.first_order_solution_matrix 𝐒₁ = [S₁[:,1:nPast] zeros(nVars) S₁[:,nPast+1:end]] - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution if algorithm == :second_order state_update = function(state::Vector{T}, shock::Vector{S}) where {T,S} aug_state = [state[past_idx]; 1; shock] - return 𝐒₁ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + return 𝐒₁ * aug_state + 𝐒₂ * compressed_kron²_power(aug_state) / 2 end else # :third_order - 𝐒₃ = 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ + 𝐒₃ = 𝓂.caches.third_order_solution state_update = function(state::Vector{T}, shock::Vector{S}) where {T,S} aug_state = [state[past_idx]; 1; shock] - return 𝐒₁ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + 𝐒₃ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 + return 𝐒₁ * aug_state + 𝐒₂ * compressed_kron²_power(aug_state) / 2 + 𝐒₃ * compressed_kron³_power(aug_state) / 6 end end elseif algorithm == :pruned_second_order S₁ = 𝓂.caches.first_order_solution_matrix 𝐒₁ = [S₁[:,1:nPast] zeros(nVars) S₁[:,nPast+1:end]] - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution state_update = (state, shock) -> pruned_second_order_state_update(state, shock, past_idx, nVars, 𝐒₁, 𝐒₂) elseif algorithm == :pruned_third_order S₁ = 𝓂.caches.first_order_solution_matrix 𝐒₁ = [S₁[:,1:nPast] zeros(nVars) S₁[:,nPast+1:end]] - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ - 𝐒₃ = 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ + 𝐒₂ = 𝓂.caches.second_order_solution + 𝐒₃ = 𝓂.caches.third_order_solution state_update = (state, shock) -> pruned_third_order_state_update(state, shock, past_idx, nVars, 𝐒₁, 𝐒₂, 𝐒₃) end end diff --git a/src/common_docstrings.jl b/src/common_docstrings.jl index 840780b1c..401300af6 100644 --- a/src/common_docstrings.jl +++ b/src/common_docstrings.jl @@ -13,7 +13,7 @@ const GENERALISED_IRF® = "`generalised_irf` [Default: `$(DEFAULT_GENERALISED_IR const GENERALISED_IRF_WARMUP_ITERATIONS® = "`generalised_irf_warmup_iterations` [Default: `$(DEFAULT_GENERALISED_IRF_WARMUP)`, Type: `Int`]: number of warm-up iterations used to draw the baseline paths in the generalised IRF simulation. Only applied when `generalised_irf = true`." const GENERALISED_IRF_DRAWS® = "`generalised_irf_draws` [Default: `$(DEFAULT_GENERALISED_IRF_DRAWS)`, Type: `Int`]: number of Monte Carlo draws used to compute the generalised IRF. Only applied when `generalised_irf = true`." const ALGORITHM® = "`algorithm` [Default: `$(DEFAULT_ALGORITHM)`, Type: `Symbol`]: algorithm to solve for the dynamics of the model. Available algorithms: `:first_order`, `:second_order`, `:pruned_second_order`, `:third_order`, `:pruned_third_order`" -const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal) and `:tempered_particle` (lowest variance per particle). They require measurement error (see `measurement_error`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:bootstrap_particle`. Smoothing (`smooth = true`) is available for the Kalman filter (Durbin-Koopman) and the particle filters (fixed-interval smoothing along the filter genealogy), but not for the inversion filter. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." +const FILTER® = "`filter` [Default: selector that chooses `$(DEFAULT_FILTER_SELECTOR(DEFAULT_ALGORITHM))` in case `algorithm = $(DEFAULT_ALGORITHM)` and `:inversion` otherwise, Type: `Symbol`]: filter used to compute the variables and shocks given the data, model, and parameters. The Kalman filter (`:kalman`) is exact but only valid for linear problems. The inversion filter (`:inversion`) works for linear and nonlinear models and backs out the structural shocks that reproduce the data exactly (so it admits no measurement error and needs at least as many shocks as observables). It is by far the fastest of the nonlinear options, but it can fail: at higher order it solves a nonlinear system per period, and if no shock reproduces the observation — or the solver does not converge to one — the whole likelihood is rejected rather than merely degraded. Because it pins the state down exactly, its smoothed and filtered estimates are the same object. The particle filters integrate the shocks out by Monte Carlo and work for linear and nonlinear models: `:bootstrap_particle` (sequential importance resampling), `:auxiliary_particle` (look-ahead proposal), `:tempered_particle` (bridges from the prior) and `:guided_particle` (conditionally optimal proposal). `:guided_particle` draws each shock from its own conditional given the ancestor and the observation, rather than blindly, and is usually both the cheapest and the most accurate of the four. They require measurement error (see `measurement_error`) and are stochastic, non-differentiable estimators suited to gradient-free samplers; `:particle` is an alias for `:guided_particle`. Smoothing (`smooth = true`) is available for the Kalman filter (Durbin-Koopman) and the particle filters (fixed-interval smoothing along the filter genealogy), but not for the inversion filter. If a nonlinear solution algorithm is selected and the default is used, the inversion filter is applied automatically. See the Filters section of the documentation for guidance on choosing between them." const LEVELS® = "return levels or absolute deviations from the relevant steady state corresponding to the solution algorithm (e.g. stochastic steady state for higher order solution algorithms)." const CONDITIONS® = "`conditions` [Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}}`]: conditions for which to find the corresponding shocks. The input can have multiple formats, but for all types of entries, the first dimension corresponds to variables and the second dimension to the number of periods. The conditions can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the conditions are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as conditions. Note that conditioning variables to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input conditions is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as conditions and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of variables (of type `Symbol` or `String`) for which conditions are specified can be included and all other variables are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the conditions for the specified variables bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." const SHOCK_CONDITIONS® = "`shocks` [Default: `nothing`, Type: `Union{Matrix{Union{Nothing,Float64}}, SparseMatrixCSC{Float64}, KeyedArray{Union{Nothing,Float64}}, KeyedArray{Float64}, Nothing}`]: known values of shocks. This argument allows including certain shock values. By entering restrictions on the shocks in this way the problem to match the conditions on endogenous variables is restricted to the remaining free shocks in the respective period. The input can have multiple formats, but for all types of entries, the first dimension corresponds to shocks and the second dimension to the number of periods. `shocks` can be specified using a matrix of type `Matrix{Union{Nothing,Float64}}`. In this case the shocks are matrix elements of type `Float64` and all remaining (free) entries are `nothing`. A `SparseMatrixCSC{Float64}` can also be used as input. In this case only non-zero elements are taken as certain shock values. Note that conditioning shocks to be zero using a `SparseMatrixCSC{Float64}` as input is not possible (use other input formats to do so). Another possibility to input known shocks is by using a `KeyedArray`. The `KeyedArray` type is provided by the `AxisKeys` package. A `KeyedArray{Union{Nothing,Float64}}` can be used where, similar to `Matrix{Union{Nothing,Float64}}`, all entries of type `Float64` are recognised as known shocks and all other entries have to be `nothing`. Furthermore, in the primary axis a subset of shocks (of type `Symbol` or `String`) for which values are specified can be included and all other shocks are considered free. The same goes for the case when using `KeyedArray{Float64}}` as input, whereas in this case the values for the specified shocks bind for all periods specified in the `KeyedArray`, because there are no `nothing` entries permitted with this type." @@ -31,21 +31,21 @@ const PARTICLE_RESAMPLING® = "`particle_resampling` [Default: `:$(DEFAULT_PARTI const PARTICLE_RESAMPLING_THRESHOLD® = "`particle_resampling_threshold` [Default: `$(DEFAULT_PARTICLE_RESAMPLING_THRESHOLD)`, Type: `Real`]: resample whenever the effective sample size falls below `particle_resampling_threshold * n_particles`." const PARTICLE_INITIAL_STATE_SCALING® = "`particle_initial_state_scaling` [Default: `$(DEFAULT_PARTICLE_INITIAL_STATE_SCALING)`, Type: `Real`]: scales the covariance of the initial particle cloud around the initial state." const PARTICLE_RNG® = "`particle_rng` [Default: `Random.default_rng()`, Type: `AbstractRNG`]: random number generator used by the particle filters (pass a seeded RNG for reproducible results)." -const TEMPERING_TARGET_RATIO® = "`tempering_target_ratio` [Default: `$(DEFAULT_TEMPERING_TARGET_RATIO)`, Type: `Real`]: target inefficiency ratio that sets the tempering schedule of `filter = :tempered_particle`." -const TEMPERING_MH_STEPS® = "`tempering_mh_steps` [Default: `$(DEFAULT_TEMPERING_MH_STEPS)`, Type: `Int`]: number of Metropolis-Hastings mutation steps per tempering stage." -const TEMPERING_MAX_STAGES® = "`tempering_max_stages` [Default: `$(DEFAULT_TEMPERING_MAX_STAGES)`, Type: `Int`]: cap on the number of tempering stages per period." -const TEMPERING_MH_SCALE® = "`tempering_mh_scale` [Default: `$(DEFAULT_TEMPERING_MH_SCALE)`, Type: `Real`]: scale of the random-walk Metropolis-Hastings proposal used in the mutation step." -const TEMPERING_KEYWORDS® = join(("- " * TEMPERING_TARGET_RATIO®, - "- " * TEMPERING_MH_STEPS®, - "- " * TEMPERING_MAX_STAGES®, - "- " * TEMPERING_MH_SCALE®), "\n") +const PARTICLE_TARGET_RATIO® = "`particle_target_ratio` [Default: `$(DEFAULT_PARTICLE_TARGET_RATIO)`, Type: `Real`]: target inefficiency ratio that sets the bridging schedule of `:tempered_particle` and `:guided_particle` — how much weight degeneracy one intermediate step may add, which is what picks the step sizes. Lower means more, gentler bridging steps: each one then discards fewer particles at its resampling, which is the main thing limiting the accuracy of the filtered estimates. It is a more effective use of compute than raising `n_particles`, at the cost of more stages per period." +const PARTICLE_MH_STEPS® = "`particle_mh_steps` [Default: `$(DEFAULT_GUIDED_MH_STEPS)` for `filter = :guided_particle`, `$(DEFAULT_TEMPERED_MH_STEPS)` otherwise, Type: `Int`]: number of Metropolis-Hastings mutation steps per bridging stage, for `:tempered_particle` and `:guided_particle`. This is what rejuvenates the particle cloud. For `:tempered_particle`, which bridges all the way from the prior, it rather than `n_particles` is what limits accuracy — raise it first when estimates look seed-sensitive. The guided filter bridges from a proposal that is already close to the target and needs far less of it, which is why its default is lower." +const PARTICLE_MAX_STAGES® = "`particle_max_stages` [Default: `$(DEFAULT_PARTICLE_MAX_STAGES)`, Type: `Int`]: cap on the number of bridging stages per period, for `:tempered_particle` and `:guided_particle` alike. The tempered filter typically uses of the order of ten; the guided filter reaches its target in a single stage in an ordinary period and spends more only where its proposal fits badly." +const PARTICLE_MH_SCALE® = "`particle_mh_scale` [Default: `$(DEFAULT_PARTICLE_MH_SCALE)`, Type: `Real`]: starting scale of the random-walk Metropolis-Hastings proposal used in the mutation step of `:tempered_particle` and `:guided_particle`. The proposal is preconditioned by the stage's own posterior scale — which changes with the stage for the tempered filter and is the proposal covariance for the guided one — so a value near one is appropriate whatever the model, and the filter adapts the scale during the run towards a $(round(Int, 100 * DEFAULT_PARTICLE_MH_TARGET_ACCEPTANCE))% acceptance rate." +const PARTICLE_BRIDGING_KEYWORDS® = join(("- " * PARTICLE_TARGET_RATIO®, + "- " * PARTICLE_MH_STEPS®, + "- " * PARTICLE_MAX_STAGES®, + "- " * PARTICLE_MH_SCALE®), "\n") const PARTICLE_FILTER_KEYWORDS® = join(("- " * MEASUREMENT_ERROR®, "- " * N_PARTICLES®, "- " * PARTICLE_RESAMPLING®, "- " * PARTICLE_RESAMPLING_THRESHOLD®, "- " * PARTICLE_INITIAL_STATE_SCALING®, "- " * PARTICLE_RNG®, - TEMPERING_KEYWORDS®), "\n") + PARTICLE_BRIDGING_KEYWORDS®), "\n") const ON_FAILURE_LOGLIKELIHOOD® = "`on_failure_loglikelihood` [Default: selector that returns `-1e6` for the particle filters and `-Inf` otherwise, Type: `AbstractFloat`]: value to return if the loglikelihood calculation fails (e.g. the solution did not converge). The particle filters default to a large finite penalty rather than `-Inf` because they can fail for purely stochastic reasons, and `-Inf` would kill a sampler's chain state instead of rejecting a single proposal." const INITIAL_COVARIANCE® = "`initial_covariance` [Default: `:theoretical`, Type: `Union{Symbol,AbstractMatrix{<:Real}}`]: how to initialise the filter's state covariance (for the particle filters, the covariance the initial cloud is drawn from). `:theoretical` uses the first-order ergodic values from the Lyapunov equation, `:diagonal` starts diffuse with 10.0 along the diagonal, or supply a matrix of the appropriate size." const DATA_IN_LEVELS® = "`data_in_levels` [Default: `$(DEFAULT_DATA_IN_LEVELS)`, Type: `Bool`]: indicator whether the data is provided in levels. If `true` the input to the data argument will have the non-stochastic steady state subtracted." diff --git a/src/default_options.jl b/src/default_options.jl index 9eb26ebf3..c309fb2cf 100644 --- a/src/default_options.jl +++ b/src/default_options.jl @@ -12,14 +12,19 @@ const DEFAULT_PRESAMPLE_PERIODS = 0 # ── Filter registry ────────────────────────────────────────────────────────── # Each particle-filter variant is its own `filter` value, so the filter is fully # identified by a single symbol (no separate "which particle filter" argument). -const PARTICLE_FILTERS = (:bootstrap_particle, :auxiliary_particle, :tempered_particle) +const PARTICLE_FILTERS = (:bootstrap_particle, :auxiliary_particle, :tempered_particle, :guided_particle) const SUPPORTED_FILTERS = (:kalman, :inversion, PARTICLE_FILTERS...) -# `:particle` is accepted as a convenience alias for the bootstrap filter. -const PARTICLE_FILTER_ALIASES = Dict(:particle => :bootstrap_particle) +# `:particle` is accepted as a convenience alias for the variant a user asking for +# "a particle filter" should get. That is the guided filter: it draws each shock from +# its own conditional rather than blindly, which on a Smets-Wouters-sized problem is +# both an order of magnitude cheaper and several times more accurate than the +# bootstrap filter the alias used to point at. +const PARTICLE_FILTER_ALIASES = Dict(:particle => :guided_particle) # Maps a filter symbol onto the internal variant tag used for dispatch. const PARTICLE_FILTER_VARIANT = Dict(:bootstrap_particle => :bootstrap, :auxiliary_particle => :auxiliary, - :tempered_particle => :tempered) + :tempered_particle => :tempered, + :guided_particle => :guided) # ── Measurement error ──────────────────────────────────────────────────────── # `measurement_error` is the covariance H of ηₜ ~ N(0, H) in yₜ = C xₜ + ηₜ. It is @@ -44,6 +49,12 @@ const DEFAULT_PARTICLE_MEASUREMENT_ERROR_FRACTION = 0.1 const DEFAULT_ON_FAILURE_LOGLIKELIHOOD_SELECTOR = filter -> get(PARTICLE_FILTER_ALIASES, filter, filter) ∈ PARTICLE_FILTERS ? -1e6 : -Inf # ── Particle filter defaults (see `src/filter/particle.jl`) ────────────────── +# +# Naming: a setting shared by more than one particle filter is `PARTICLE_…` and +# its keyword argument is `particle_…`; a setting that belongs to one variant +# carries that variant's name instead (`GUIDED_…`, `TEMPERED_…`). The one +# deliberate exception is `n_particles`, kept under its conventional name. +# # 10_000 particles keeps a Smets-Wouters-sized problem (7 observables, ~180 # periods) accurate to a couple of log-likelihood points in well under a second # per evaluation; raise it when the likelihood is used inside a sampler. @@ -51,11 +62,73 @@ const DEFAULT_N_PARTICLES = 10_000 const DEFAULT_PARTICLE_RESAMPLING = :systematic const DEFAULT_PARTICLE_RESAMPLING_THRESHOLD = 0.5 const DEFAULT_PARTICLE_INITIAL_STATE_SCALING = 1.0 -# Tempered particle filter (Herbst & Schorfheide, 2019) controls -const DEFAULT_TEMPERING_TARGET_RATIO = 2.0 -const DEFAULT_TEMPERING_MH_STEPS = 1 -const DEFAULT_TEMPERING_MAX_STAGES = 100 -const DEFAULT_TEMPERING_MH_SCALE = 0.3 + +# ── Bridging controls, shared by `:tempered_particle` and `:guided_particle` ── +# Both filters reach the period's target through a sequence of intermediate +# distributions, reweighting, resampling and mutating along the way; these set how +# finely they step and how hard they mutate. + +# How much weight inefficiency one bridging step is allowed to add, which is what +# picks the step sizes. Lower means more, smaller steps. Set below Herbst & +# Schorfheide's own value of 2 because for the tempered filter — which has to +# bridge all the way from the prior — the bridging, not the particle count, is +# what limits accuracy, and buying accuracy here is cheaper per unit of compute +# than raising `n_particles`. +const DEFAULT_PARTICLE_TARGET_RATIO = 1.5 + +# Metropolis-Hastings mutation steps per bridging stage. The two filters want +# different amounts and get their own defaults, resolved by the selector below. +# +# The tempered filter is highly sensitive to this: it bridges from the prior, so +# mutation is what rejuvenates a cloud that would otherwise be badly degenerate, +# and going from one step to four roughly halves the run-to-run spread of both the +# estimates and the likelihood. +const DEFAULT_TEMPERED_MH_STEPS = 4 +# The guided filter bridges from a proposal already close to the target, so it +# needs far less. Its *estimates* are flat in this knob — any value from zero +# upwards is within measurement noise — and only the likelihood discriminates, +# putting the optimum at two at both perturbation orders. Anyone re-measuring this +# should know the likelihood dispersion is a much noisier statistic than the +# estimates one (it is the log of an average of heavy-tailed weights) and needs +# paired seeds, and plenty of them, to resolve at all. +const DEFAULT_GUIDED_MH_STEPS = 2 +# Which of the two a call gets, from the filter it selected. +const DEFAULT_PARTICLE_MH_STEPS_SELECTOR = filter -> get(PARTICLE_FILTER_ALIASES, filter, filter) == :guided_particle ? DEFAULT_GUIDED_MH_STEPS : DEFAULT_TEMPERED_MH_STEPS + +const DEFAULT_PARTICLE_MAX_STAGES = 100 +# Starting value for the Metropolis mutation step, in units of the stage's own +# posterior scale (the proposal is preconditioned by it, see `src/filter/particle.jl`). +# 2.38/sqrt(d) is the textbook optimum for a d-dimensional random walk; the filter +# adapts from here towards the target acceptance rate below, so this only sets +# where it starts. +const DEFAULT_PARTICLE_MH_SCALE = 1.0 +# Adaptation of that scale: the target acceptance rate, the gain of the log-scale +# update towards it, and the bounds the scale is clamped to. +const DEFAULT_PARTICLE_MH_TARGET_ACCEPTANCE = 0.25 +const DEFAULT_PARTICLE_MH_ADAPTATION_GAIN = 1.0 +const DEFAULT_PARTICLE_MH_SCALE_BOUNDS = (1e-8, 1e2) + +# ── Guided-filter specifics ────────────────────────────────────────────────── +# Newton steps refining the proposal's centre, the width of the proposal as a +# multiple of the Laplace scale, and whether filtered shock estimates are reported +# from the proposal mean rather than the draw. The reasoning behind each value is +# at its point of use in `src/filter/particle.jl`. +const DEFAULT_GUIDED_NEWTON_STEPS = 2 +const DEFAULT_GUIDED_PROPOSAL_SCALE = 1.0 +const DEFAULT_GUIDED_RAO_BLACKWELL = true + +# ── Internal sizing and diagnostics, common to every particle filter ────────── +# Transition scratch budget, smallest block worth a `gemm`, and the arithmetic per +# sweep below which the swarm is propagated on the calling thread. +const DEFAULT_PARTICLE_SCRATCH_BYTES = 256 * 2^20 +const DEFAULT_PARTICLE_MIN_BLOCK = 64 +const DEFAULT_PARTICLE_PARALLEL_MIN_WORK = 1 << 20 +# Chunking for the memory-bound copy passes (resampling gather, Metropolis accept). +const DEFAULT_PARTICLE_COPY_CHUNK = 2048 +const DEFAULT_PARTICLE_COPY_MAX_TASKS = 8 +# Mean effective sample size below which the filter warns that its proposal is a +# poor fit to the data. +const DEFAULT_PARTICLE_LOW_ESS_FRACTION = 0.05 const DEFAULT_DATA_IN_LEVELS = true const DEFAULT_LEVELS = true diff --git a/src/filter/decomposition.jl b/src/filter/decomposition.jl new file mode 100644 index 000000000..219fd0a93 --- /dev/null +++ b/src/filter/decomposition.jl @@ -0,0 +1,653 @@ +@stable default_mode = "disable" begin + +# Historical shock decomposition, shared by the inversion and the particle +# filters. Both produce a shock path and then attribute the observed trajectory +# to the individual shocks, and that attribution belongs to neither filter, so it +# lives here: `inversion.jl` calls in from its `calculate_*` routines, +# `particle.jl` from `run_particle_estimates`. +# +# Not to be confused with `src/aumann_shapley.jl`, which applies the same +# cooperative-game idea to the *variance* decomposition of the moments. +# +# --------------------------------------------------------------------- +# Aumann–Shapley shock decomposition (marginal-contribution driver) +# --------------------------------------------------------------------- +# +# Computes per-period Shapley shares for the inversion- and particle-filter shock +# decomposition under pruned 2nd / 3rd order solutions via the path- +# integral identity +# φᵢ(v, t) = ∫₀¹ ∂Ṽ_t(s·𝟙)/∂xᵢ ds +# ≈ Σ_k w_k · ∂Ṽ_t(s_k·𝟙)/∂xᵢ (Gauss–Legendre) +# where Ṽ_t is the polynomial extension of `S → ŝ_t(S)[v]`. The production +# drivers start from the low-order Gauss–Legendre rules (2 nodes at 2nd +# order, 3 at 3rd order) and rerun with 4 nodes only when the coarse +# Shapley-efficiency closure residual exceeds `1e-3`. +# +# Per period the driver maintains, for every Gauss–Legendre node s_k: +# - one primal pruned-state trajectory under shocks scaled by s_k; +# - one tangent trajectory per shock direction i = 1..nᵉ giving +# ∂ŝ_t/∂xᵢ at x = s_k·𝟙. +# Each tangent recursion mirrors the primal recursion with derivatives +# threaded by the chain rule; the shock contribution to the tangent's +# augmented vector picks up `εᵢ_t · eᵢ` because ∂(xᵢ·εᵢ_t)/∂xᵢ = εᵢ_t. +# A separate s = 0 primal trajectory is propagated to obtain V(∅). +# +# Each function fills `decomposition[:, 1:nᵉ, :]` with per-shock Aumann– +# Shapley shares of the incremental response `V(N) − V(∅)`. By linearity, +# these columns equal each shock's standalone effect plus its allocated share +# of the cross-shock interaction. The `decomposition[:, nᵉ+1, :]` column keeps +# the zero-shock / initial-values path `V(∅)` plus any tiny numerical closure +# residual, matching the layout the public API expects when +# `marginal_contribution = true`. + +# Pruned 2nd-order state update: new_s1, new_s2 = 𝐒₁·aug1, 𝐒₁·aug2 + ½𝐒₂·(aug1⊗aug1). +# Builds augmented vectors via copyto!, computes kron product, and applies solution matrices in-place. +function pruned_state_update_2nd_order!( + new_s1, new_s2, s1, s2, past_idx, shock_dir, zero_dir, + aug1, aug2, kk, 𝐒) + n_past = length(past_idx) + @views copyto!(aug1[1:n_past], s1[past_idx]) + aug1[n_past + 1] = 1.0 + copyto!(aug1, n_past + 2, shock_dir, 1, length(shock_dir)) + @views copyto!(aug2[1:n_past], s2[past_idx]) + aug2[n_past + 1] = 0.0 + copyto!(aug2, n_past + 2, zero_dir, 1, length(zero_dir)) + compressed_kron²_power!(kk, aug1) + ℒ.mul!(new_s1, 𝐒[1], aug1) + ℒ.mul!(new_s2, 𝐒[1], aug2) + ℒ.mul!(new_s2, 𝐒[2], kk, 0.5, 1.0) + return nothing +end + +# Pruned 3rd-order state update: extends 2nd-order with new_s3 = 𝐒₁·aug3 + 𝐒₂·(aug1̂⊗aug2) + ⅙𝐒₃·(aug1⊗aug1⊗aug1). +# aug1̂ is the no-constant variant of aug1 (constant slot = 0). +function pruned_state_update_3rd_order!( + new_s1, new_s2, new_s3, s1, s2, s3, past_idx, shock_dir, zero_dir, + aug1, aug1̂, aug2, aug3, k11, k12̂, k111, 𝐒) + n_past = length(past_idx) + @views copyto!(aug1[1:n_past], s1[past_idx]) + aug1[n_past + 1] = 1.0 + copyto!(aug1, n_past + 2, shock_dir, 1, length(shock_dir)) + @views copyto!(aug1̂[1:n_past], s1[past_idx]) + aug1̂[n_past + 1] = 0.0 + copyto!(aug1̂, n_past + 2, shock_dir, 1, length(shock_dir)) + @views copyto!(aug2[1:n_past], s2[past_idx]) + aug2[n_past + 1] = 0.0 + copyto!(aug2, n_past + 2, zero_dir, 1, length(zero_dir)) + @views copyto!(aug3[1:n_past], s3[past_idx]) + aug3[n_past + 1] = 0.0 + copyto!(aug3, n_past + 2, zero_dir, 1, length(zero_dir)) + compressed_kron²_power!(k11, aug1) + compressed_kron²!(k12̂, aug1̂, aug2) + compressed_kron³_power!(k111, aug1) + ℒ.mul!(new_s1, 𝐒[1], aug1) + ℒ.mul!(new_s2, 𝐒[1], aug2) + ℒ.mul!(new_s2, 𝐒[2], k11, 0.5, 1.0) + ℒ.mul!(new_s3, 𝐒[1], aug3) + ℒ.mul!(new_s3, 𝐒[2], k12̂, 1.0, 1.0) + ℒ.mul!(new_s3, 𝐒[3], k111, 1/6, 1.0) + return nothing +end + +function advance_aumann_shapley_pruned_2nd_warmup!( + s₁, s₂, s₁⁺, s₂⁺, + ds₁ᵢ, ds₂ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, + warmup_shocks::AbstractMatrix, + sₖ, + iₚ, + a₁, a₂, da₁, da₂, + k₁₁, dk₁₁, + ε̄ₜ, εᵢₜ, ε₀, + 𝐒) + nₚ = length(iₚ) + + for w in axes(warmup_shocks, 2) + εₜ = @view warmup_shocks[:, w] + ε̄ₜ .= sₖ .* εₜ + pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε̄ₜ, ε₀, a₁, a₂, k₁₁, 𝐒) + + for i in eachindex(ds₁ᵢ) + fill!(εᵢₜ, 0.0) + εᵢₜ[i] = εₜ[i] + + @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) + da₁[nₚ + 1] = 0.0 + copyto!(da₁, nₚ + 2, εᵢₜ, 1, size(warmup_shocks, 1)) + + @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) + da₂[nₚ + 1] = 0.0 + copyto!(da₂, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) + + # C₂ is symmetric, so d C₂(a₁, a₁) = 2 C₂(da₁, a₁). + compressed_kron²!(dk₁₁, da₁, a₁) + + ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) + # The outer 1/2 cancels the derivative's factor of 2. + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 1.0, 1.0) + + copyto!(ds₁ᵢ[i], ds₁ᵢ⁺[i]) + copyto!(ds₂ᵢ[i], ds₂ᵢ⁺[i]) + end + + copyto!(s₁, s₁⁺) + copyto!(s₂, s₂⁺) + end + + return nothing +end + +function advance_aumann_shapley_pruned_3rd_warmup!( + s₁, s₂, s₃, s₁⁺, s₂⁺, s₃⁺, + ds₁ᵢ, ds₂ᵢ, ds₃ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, ds₃ᵢ⁺, + warmup_shocks::AbstractMatrix, + sₖ, + iₚ, + a₁, a₁⁰, a₂, a₃, da₁, da₂, da₃, + k₁₁, k₁₂⁰, k₁₁₁, dk₁₁, dk₁₂⁰, dk₁₁₁, + k₂tmp, + ε̄ₜ, εᵢₜ, ε₀, + 𝐒) + nₚ = length(iₚ) + + for w in axes(warmup_shocks, 2) + εₜ = @view warmup_shocks[:, w] + ε̄ₜ .= sₖ .* εₜ + pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε̄ₜ, ε₀, + a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) + + for i in eachindex(ds₁ᵢ) + fill!(εᵢₜ, 0.0) + εᵢₜ[i] = εₜ[i] + + @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) + da₁[nₚ + 1] = 0.0 + copyto!(da₁, nₚ + 2, εᵢₜ, 1, size(warmup_shocks, 1)) + + @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) + da₂[nₚ + 1] = 0.0 + copyto!(da₂, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) + + @views copyto!(da₃[1:nₚ], ds₃ᵢ[i][iₚ]) + da₃[nₚ + 1] = 0.0 + copyto!(da₃, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) + + # C₂ is symmetric, so d C₂(a₁, a₁) = 2 C₂(da₁, a₁). + compressed_kron²!(dk₁₁, da₁, a₁) + + compressed_kron²!(dk₁₂⁰, da₁, a₂) + compressed_kron²!(k₂tmp, a₁⁰, da₂) + dk₁₂⁰ .+= k₂tmp + + # C₃ is symmetric, so d C₃(a₁, a₁, a₁) = 3 C₃(da₁, a₁, a₁). + compressed_kron³!(dk₁₁₁, da₁, a₁, a₁) + + ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) + # The outer 1/2 cancels the pair derivative's factor of 2. + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 1.0, 1.0) + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[1], da₃) + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[2], dk₁₂⁰, 1.0, 1.0) + # The outer 1/6 times the cubic derivative's factor of 3 is 1/2. + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[3], dk₁₁₁, 1/2, 1.0) + + copyto!(ds₁ᵢ[i], ds₁ᵢ⁺[i]) + copyto!(ds₂ᵢ[i], ds₂ᵢ⁺[i]) + copyto!(ds₃ᵢ[i], ds₃ᵢ⁺[i]) + end + + copyto!(s₁, s₁⁺) + copyto!(s₂, s₂⁺) + copyto!(s₃, s₃⁺) + end + + return nothing +end + +function aumann_shapley_shock_decomposition_pruned_2nd_order!( + decomposition::AbstractArray{R}, + variables::AbstractMatrix, + shocks::AbstractMatrix, + initial_state, + 𝐒, + T, + nE::Int; + verbose::Bool = false, + warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real + n_nodes = 2 + max_error = aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, + variables, + shocks, + initial_state, + 𝐒, + T, + nE, + n_nodes; + warmup_shocks = warmup_shocks) + if verbose + println("Aumann-Shapley second-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) + end + while max_error > AUMANN_SHAPLEY_REFINEMENT_RTOL && n_nodes < AUMANN_SHAPLEY_REFINEMENT_MAX_NODES + next_nodes = min(n_nodes + 1, AUMANN_SHAPLEY_REFINEMENT_MAX_NODES) + if verbose + println("Aumann-Shapley second-order shock decomposition rerunning with ", next_nodes, " nodes after closure error ", max_error, " at ", n_nodes, " nodes") + end + n_nodes = next_nodes + max_error = aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, + variables, + shocks, + initial_state, + 𝐒, + T, + nE, + n_nodes; + warmup_shocks = warmup_shocks) + if verbose + println("Aumann-Shapley second-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) + end + end + return decomposition +end + +function aumann_shapley_shock_decomposition_pruned_2nd_order!( + decomposition::AbstractArray{R}, + variables::AbstractMatrix, + shocks::AbstractMatrix, + initial_state, + 𝐒, + T, + nE::Int, + n_nodes::Int; + warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real + nᵥ = T.nVars + iₚ = T.past_not_future_and_mixed_idx + nₚ = length(iₚ) + n_aug = nₚ + 1 + nE + n_kron = n_aug * (n_aug + 1) ÷ 2 + nₜ = size(decomposition, 3) + + nodes, weights = gausslegendre_unit_interval(n_nodes) + + # Scratch buffers — one set reused sequentially across quadrature nodes. + # s₁/s₂ are primal pruned state components; s₁⁺/s₂⁺ are next-period outputs. + s₁ = zeros(R, nᵥ) + s₂ = zeros(R, nᵥ) + s₁⁺ = zeros(R, nᵥ) + s₂⁺ = zeros(R, nᵥ) + # ds* buffers hold tangent states ∂s/∂xᵢ for each shock direction i. + ds₁ᵢ = [zeros(R, nᵥ) for _ in 1:nE] + ds₂ᵢ = [zeros(R, nᵥ) for _ in 1:nE] + ds₁ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] + ds₂ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] + + # Augmented primal/tangent vectors [past state; constant; shocks]. + a₁ = Vector{R}(undef, n_aug) + a₂ = Vector{R}(undef, n_aug) + da₁ = Vector{R}(undef, n_aug) + da₂ = Vector{R}(undef, n_aug) + # Kronecker workspaces for a₁⊗a₁ and its directional derivative. + k₁₁ = Vector{R}(undef, n_kron) + dk₁₁ = Vector{R}(undef, n_kron) + + # Shock-direction vectors: scaled node shocks, basis shock i, and zero shocks. + ε̄ₜ = zeros(R, nE) + εᵢₜ = zeros(R, nE) + ε₀ = zeros(R, nE) + + # --- Pass 1: V(∅) trajectory (zero shocks) → store in decomposition[:, nE+1, :]. --- + s₁ .= initial_state[1] + s₂ .= initial_state[2] + if !isnothing(warmup_shocks) + for _ in axes(warmup_shocks, 2) + pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε₀, ε₀, a₁, a₂, k₁₁, 𝐒) + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + end + end + # Propagate the baseline path with all shocks set to zero. + for t in 1:nₜ + pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε₀, ε₀, a₁, a₂, k₁₁, 𝐒) + @inbounds for v in 1:nᵥ + decomposition[v, nE + 1, t] = s₁⁺[v] + s₂⁺[v] + end + # Swap current and next buffers instead of allocating a fresh state. + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + end + + # --- Pass 2: one node at a time, accumulate weighted tangents. --- + @views fill!(decomposition[:, 1:nE, :], zero(R)) + + # Quadrature over shock scaling nodes; each node contributes one weighted path. + for k in 1:n_nodes + sₖ = nodes[k] + wₖ = weights[k] + s₁ .= initial_state[1] + s₂ .= initial_state[2] + # Reset the tangent trajectories for this quadrature node. + for i in 1:nE + fill!(ds₁ᵢ[i], 0.0) + fill!(ds₂ᵢ[i], 0.0) + end + if !isnothing(warmup_shocks) + advance_aumann_shapley_pruned_2nd_warmup!(s₁, s₂, s₁⁺, s₂⁺, + ds₁ᵢ, ds₂ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, + warmup_shocks, + sₖ, + iₚ, + a₁, a₂, da₁, da₂, + k₁₁, dk₁₁, + ε̄ₜ, εᵢₜ, ε₀, + 𝐒) + end + + # March forward one period at a time, updating the primal path and all tangents. + for t in 1:nₜ + εₜ = @view shocks[:, t] + ε̄ₜ .= sₖ .* εₜ + pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε̄ₜ, ε₀, a₁, a₂, k₁₁, 𝐒) + + # For each shock direction i, propagate tangent recursions and + # accumulate node-weighted directional derivatives. + for i in 1:nE + fill!(εᵢₜ, 0.0) + εᵢₜ[i] = εₜ[i] + + @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) + da₁[nₚ + 1] = 0.0 + copyto!(da₁, nₚ + 2, εᵢₜ, 1, nE) + + @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) + da₂[nₚ + 1] = 0.0 + copyto!(da₂, nₚ + 2, ε₀, 1, nE) + + # C₂ is symmetric, so d C₂(a₁, a₁) = 2 C₂(da₁, a₁). + compressed_kron²!(dk₁₁, da₁, a₁) + + # Plain form: ds₁ᵢ⁺ = S1 * da₁ + ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) + # The outer 1/2 cancels the derivative's factor of 2. + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 1.0, 1.0) + + @inbounds for v in 1:nᵥ + decomposition[v, i, t] += wₖ * (ds₁ᵢ⁺[i][v] + ds₂ᵢ⁺[i][v]) + end + end + + # Advance the primal and tangent buffers to the next period. + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + for i in 1:nE + ds₁ᵢ[i], ds₁ᵢ⁺[i] = ds₁ᵢ⁺[i], ds₁ᵢ[i] + ds₂ᵢ[i], ds₂ᵢ⁺[i] = ds₂ᵢ⁺[i], ds₂ᵢ[i] + end + end + end + + max_residual = zero(R) + max_reference = zero(R) + @inbounds for t in 1:nₜ, v in 1:nᵥ + ϕsum = zero(R) + for i in 1:nE + ϕsum += decomposition[v, i, t] + end + residual = variables[v, t] - (decomposition[v, nE + 1, t] + ϕsum) + max_residual = max(max_residual, abs(residual)) + max_reference = max(max_reference, abs(variables[v, t])) + end + + T = float(R) + scale = max(T(max_reference), sqrt(eps(T))) + return T(max_residual) / scale +end + + +function aumann_shapley_shock_decomposition_pruned_3rd_order!( + decomposition::AbstractArray{R}, + variables::AbstractMatrix, + shocks::AbstractMatrix, + initial_state, + 𝐒, + T, + nE::Int; + verbose::Bool = false, + warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real + n_nodes = 3 + max_error = aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, + variables, + shocks, + initial_state, + 𝐒, + T, + nE, + n_nodes; + warmup_shocks = warmup_shocks) + if verbose + println("Aumann-Shapley third-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) + end + while max_error > AUMANN_SHAPLEY_REFINEMENT_RTOL && n_nodes < AUMANN_SHAPLEY_REFINEMENT_MAX_NODES + next_nodes = min(n_nodes + 1, AUMANN_SHAPLEY_REFINEMENT_MAX_NODES) + if verbose + println("Aumann-Shapley third-order shock decomposition rerunning with ", next_nodes, " nodes after closure error ", max_error, " at ", n_nodes, " nodes") + end + n_nodes = next_nodes + max_error = aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, + variables, + shocks, + initial_state, + 𝐒, + T, + nE, + n_nodes; + warmup_shocks = warmup_shocks) + if verbose + println("Aumann-Shapley third-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) + end + end + return decomposition +end + +function aumann_shapley_shock_decomposition_pruned_3rd_order!( + decomposition::AbstractArray{R}, + variables::AbstractMatrix, + shocks::AbstractMatrix, + initial_state, + 𝐒, + T, + nE::Int, + n_nodes::Int; + warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real + nᵥ = T.nVars + iₚ = T.past_not_future_and_mixed_idx + nₚ = length(iₚ) + n_aug = nₚ + 1 + nE + n_kron2 = n_aug * (n_aug + 1) ÷ 2 + n_kron3 = n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6 + nₜ = size(decomposition, 3) + + nodes, weights = gausslegendre_unit_interval(n_nodes) + + # Scratch buffers — one set reused sequentially across quadrature nodes. + # s₁/s₂/s₃ are primal pruned state components; s*⁺ are next-period outputs. + s₁ = zeros(R, nᵥ) + s₂ = zeros(R, nᵥ) + s₃ = zeros(R, nᵥ) + s₁⁺ = zeros(R, nᵥ) + s₂⁺ = zeros(R, nᵥ) + s₃⁺ = zeros(R, nᵥ) + # ds* buffers hold tangent states ∂s/∂xᵢ for each shock direction i. + ds₁ᵢ = [zeros(R, nᵥ) for _ in 1:nE] + ds₂ᵢ = [zeros(R, nᵥ) for _ in 1:nE] + ds₃ᵢ = [zeros(R, nᵥ) for _ in 1:nE] + ds₁ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] + ds₂ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] + ds₃ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] + + # Augmented primal/tangent vectors [past state; constant; shocks]. + # a₁⁰ is a₁ with zero constant slot for the third-order cross term. + a₁ = Vector{R}(undef, n_aug) + a₁⁰ = Vector{R}(undef, n_aug) + a₂ = Vector{R}(undef, n_aug) + a₃ = Vector{R}(undef, n_aug) + da₁ = Vector{R}(undef, n_aug) + da₂ = Vector{R}(undef, n_aug) + da₃ = Vector{R}(undef, n_aug) + + # Kronecker workspaces for primal terms and directional derivatives: + # k₁₁=a₁⊗a₁, k₁₂⁰=a₁⁰⊗a₂, k₁₁₁=(a₁⊗a₁)⊗a₁ and their d/dxᵢ variants. + k₁₁ = Vector{R}(undef, n_kron2) + k₁₂⁰ = Vector{R}(undef, n_kron2) + dk₁₁ = Vector{R}(undef, n_kron2) + dk₁₂⁰ = Vector{R}(undef, n_kron2) + k₂tmp = Vector{R}(undef, n_kron2) + k₁₁₁ = Vector{R}(undef, n_kron3) + dk₁₁₁ = Vector{R}(undef, n_kron3) + + # Shock-direction vectors: scaled node shocks, basis shock i, and zero shocks. + ε̄ₜ = zeros(R, nE) + εᵢₜ = zeros(R, nE) + ε₀ = zeros(R, nE) + + # --- Pass 1: V(∅) trajectory (zero shocks) → store in decomposition[:, nE+1, :]. --- + s₁ .= initial_state[1] + s₂ .= initial_state[2] + s₃ .= initial_state[3] + if !isnothing(warmup_shocks) + for _ in axes(warmup_shocks, 2) + pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε₀, ε₀, + a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + s₃, s₃⁺ = s₃⁺, s₃ + end + end + # Propagate the baseline path with all shocks set to zero. + for t in 1:nₜ + pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε₀, ε₀, + a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) + @inbounds for v in 1:nᵥ + decomposition[v, nE + 1, t] = s₁⁺[v] + s₂⁺[v] + s₃⁺[v] + end + # Swap current and next buffers instead of allocating a fresh state. + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + s₃, s₃⁺ = s₃⁺, s₃ + end + + # --- Pass 2: one node at a time, accumulate weighted tangents. --- + @views fill!(decomposition[:, 1:nE, :], zero(R)) + + # Quadrature over shock scaling nodes; each node contributes one weighted path. + for k in 1:n_nodes + sₖ = nodes[k] + wₖ = weights[k] + s₁ .= initial_state[1] + s₂ .= initial_state[2] + s₃ .= initial_state[3] + # Reset the tangent trajectories for this quadrature node. + for i in 1:nE + fill!(ds₁ᵢ[i], 0.0) + fill!(ds₂ᵢ[i], 0.0) + fill!(ds₃ᵢ[i], 0.0) + end + if !isnothing(warmup_shocks) + advance_aumann_shapley_pruned_3rd_warmup!(s₁, s₂, s₃, s₁⁺, s₂⁺, s₃⁺, + ds₁ᵢ, ds₂ᵢ, ds₃ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, ds₃ᵢ⁺, + warmup_shocks, + sₖ, + iₚ, + a₁, a₁⁰, a₂, a₃, da₁, da₂, da₃, + k₁₁, k₁₂⁰, k₁₁₁, dk₁₁, dk₁₂⁰, dk₁₁₁, + k₂tmp, + ε̄ₜ, εᵢₜ, ε₀, + 𝐒) + end + + # March forward one period at a time, updating the primal path and all tangents. + for t in 1:nₜ + εₜ = @view shocks[:, t] + ε̄ₜ .= sₖ .* εₜ + + pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε̄ₜ, ε₀, + a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) + + # For each shock direction i, propagate first/second/third-order + # tangents and accumulate the node-weighted contribution. + for i in 1:nE + fill!(εᵢₜ, 0.0) + εᵢₜ[i] = εₜ[i] + + @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) + da₁[nₚ + 1] = 0.0 + copyto!(da₁, nₚ + 2, εᵢₜ, 1, nE) + + @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) + da₂[nₚ + 1] = 0.0 + copyto!(da₂, nₚ + 2, ε₀, 1, nE) + + @views copyto!(da₃[1:nₚ], ds₃ᵢ[i][iₚ]) + da₃[nₚ + 1] = 0.0 + copyto!(da₃, nₚ + 2, ε₀, 1, nE) + + # C₂ is symmetric, so d C₂(a₁, a₁) = 2 C₂(da₁, a₁). + compressed_kron²!(dk₁₁, da₁, a₁) + + # d(aug1_no_const ⊗ aug2) = (d aug1 ⊗ aug2) + (aug1_no_const ⊗ d aug2) + compressed_kron²!(dk₁₂⁰, da₁, a₂) + compressed_kron²!(k₂tmp, a₁⁰, da₂) + dk₁₂⁰ .+= k₂tmp + + # C₃ is symmetric, so d C₃(a₁, a₁, a₁) = 3 C₃(da₁, a₁, a₁). + compressed_kron³!(dk₁₁₁, da₁, a₁, a₁) + + # Plain form: ds₁ᵢ⁺ = S1 * da₁ + ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) + # Plain form: ds₂ᵢ⁺ = S1 * da₂ + 0.5 * S2 * d(a₁⊗a₁) + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) + # The outer 1/2 cancels the pair derivative's factor of 2. + ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 1.0, 1.0) + # Plain form: ds₃ᵢ⁺ = S1 * da₃ + S2 * d(a₁⁰⊗a₂) + (1/6) * S3 * d(a₁⊗a₁⊗a₁) + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[1], da₃) + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[2], dk₁₂⁰, 1.0, 1.0) + # The outer 1/6 times the cubic derivative's factor of 3 is 1/2. + ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[3], dk₁₁₁, 1/2, 1.0) + + @inbounds for v in 1:nᵥ + decomposition[v, i, t] += wₖ * ( + ds₁ᵢ⁺[i][v] + + ds₂ᵢ⁺[i][v] + + ds₃ᵢ⁺[i][v] + ) + end + end + + # Advance the primal and tangent buffers to the next period. + s₁, s₁⁺ = s₁⁺, s₁ + s₂, s₂⁺ = s₂⁺, s₂ + s₃, s₃⁺ = s₃⁺, s₃ + for i in 1:nE + ds₁ᵢ[i], ds₁ᵢ⁺[i] = ds₁ᵢ⁺[i], ds₁ᵢ[i] + ds₂ᵢ[i], ds₂ᵢ⁺[i] = ds₂ᵢ⁺[i], ds₂ᵢ[i] + ds₃ᵢ[i], ds₃ᵢ⁺[i] = ds₃ᵢ⁺[i], ds₃ᵢ[i] + end + end + end + max_residual = zero(R) + max_reference = zero(R) + @inbounds for t in 1:nₜ, v in 1:nᵥ + ϕsum = zero(R) + for i in 1:nE + ϕsum += decomposition[v, i, t] + end + residual = variables[v, t] - (decomposition[v, nE + 1, t] + ϕsum) + max_residual = max(max_residual, abs(residual)) + max_reference = max(max_reference, abs(variables[v, t])) + end + + T = float(R) + scale = max(T(max_reference), sqrt(eps(T))) + return T(max_residual) / scale +end + +end # @stable diff --git a/src/filter/find_shocks.jl b/src/filter/find_shocks.jl index acccbc7cf..35222b556 100644 --- a/src/filter/find_shocks.jl +++ b/src/filter/find_shocks.jl @@ -44,22 +44,16 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, ensure_conditional_forecast_constants!(constants; third_order = third_order) - shock_idxs = so.shock_idxs - shock²_idxs = so.shock²_idxs - shockvar²_idxs = so.shockvar²_idxs - var_vol²_idxs = so.var_vol²_idxs - var²_idxs = so.var²_idxs - shockvar_idxs = sparse(ℒ.kron(so.e_in_s⁺, so.s_in_s)).nzind - - var_vol³_idxs = to.var_vol³_idxs - shock_idxs2 = to.shock_idxs2 - shock_idxs3 = to.shock_idxs3 - shock³_idxs = to.shock³_idxs - shockvar1_idxs = to.shockvar1_idxs - shockvar2_idxs = to.shockvar2_idxs - shockvar3_idxs = to.shockvar3_idxs - shockvar³2_idxs = to.shockvar³2_idxs - shockvar³_idxs = to.shockvar³_idxs + shockvar_no_vol_cols = so.shockvar_no_vol_cols + shockvar²_cols = so.shockvar²_cols + shock²_cols = so.shock²_cols + var_vol²_cols = so.var_vol²_cols + var²_cols = so.var²_cols + + var_vol³_cols = to.var_vol³_cols + shockvar³2_cols = to.shockvar³2_cols + shockvar³_cols = to.shockvar³_cols + shock³_cols = to.shock³_cols fixed_shock_idx = setdiff(1:n_exo, free_shock_idx) @@ -70,6 +64,7 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, J = ℒ.I(n_exo) nPast = T.nPast_not_future_and_mixed + n_global = nPast + 1 + n_exo third_order_pruning = third_order && pruning ensure_find_shocks_state_buffers!(ws, n_exo, nPast; third_order = third_order, @@ -77,6 +72,8 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, kron_state_vol = ws.kron_state_vol kron_I_state = ws.kron_I_state + n_exo² = n_exo * (n_exo + 1) ÷ 2 + if isnothing(𝐒₃) # Second order (pruned or non-pruned) if pruning @@ -94,13 +91,13 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, if isnothing(𝐒₂) 𝐒ⁱ = copy(𝐒¹ᵉ) - 𝐒ⁱ²ᵉ = zeros(size(𝐒¹ᵉ, 1), n_exo^2) + 𝐒ⁱ²ᵉ = zeros(size(𝐒¹ᵉ, 1), n_exo²) else - 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_idxs] - 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_cols] + 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_cols] + 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_cols] - ℒ.kron!(kron_state_vol, state_vol, state_vol) + compressed_kron²_power!(kron_state_vol, state_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kron_state_vol, -1/2, 1) ℒ.kron!(kron_I_state, J, state_vol) @@ -119,13 +116,13 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, if isnothing(𝐒₂) 𝐒ⁱ = copy(𝐒¹ᵉ) - 𝐒ⁱ²ᵉ = zeros(size(𝐒¹ᵉ, 1), n_exo^2) + 𝐒ⁱ²ᵉ = zeros(size(𝐒¹ᵉ, 1), n_exo²) else - 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_idxs] - 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_cols] + 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_cols] + 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_cols] - ℒ.kron!(kron_state_vol, state_vol, state_vol) + compressed_kron²_power!(kron_state_vol, state_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kron_state_vol, -1/2, 1) ℒ.kron!(kron_I_state, J, state_vol) @@ -137,8 +134,6 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, 𝐒ⁱ³ᵉ = nothing else # third_order # Third order (pruned or non-pruned) - II = sparse(ℒ.I(n_exo^2)) - if pruning state₁ = initial_state[1][T.past_not_future_and_mixed_idx] state₂ = initial_state[2][T.past_not_future_and_mixed_idx] @@ -149,31 +144,31 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, 𝐒¹⁻ᵛ = @views 𝐒₁[cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = @views 𝐒₁[cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_idxs] - 𝐒²⁻ = @views 𝐒₂[cond_var_idx, var²_idxs] - 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_idxs] - 𝐒²⁻ᵛᵉ = @views 𝐒₂[cond_var_idx, shockvar_idxs] - 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_cols] + 𝐒²⁻ = @views 𝐒₂[cond_var_idx, var²_cols] + 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_cols] + 𝐒²⁻ᵛᵉ = @views 𝐒₂[cond_var_idx, shockvar_no_vol_cols] + 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_cols] - 𝐒³⁻ᵛ = @views 𝐒₃[cond_var_idx, var_vol³_idxs] - 𝐒³⁻ᵉ² = @views 𝐒₃[cond_var_idx, shockvar³2_idxs] - 𝐒³⁻ᵉ = @views 𝐒₃[cond_var_idx, shockvar³_idxs] - 𝐒³ᵉ = @views 𝐒₃[cond_var_idx, shock³_idxs] + 𝐒³⁻ᵛ = @views 𝐒₃[cond_var_idx, var_vol³_cols] + 𝐒³⁻ᵉ² = @views 𝐒₃[cond_var_idx, shockvar³2_cols] + 𝐒³⁻ᵉ = @views 𝐒₃[cond_var_idx, shockvar³_cols] + 𝐒³ᵉ = @views 𝐒₃[cond_var_idx, shock³_cols] shock_independent = copy(conditions) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state_vol, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₃, -1, 1) - ℒ.kron!(kron_state_vol, state_vol, state_vol) + compressed_kron²_power!(kron_state_vol, state_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kron_state_vol, -1/2, 1) kron_state₁₂ = ws.kron_state₁₂ - ℒ.kron!(kron_state₁₂, state₁, state₂) + compressed_kron²!(kron_state₁₂, state₁, state₂) ℒ.mul!(shock_independent, 𝐒²⁻, kron_state₁₂, -1, 1) kron_state_vol3 = ws.kron_state_vol3 - ℒ.kron!(kron_state_vol3, state_vol, kron_state_vol) + compressed_kron³_power!(kron_state_vol3, state_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kron_state_vol3, -1/6, 1) ℒ.kron!(kron_I_state, J, state_vol) @@ -187,7 +182,12 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, 𝐒²⁻ᵛᵉ * kron_I_state₂ + 𝐒³⁻ᵉ² * kron_I_state_state / 2 - 𝐒ⁱ²ᵉ = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * ℒ.kron(II, state_vol) / 2 + 𝐒³⁻ᵉ_state = compressed_triple_state_to_pair(state_vol, + n_global, + nPast + 1, + n_exo, + shockvar³_cols) + 𝐒ⁱ²ᵉ = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * 𝐒³⁻ᵉ_state 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 else state = initial_state[T.past_not_future_and_mixed_idx] @@ -196,23 +196,23 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, 𝐒¹⁻ᵛ = @views 𝐒₁[cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = @views 𝐒₁[cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_idxs] - 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = @views 𝐒₂[cond_var_idx, var_vol²_cols] + 𝐒²⁻ᵉ = @views 𝐒₂[cond_var_idx, shockvar²_cols] + 𝐒²ᵉ = @views 𝐒₂[cond_var_idx, shock²_cols] - 𝐒³⁻ᵛ = @views 𝐒₃[cond_var_idx, var_vol³_idxs] - 𝐒³⁻ᵉ² = @views 𝐒₃[cond_var_idx, shockvar³2_idxs] - 𝐒³⁻ᵉ = @views 𝐒₃[cond_var_idx, shockvar³_idxs] - 𝐒³ᵉ = @views 𝐒₃[cond_var_idx, shock³_idxs] + 𝐒³⁻ᵛ = @views 𝐒₃[cond_var_idx, var_vol³_cols] + 𝐒³⁻ᵉ² = @views 𝐒₃[cond_var_idx, shockvar³2_cols] + 𝐒³⁻ᵉ = @views 𝐒₃[cond_var_idx, shockvar³_cols] + 𝐒³ᵉ = @views 𝐒₃[cond_var_idx, shock³_cols] shock_independent = copy(conditions) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state_vol, -1, 1) - ℒ.kron!(kron_state_vol, state_vol, state_vol) + compressed_kron²_power!(kron_state_vol, state_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kron_state_vol, -1/2, 1) kron_state_vol3 = ws.kron_state_vol3 - ℒ.kron!(kron_state_vol3, state_vol, kron_state_vol) + compressed_kron³_power!(kron_state_vol3, state_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kron_state_vol3, -1/6, 1) ℒ.kron!(kron_I_state, J, state_vol) @@ -223,7 +223,12 @@ function find_shocks_conditional_forecast(::Val{:LagrangeNewton}, 𝐒²⁻ᵉ * kron_I_state + 𝐒³⁻ᵉ² * kron_I_state_state / 2 - 𝐒ⁱ²ᵉ = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * ℒ.kron(II, state_vol) / 2 + 𝐒³⁻ᵉ_state = compressed_triple_state_to_pair(state_vol, + n_global, + nPast + 1, + n_exo, + shockvar³_cols) + 𝐒ⁱ²ᵉ = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * 𝐒³⁻ᵉ_state 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 end end @@ -930,14 +935,16 @@ function find_shocks(::Val{:LagrangeNewton}, fxλp = zeros(R, length(xλ), length(xλ)) - tmp = zeros(R, size(𝐒ⁱ, 2) * size(𝐒ⁱ, 2)) + n_shock = size(𝐒ⁱ, 2) + tmp = zeros(R, n_shock, n_shock) + tmp_coeff = zeros(R, length(kron_buffer)) - lI = R(-2) * vec(ℒ.I(size(𝐒ⁱ, 2))) + lI = R(-2) * Matrix(ℒ.I(n_shock)) iter = 0 @inbounds for i in 1:max_iter iter = i - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(∂x, 𝐒ⁱ²ᵉ, kron_buffer2) ℒ.axpby!(1, 𝐒ⁱ, 2, ∂x) @@ -952,7 +959,8 @@ function find_shocks(::Val{:LagrangeNewton}, # fXλ = [(𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(length(x)), x))' * λ - 2 * x # shock_independent - (𝐒ⁱ * x + 𝐒ⁱ²ᵉ * ℒ.kron(x,x))] - ℒ.mul!(tmp, 𝐒ⁱ²ᵉ', λ) + ℒ.mul!(tmp_coeff, 𝐒ⁱ²ᵉ', λ) + compressed_pair_hessian!(tmp, tmp_coeff) ℒ.axpby!(1, lI, 2, tmp) fxλp[1:size(𝐒ⁱ, 2), 1:size(𝐒ⁱ, 2)] = tmp @@ -989,7 +997,7 @@ function find_shocks(::Val{:LagrangeNewton}, # λ = xλ[size(𝐒ⁱ, 2)+1:end] copyto!(λ, 1, xλ, size(𝐒ⁱ,2) + 1, length(λ)) - ℒ.kron!(kron_buffer, x, x) + compressed_kron²_power!(kron_buffer, x) ℒ.mul!(x̂, 𝐒ⁱ²ᵉ, kron_buffer) @@ -1054,21 +1062,20 @@ function find_shocks(::Val{:LagrangeNewton}, fxλp = zeros(R, length(xλ), length(xλ)) - tmp = zeros(R, size(𝐒ⁱ, 2) * size(𝐒ⁱ, 2)) - - tmp2 = zeros(R, size(𝐒ⁱ, 1),size(𝐒ⁱ, 2) * size(𝐒ⁱ, 2)) - - II = sparse(ℒ.I(length(x)^2)) + n_shock = size(𝐒ⁱ, 2) + tmp = zeros(R, n_shock, n_shock) + tmp_coeff = zeros(R, length(kron_buffer)) + tmp3_coeff = zeros(R, size(𝐒ⁱ³ᵉ, 2)) - lI = R(-2) * vec(ℒ.I(size(𝐒ⁱ, 2))) + lI = R(-2) * Matrix(ℒ.I(n_shock)) iter = 0 @inbounds for i in 1:max_iter iter = i # Initialize x ⊗ x for the current iterate before using kron_buffer in Jacobian terms. - ℒ.kron!(kron_buffer, x, x) - ℒ.kron!(kron_buffer2, J, x) - ℒ.kron!(kron_buffer3, J, kron_buffer) + compressed_kron²_power!(kron_buffer, x) + compressed_kron²!(kron_buffer2, x, J) + compressed_kron³!(kron_buffer3, x, x, J) copy!(∂x, 𝐒ⁱ) ℒ.mul!(∂x, 𝐒ⁱ²ᵉ, kron_buffer2, 2, 1) @@ -1084,12 +1091,13 @@ function find_shocks(::Val{:LagrangeNewton}, # fXλ = [(𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(length(x)), x) + 3 * 𝐒ⁱ³ᵉ * ℒ.kron(ℒ.I(length(x)), ℒ.kron(x, x)))' * λ - 2 * x # shock_independent - (𝐒ⁱ * x + 𝐒ⁱ²ᵉ * ℒ.kron(x,x) + 𝐒ⁱ³ᵉ * ℒ.kron(x, ℒ.kron(x, x)))] - x_kron_II!(kron_buffer4, x) - # ℒ.kron!(kron_buffer4, II, x) - ℒ.mul!(tmp2, 𝐒ⁱ³ᵉ, kron_buffer4) - ℒ.mul!(tmp, tmp2', λ) - ℒ.mul!(tmp, 𝐒ⁱ²ᵉ', λ, 2, 6) - ℒ.axpy!(1,lI,tmp) + # ∇²[coeff'·compressed_kron²_power(x)] is 2·compressed_pair_hessian!(coeff), + # so the pair coefficients carry the factor 2 (as in the 2nd-order method). + ℒ.mul!(tmp_coeff, 𝐒ⁱ²ᵉ', λ, 2, 0) + compressed_pair_hessian!(tmp, tmp_coeff) + ℒ.mul!(tmp3_coeff, 𝐒ⁱ³ᵉ', λ, 6, 0) + compressed_triple_hessian!(tmp, tmp3_coeff, x) + ℒ.axpy!(1, lI, tmp) fxλp[1:size(𝐒ⁱ, 2), 1:size(𝐒ⁱ, 2)] = tmp @@ -1119,9 +1127,9 @@ function find_shocks(::Val{:LagrangeNewton}, # λ = xλ[size(𝐒ⁱ, 2)+1:end] copyto!(λ, 1, xλ, size(𝐒ⁱ,2) + 1, length(λ)) - ℒ.kron!(kron_buffer, x, x) + compressed_kron²_power!(kron_buffer, x) - ℒ.kron!(kron_buffer², x, kron_buffer) + compressed_kron³_power!(kron_buffer², x) ℒ.mul!(x̂, 𝐒ⁱ, x) diff --git a/src/filter/inversion.jl b/src/filter/inversion.jl index 346a98d3d..49eda1711 100644 --- a/src/filter/inversion.jl +++ b/src/filter/inversion.jl @@ -1,653 +1,5 @@ @stable default_mode = "disable" begin - -# --------------------------------------------------------------------- -# Aumann–Shapley shock decomposition (marginal-contribution driver) -# --------------------------------------------------------------------- -# -# Computes per-period Shapley shares for the inversion-filter shock -# decomposition under pruned 2nd / 3rd order solutions via the path- -# integral identity -# φᵢ(v, t) = ∫₀¹ ∂Ṽ_t(s·𝟙)/∂xᵢ ds -# ≈ Σ_k w_k · ∂Ṽ_t(s_k·𝟙)/∂xᵢ (Gauss–Legendre) -# where Ṽ_t is the polynomial extension of `S → ŝ_t(S)[v]`. The production -# drivers start from the low-order Gauss–Legendre rules (2 nodes at 2nd -# order, 3 at 3rd order) and rerun with 4 nodes only when the coarse -# Shapley-efficiency closure residual exceeds `1e-3`. -# -# Per period the driver maintains, for every Gauss–Legendre node s_k: -# - one primal pruned-state trajectory under shocks scaled by s_k; -# - one tangent trajectory per shock direction i = 1..nᵉ giving -# ∂ŝ_t/∂xᵢ at x = s_k·𝟙. -# Each tangent recursion mirrors the primal recursion with derivatives -# threaded by the chain rule; the shock contribution to the tangent's -# augmented vector picks up `εᵢ_t · eᵢ` because ∂(xᵢ·εᵢ_t)/∂xᵢ = εᵢ_t. -# A separate s = 0 primal trajectory is propagated to obtain V(∅). -# -# Each function fills `decomposition[:, 1:nᵉ, :]` with per-shock Aumann– -# Shapley shares of the incremental response `V(N) − V(∅)`. By linearity, -# these columns equal each shock's standalone effect plus its allocated share -# of the cross-shock interaction. The `decomposition[:, nᵉ+1, :]` column keeps -# the zero-shock / initial-values path `V(∅)` plus any tiny numerical closure -# residual, matching the layout the public API expects when -# `marginal_contribution = true`. - -# Pruned 2nd-order state update: new_s1, new_s2 = 𝐒₁·aug1, 𝐒₁·aug2 + ½𝐒₂·(aug1⊗aug1). -# Builds augmented vectors via copyto!, computes kron product, and applies solution matrices in-place. -function pruned_state_update_2nd_order!( - new_s1, new_s2, s1, s2, past_idx, shock_dir, zero_dir, - aug1, aug2, kk, 𝐒) - n_past = length(past_idx) - @views copyto!(aug1[1:n_past], s1[past_idx]) - aug1[n_past + 1] = 1.0 - copyto!(aug1, n_past + 2, shock_dir, 1, length(shock_dir)) - @views copyto!(aug2[1:n_past], s2[past_idx]) - aug2[n_past + 1] = 0.0 - copyto!(aug2, n_past + 2, zero_dir, 1, length(zero_dir)) - ℒ.kron!(kk, aug1, aug1) - ℒ.mul!(new_s1, 𝐒[1], aug1) - ℒ.mul!(new_s2, 𝐒[1], aug2) - ℒ.mul!(new_s2, 𝐒[2], kk, 0.5, 1.0) - return nothing -end - -# Pruned 3rd-order state update: extends 2nd-order with new_s3 = 𝐒₁·aug3 + 𝐒₂·(aug1̂⊗aug2) + ⅙𝐒₃·(aug1⊗aug1⊗aug1). -# aug1̂ is the no-constant variant of aug1 (constant slot = 0). -function pruned_state_update_3rd_order!( - new_s1, new_s2, new_s3, s1, s2, s3, past_idx, shock_dir, zero_dir, - aug1, aug1̂, aug2, aug3, k11, k12̂, k111, 𝐒) - n_past = length(past_idx) - @views copyto!(aug1[1:n_past], s1[past_idx]) - aug1[n_past + 1] = 1.0 - copyto!(aug1, n_past + 2, shock_dir, 1, length(shock_dir)) - @views copyto!(aug1̂[1:n_past], s1[past_idx]) - aug1̂[n_past + 1] = 0.0 - copyto!(aug1̂, n_past + 2, shock_dir, 1, length(shock_dir)) - @views copyto!(aug2[1:n_past], s2[past_idx]) - aug2[n_past + 1] = 0.0 - copyto!(aug2, n_past + 2, zero_dir, 1, length(zero_dir)) - @views copyto!(aug3[1:n_past], s3[past_idx]) - aug3[n_past + 1] = 0.0 - copyto!(aug3, n_past + 2, zero_dir, 1, length(zero_dir)) - ℒ.kron!(k11, aug1, aug1) - ℒ.kron!(k12̂, aug1̂, aug2) - ℒ.kron!(k111, k11, aug1) - ℒ.mul!(new_s1, 𝐒[1], aug1) - ℒ.mul!(new_s2, 𝐒[1], aug2) - ℒ.mul!(new_s2, 𝐒[2], k11, 0.5, 1.0) - ℒ.mul!(new_s3, 𝐒[1], aug3) - ℒ.mul!(new_s3, 𝐒[2], k12̂, 1.0, 1.0) - ℒ.mul!(new_s3, 𝐒[3], k111, 1/6, 1.0) - return nothing -end - -function advance_aumann_shapley_pruned_2nd_warmup!( - s₁, s₂, s₁⁺, s₂⁺, - ds₁ᵢ, ds₂ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, - warmup_shocks::AbstractMatrix, - sₖ, - iₚ, - a₁, a₂, da₁, da₂, - k₁₁, dk₁₁, dk₁₁′, - ε̄ₜ, εᵢₜ, ε₀, - 𝐒) - nₚ = length(iₚ) - - for w in axes(warmup_shocks, 2) - εₜ = @view warmup_shocks[:, w] - ε̄ₜ .= sₖ .* εₜ - pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε̄ₜ, ε₀, a₁, a₂, k₁₁, 𝐒) - - for i in eachindex(ds₁ᵢ) - fill!(εᵢₜ, 0.0) - εᵢₜ[i] = εₜ[i] - - @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) - da₁[nₚ + 1] = 0.0 - copyto!(da₁, nₚ + 2, εᵢₜ, 1, size(warmup_shocks, 1)) - - @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) - da₂[nₚ + 1] = 0.0 - copyto!(da₂, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) - - ℒ.kron!(dk₁₁, da₁, a₁) - ℒ.kron!(dk₁₁′, a₁, da₁) - dk₁₁ .+= dk₁₁′ - - ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 0.5, 1.0) - - copyto!(ds₁ᵢ[i], ds₁ᵢ⁺[i]) - copyto!(ds₂ᵢ[i], ds₂ᵢ⁺[i]) - end - - copyto!(s₁, s₁⁺) - copyto!(s₂, s₂⁺) - end - - return nothing -end - -function advance_aumann_shapley_pruned_3rd_warmup!( - s₁, s₂, s₃, s₁⁺, s₂⁺, s₃⁺, - ds₁ᵢ, ds₂ᵢ, ds₃ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, ds₃ᵢ⁺, - warmup_shocks::AbstractMatrix, - sₖ, - iₚ, - a₁, a₁⁰, a₂, a₃, da₁, da₂, da₃, - k₁₁, k₁₂⁰, k₁₁₁, dk₁₁, dk₁₂⁰, dk₁₁₁, - k₂tmp, k₃tmp, - ε̄ₜ, εᵢₜ, ε₀, - 𝐒) - nₚ = length(iₚ) - - for w in axes(warmup_shocks, 2) - εₜ = @view warmup_shocks[:, w] - ε̄ₜ .= sₖ .* εₜ - pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε̄ₜ, ε₀, - a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) - - for i in eachindex(ds₁ᵢ) - fill!(εᵢₜ, 0.0) - εᵢₜ[i] = εₜ[i] - - @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) - da₁[nₚ + 1] = 0.0 - copyto!(da₁, nₚ + 2, εᵢₜ, 1, size(warmup_shocks, 1)) - - @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) - da₂[nₚ + 1] = 0.0 - copyto!(da₂, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) - - @views copyto!(da₃[1:nₚ], ds₃ᵢ[i][iₚ]) - da₃[nₚ + 1] = 0.0 - copyto!(da₃, nₚ + 2, ε₀, 1, size(warmup_shocks, 1)) - - ℒ.kron!(dk₁₁, da₁, a₁) - ℒ.kron!(k₂tmp, a₁, da₁) - dk₁₁ .+= k₂tmp - - ℒ.kron!(dk₁₂⁰, da₁, a₂) - ℒ.kron!(k₂tmp, a₁⁰, da₂) - dk₁₂⁰ .+= k₂tmp - - ℒ.kron!(dk₁₁₁, dk₁₁, a₁) - ℒ.kron!(k₃tmp, k₁₁, da₁) - dk₁₁₁ .+= k₃tmp - - ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 0.5, 1.0) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[1], da₃) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[2], dk₁₂⁰, 1.0, 1.0) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[3], dk₁₁₁, 1/6, 1.0) - - copyto!(ds₁ᵢ[i], ds₁ᵢ⁺[i]) - copyto!(ds₂ᵢ[i], ds₂ᵢ⁺[i]) - copyto!(ds₃ᵢ[i], ds₃ᵢ⁺[i]) - end - - copyto!(s₁, s₁⁺) - copyto!(s₂, s₂⁺) - copyto!(s₃, s₃⁺) - end - - return nothing -end - -function aumann_shapley_shock_decomposition_pruned_2nd_order!( - decomposition::AbstractArray{R}, - variables::AbstractMatrix, - shocks::AbstractMatrix, - initial_state, - 𝐒, - T, - nE::Int; - verbose::Bool = false, - warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real - n_nodes = 2 - max_error = aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, - variables, - shocks, - initial_state, - 𝐒, - T, - nE, - n_nodes; - warmup_shocks = warmup_shocks) - if verbose - println("Aumann-Shapley second-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) - end - while max_error > AUMANN_SHAPLEY_REFINEMENT_RTOL && n_nodes < AUMANN_SHAPLEY_REFINEMENT_MAX_NODES - next_nodes = min(n_nodes + 1, AUMANN_SHAPLEY_REFINEMENT_MAX_NODES) - if verbose - println("Aumann-Shapley second-order shock decomposition rerunning with ", next_nodes, " nodes after closure error ", max_error, " at ", n_nodes, " nodes") - end - n_nodes = next_nodes - max_error = aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, - variables, - shocks, - initial_state, - 𝐒, - T, - nE, - n_nodes; - warmup_shocks = warmup_shocks) - if verbose - println("Aumann-Shapley second-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) - end - end - return decomposition -end - -function aumann_shapley_shock_decomposition_pruned_2nd_order!( - decomposition::AbstractArray{R}, - variables::AbstractMatrix, - shocks::AbstractMatrix, - initial_state, - 𝐒, - T, - nE::Int, - n_nodes::Int; - warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real - nᵥ = T.nVars - iₚ = T.past_not_future_and_mixed_idx - nₚ = length(iₚ) - n_aug = nₚ + 1 + nE - n_kron = n_aug^2 - nₜ = size(decomposition, 3) - - nodes, weights = gausslegendre_unit_interval(n_nodes) - - # Scratch buffers — one set reused sequentially across quadrature nodes. - # s₁/s₂ are primal pruned state components; s₁⁺/s₂⁺ are next-period outputs. - s₁ = zeros(R, nᵥ) - s₂ = zeros(R, nᵥ) - s₁⁺ = zeros(R, nᵥ) - s₂⁺ = zeros(R, nᵥ) - # ds* buffers hold tangent states ∂s/∂xᵢ for each shock direction i. - ds₁ᵢ = [zeros(R, nᵥ) for _ in 1:nE] - ds₂ᵢ = [zeros(R, nᵥ) for _ in 1:nE] - ds₁ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] - ds₂ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] - - # Augmented primal/tangent vectors [past state; constant; shocks]. - a₁ = Vector{R}(undef, n_aug) - a₂ = Vector{R}(undef, n_aug) - da₁ = Vector{R}(undef, n_aug) - da₂ = Vector{R}(undef, n_aug) - # Kronecker workspaces for a₁⊗a₁ and its directional derivative. - k₁₁ = Vector{R}(undef, n_kron) - dk₁₁ = Vector{R}(undef, n_kron) - dk₁₁′ = Vector{R}(undef, n_kron) - - # Shock-direction vectors: scaled node shocks, basis shock i, and zero shocks. - ε̄ₜ = zeros(R, nE) - εᵢₜ = zeros(R, nE) - ε₀ = zeros(R, nE) - - # --- Pass 1: V(∅) trajectory (zero shocks) → store in decomposition[:, nE+1, :]. --- - s₁ .= initial_state[1] - s₂ .= initial_state[2] - if !isnothing(warmup_shocks) - for _ in axes(warmup_shocks, 2) - pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε₀, ε₀, a₁, a₂, k₁₁, 𝐒) - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - end - end - # Propagate the baseline path with all shocks set to zero. - for t in 1:nₜ - pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε₀, ε₀, a₁, a₂, k₁₁, 𝐒) - @inbounds for v in 1:nᵥ - decomposition[v, nE + 1, t] = s₁⁺[v] + s₂⁺[v] - end - # Swap current and next buffers instead of allocating a fresh state. - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - end - - # --- Pass 2: one node at a time, accumulate weighted tangents. --- - @views fill!(decomposition[:, 1:nE, :], zero(R)) - - # Quadrature over shock scaling nodes; each node contributes one weighted path. - for k in 1:n_nodes - sₖ = nodes[k] - wₖ = weights[k] - s₁ .= initial_state[1] - s₂ .= initial_state[2] - # Reset the tangent trajectories for this quadrature node. - for i in 1:nE - fill!(ds₁ᵢ[i], 0.0) - fill!(ds₂ᵢ[i], 0.0) - end - if !isnothing(warmup_shocks) - advance_aumann_shapley_pruned_2nd_warmup!(s₁, s₂, s₁⁺, s₂⁺, - ds₁ᵢ, ds₂ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, - warmup_shocks, - sₖ, - iₚ, - a₁, a₂, da₁, da₂, - k₁₁, dk₁₁, dk₁₁′, - ε̄ₜ, εᵢₜ, ε₀, - 𝐒) - end - - # March forward one period at a time, updating the primal path and all tangents. - for t in 1:nₜ - εₜ = @view shocks[:, t] - ε̄ₜ .= sₖ .* εₜ - pruned_state_update_2nd_order!(s₁⁺, s₂⁺, s₁, s₂, iₚ, ε̄ₜ, ε₀, a₁, a₂, k₁₁, 𝐒) - - # For each shock direction i, propagate tangent recursions and - # accumulate node-weighted directional derivatives. - for i in 1:nE - fill!(εᵢₜ, 0.0) - εᵢₜ[i] = εₜ[i] - - @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) - da₁[nₚ + 1] = 0.0 - copyto!(da₁, nₚ + 2, εᵢₜ, 1, nE) - - @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) - da₂[nₚ + 1] = 0.0 - copyto!(da₂, nₚ + 2, ε₀, 1, nE) - - # d(aug1 ⊗ aug1) = (d aug1 ⊗ aug1) + (aug1 ⊗ d aug1) - ℒ.kron!(dk₁₁, da₁, a₁) - ℒ.kron!(dk₁₁′, a₁, da₁) - dk₁₁ .+= dk₁₁′ - - # Plain form: ds₁ᵢ⁺ = S1 * da₁ - ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) - # Plain form: ds₂ᵢ⁺ = S1 * da₂ + 0.5 * S2 * d(a₁⊗a₁) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 0.5, 1.0) - - @inbounds for v in 1:nᵥ - decomposition[v, i, t] += wₖ * (ds₁ᵢ⁺[i][v] + ds₂ᵢ⁺[i][v]) - end - end - - # Advance the primal and tangent buffers to the next period. - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - for i in 1:nE - ds₁ᵢ[i], ds₁ᵢ⁺[i] = ds₁ᵢ⁺[i], ds₁ᵢ[i] - ds₂ᵢ[i], ds₂ᵢ⁺[i] = ds₂ᵢ⁺[i], ds₂ᵢ[i] - end - end - end - - max_residual = zero(R) - max_reference = zero(R) - @inbounds for t in 1:nₜ, v in 1:nᵥ - ϕsum = zero(R) - for i in 1:nE - ϕsum += decomposition[v, i, t] - end - residual = variables[v, t] - (decomposition[v, nE + 1, t] + ϕsum) - max_residual = max(max_residual, abs(residual)) - max_reference = max(max_reference, abs(variables[v, t])) - end - - T = float(R) - scale = max(T(max_reference), sqrt(eps(T))) - return T(max_residual) / scale -end - - -function aumann_shapley_shock_decomposition_pruned_3rd_order!( - decomposition::AbstractArray{R}, - variables::AbstractMatrix, - shocks::AbstractMatrix, - initial_state, - 𝐒, - T, - nE::Int; - verbose::Bool = false, - warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real - n_nodes = 3 - max_error = aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, - variables, - shocks, - initial_state, - 𝐒, - T, - nE, - n_nodes; - warmup_shocks = warmup_shocks) - if verbose - println("Aumann-Shapley third-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) - end - while max_error > AUMANN_SHAPLEY_REFINEMENT_RTOL && n_nodes < AUMANN_SHAPLEY_REFINEMENT_MAX_NODES - next_nodes = min(n_nodes + 1, AUMANN_SHAPLEY_REFINEMENT_MAX_NODES) - if verbose - println("Aumann-Shapley third-order shock decomposition rerunning with ", next_nodes, " nodes after closure error ", max_error, " at ", n_nodes, " nodes") - end - n_nodes = next_nodes - max_error = aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, - variables, - shocks, - initial_state, - 𝐒, - T, - nE, - n_nodes; - warmup_shocks = warmup_shocks) - if verbose - println("Aumann-Shapley third-order shock decomposition closure error with ", n_nodes, " nodes: ", max_error) - end - end - return decomposition -end - -function aumann_shapley_shock_decomposition_pruned_3rd_order!( - decomposition::AbstractArray{R}, - variables::AbstractMatrix, - shocks::AbstractMatrix, - initial_state, - 𝐒, - T, - nE::Int, - n_nodes::Int; - warmup_shocks::Union{Nothing,AbstractMatrix} = nothing) where R <: Real - nᵥ = T.nVars - iₚ = T.past_not_future_and_mixed_idx - nₚ = length(iₚ) - n_aug = nₚ + 1 + nE - n_kron2 = n_aug^2 - n_kron3 = n_aug^3 - nₜ = size(decomposition, 3) - - nodes, weights = gausslegendre_unit_interval(n_nodes) - - # Scratch buffers — one set reused sequentially across quadrature nodes. - # s₁/s₂/s₃ are primal pruned state components; s*⁺ are next-period outputs. - s₁ = zeros(R, nᵥ) - s₂ = zeros(R, nᵥ) - s₃ = zeros(R, nᵥ) - s₁⁺ = zeros(R, nᵥ) - s₂⁺ = zeros(R, nᵥ) - s₃⁺ = zeros(R, nᵥ) - # ds* buffers hold tangent states ∂s/∂xᵢ for each shock direction i. - ds₁ᵢ = [zeros(R, nᵥ) for _ in 1:nE] - ds₂ᵢ = [zeros(R, nᵥ) for _ in 1:nE] - ds₃ᵢ = [zeros(R, nᵥ) for _ in 1:nE] - ds₁ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] - ds₂ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] - ds₃ᵢ⁺ = [zeros(R, nᵥ) for _ in 1:nE] - - # Augmented primal/tangent vectors [past state; constant; shocks]. - # a₁⁰ is a₁ with zero constant slot for the third-order cross term. - a₁ = Vector{R}(undef, n_aug) - a₁⁰ = Vector{R}(undef, n_aug) - a₂ = Vector{R}(undef, n_aug) - a₃ = Vector{R}(undef, n_aug) - da₁ = Vector{R}(undef, n_aug) - da₂ = Vector{R}(undef, n_aug) - da₃ = Vector{R}(undef, n_aug) - - # Kronecker workspaces for primal terms and directional derivatives: - # k₁₁=a₁⊗a₁, k₁₂⁰=a₁⁰⊗a₂, k₁₁₁=(a₁⊗a₁)⊗a₁ and their d/dxᵢ variants. - k₁₁ = Vector{R}(undef, n_kron2) - k₁₂⁰ = Vector{R}(undef, n_kron2) - dk₁₁ = Vector{R}(undef, n_kron2) - dk₁₂⁰ = Vector{R}(undef, n_kron2) - k₂tmp = Vector{R}(undef, n_kron2) - k₁₁₁ = Vector{R}(undef, n_kron3) - dk₁₁₁ = Vector{R}(undef, n_kron3) - k₃tmp = Vector{R}(undef, n_kron3) - - # Shock-direction vectors: scaled node shocks, basis shock i, and zero shocks. - ε̄ₜ = zeros(R, nE) - εᵢₜ = zeros(R, nE) - ε₀ = zeros(R, nE) - - # --- Pass 1: V(∅) trajectory (zero shocks) → store in decomposition[:, nE+1, :]. --- - s₁ .= initial_state[1] - s₂ .= initial_state[2] - s₃ .= initial_state[3] - if !isnothing(warmup_shocks) - for _ in axes(warmup_shocks, 2) - pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε₀, ε₀, - a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - s₃, s₃⁺ = s₃⁺, s₃ - end - end - # Propagate the baseline path with all shocks set to zero. - for t in 1:nₜ - pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε₀, ε₀, - a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) - @inbounds for v in 1:nᵥ - decomposition[v, nE + 1, t] = s₁⁺[v] + s₂⁺[v] + s₃⁺[v] - end - # Swap current and next buffers instead of allocating a fresh state. - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - s₃, s₃⁺ = s₃⁺, s₃ - end - - # --- Pass 2: one node at a time, accumulate weighted tangents. --- - @views fill!(decomposition[:, 1:nE, :], zero(R)) - - # Quadrature over shock scaling nodes; each node contributes one weighted path. - for k in 1:n_nodes - sₖ = nodes[k] - wₖ = weights[k] - s₁ .= initial_state[1] - s₂ .= initial_state[2] - s₃ .= initial_state[3] - # Reset the tangent trajectories for this quadrature node. - for i in 1:nE - fill!(ds₁ᵢ[i], 0.0) - fill!(ds₂ᵢ[i], 0.0) - fill!(ds₃ᵢ[i], 0.0) - end - if !isnothing(warmup_shocks) - advance_aumann_shapley_pruned_3rd_warmup!(s₁, s₂, s₃, s₁⁺, s₂⁺, s₃⁺, - ds₁ᵢ, ds₂ᵢ, ds₃ᵢ, ds₁ᵢ⁺, ds₂ᵢ⁺, ds₃ᵢ⁺, - warmup_shocks, - sₖ, - iₚ, - a₁, a₁⁰, a₂, a₃, da₁, da₂, da₃, - k₁₁, k₁₂⁰, k₁₁₁, dk₁₁, dk₁₂⁰, dk₁₁₁, - k₂tmp, k₃tmp, - ε̄ₜ, εᵢₜ, ε₀, - 𝐒) - end - - # March forward one period at a time, updating the primal path and all tangents. - for t in 1:nₜ - εₜ = @view shocks[:, t] - ε̄ₜ .= sₖ .* εₜ - - pruned_state_update_3rd_order!(s₁⁺, s₂⁺, s₃⁺, s₁, s₂, s₃, iₚ, ε̄ₜ, ε₀, - a₁, a₁⁰, a₂, a₃, k₁₁, k₁₂⁰, k₁₁₁, 𝐒) - - # For each shock direction i, propagate first/second/third-order - # tangents and accumulate the node-weighted contribution. - for i in 1:nE - fill!(εᵢₜ, 0.0) - εᵢₜ[i] = εₜ[i] - - @views copyto!(da₁[1:nₚ], ds₁ᵢ[i][iₚ]) - da₁[nₚ + 1] = 0.0 - copyto!(da₁, nₚ + 2, εᵢₜ, 1, nE) - - @views copyto!(da₂[1:nₚ], ds₂ᵢ[i][iₚ]) - da₂[nₚ + 1] = 0.0 - copyto!(da₂, nₚ + 2, ε₀, 1, nE) - - @views copyto!(da₃[1:nₚ], ds₃ᵢ[i][iₚ]) - da₃[nₚ + 1] = 0.0 - copyto!(da₃, nₚ + 2, ε₀, 1, nE) - - # d(aug1 ⊗ aug1) = (d aug1 ⊗ aug1) + (aug1 ⊗ d aug1) - ℒ.kron!(dk₁₁, da₁, a₁) - ℒ.kron!(k₂tmp, a₁, da₁) - dk₁₁ .+= k₂tmp - - # d(aug1_no_const ⊗ aug2) = (d aug1 ⊗ aug2) + (aug1_no_const ⊗ d aug2) - ℒ.kron!(dk₁₂⁰, da₁, a₂) - ℒ.kron!(k₂tmp, a₁⁰, da₂) - dk₁₂⁰ .+= k₂tmp - - # d(k11 ⊗ aug1) = (d k11 ⊗ aug1) + (k11 ⊗ d aug1) - ℒ.kron!(dk₁₁₁, dk₁₁, a₁) - ℒ.kron!(k₃tmp, k₁₁, da₁) - dk₁₁₁ .+= k₃tmp - - # Plain form: ds₁ᵢ⁺ = S1 * da₁ - ℒ.mul!(ds₁ᵢ⁺[i], 𝐒[1], da₁) - # Plain form: ds₂ᵢ⁺ = S1 * da₂ + 0.5 * S2 * d(a₁⊗a₁) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[1], da₂) - ℒ.mul!(ds₂ᵢ⁺[i], 𝐒[2], dk₁₁, 0.5, 1.0) - # Plain form: ds₃ᵢ⁺ = S1 * da₃ + S2 * d(a₁⁰⊗a₂) + (1/6) * S3 * d(a₁⊗a₁⊗a₁) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[1], da₃) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[2], dk₁₂⁰, 1.0, 1.0) - ℒ.mul!(ds₃ᵢ⁺[i], 𝐒[3], dk₁₁₁, 1/6, 1.0) - - @inbounds for v in 1:nᵥ - decomposition[v, i, t] += wₖ * ( - ds₁ᵢ⁺[i][v] + - ds₂ᵢ⁺[i][v] + - ds₃ᵢ⁺[i][v] - ) - end - end - - # Advance the primal and tangent buffers to the next period. - s₁, s₁⁺ = s₁⁺, s₁ - s₂, s₂⁺ = s₂⁺, s₂ - s₃, s₃⁺ = s₃⁺, s₃ - for i in 1:nE - ds₁ᵢ[i], ds₁ᵢ⁺[i] = ds₁ᵢ⁺[i], ds₁ᵢ[i] - ds₂ᵢ[i], ds₂ᵢ⁺[i] = ds₂ᵢ⁺[i], ds₂ᵢ[i] - ds₃ᵢ[i], ds₃ᵢ⁺[i] = ds₃ᵢ⁺[i], ds₃ᵢ[i] - end - end - end - max_residual = zero(R) - max_reference = zero(R) - @inbounds for t in 1:nₜ, v in 1:nᵥ - ϕsum = zero(R) - for i in 1:nE - ϕsum += decomposition[v, i, t] - end - residual = variables[v, t] - (decomposition[v, nE + 1, t] + ϕsum) - max_residual = max(max_residual, abs(residual)) - max_reference = max(max_reference, abs(variables[v, t])) - end - - T = float(R) - scale = max(T(max_reference), sqrt(eps(T))) - return T(max_residual) / scale -end - """ Compute log-likelihood using the inversion filter, which calls the find_shocks function to recover shocks that match the observables. For higher-order solutions the global @@ -873,10 +225,10 @@ function calculate_loglikelihood(::Val{:inversion}, 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx, var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx, so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -934,11 +286,12 @@ function calculate_loglikelihood(::Val{:inversion}, state¹⁻_vol = vcat(state₁, one(R)) aug_state₁ = vcat(state₁, one(R), ones(R, n_exo)) aug_state₂ = vcat(state₂, zero(R), zeros(R, n_exo)) - kronaug_state₁ = zeros(R, n_aug^2) - kron_buffer = zeros(R, n_exo^2) - kron_buffer2 = zeros(R, n_exo^2, n_exo) + kronaug_state₁ = zeros(R, n_aug * (n_aug + 1) ÷ 2) + n_exo² = n_exo * (n_exo + 1) ÷ 2 + kron_buffer = zeros(R, n_exo²) + kron_buffer2 = zeros(R, n_exo², n_exo) kron_buffer3 = zeros(R, n_exo * n_state_vol, n_exo) - kronstate¹⁻_vol = zeros(R, n_state_vol^2) + kronstate¹⁻_vol = zeros(R, n_state_vol * (n_state_vol + 1) ÷ 2) shock_independent = zeros(R, n_cond) 𝐒ⁱ = Matrix{R}(𝐒¹ᵉ) jacc = Matrix{R}(𝐒¹ᵉ) @@ -986,7 +339,7 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂) - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -1014,7 +367,7 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) @@ -1098,7 +451,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # jacc = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(T.nExo), x) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jacc, 𝐒ⁱ²ᵉ, kron_buffer2) @@ -1129,7 +482,7 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂) - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -1194,10 +547,10 @@ function calculate_loglikelihood(::Val{:inversion}, 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,cc.var_vol²_cols] # 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -1272,7 +625,7 @@ function calculate_loglikelihood(::Val{:inversion}, aug_state[n_past + 1] = one(R) copyto!(aug_state, n_past + 2, view(warmup_shocks, :, w), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(state, 𝐒⁻¹, aug_state) ℒ.mul!(state, 𝐒⁻², kronaug_state, 1/2, 1) end @@ -1297,7 +650,7 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) # shock_independent = data_in_deviations[:,i] - (𝐒¹⁻ᵛ * state¹⁻_vol + 𝐒²⁻ᵛ * ℒ.kron(state¹⁻_vol, state¹⁻_vol) / 2) @@ -1378,7 +731,7 @@ function calculate_loglikelihood(::Val{:inversion}, # end # jacc = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(T.nExo), x) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jacc, 𝐒ⁱ²ᵉ, kron_buffer2) @@ -1409,7 +762,7 @@ function calculate_loglikelihood(::Val{:inversion}, # println("Match with data: $res") # state = 𝐒⁻¹ * aug_state + 𝐒⁻² * ℒ.kron(aug_state, aug_state) / 2 - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(state, 𝐒⁻¹, aug_state) ℒ.mul!(state, 𝐒⁻², kronaug_state, 1/2 ,1) end @@ -1468,17 +821,19 @@ function calculate_loglikelihood(::Val{:inversion}, var_vol²_idxs = cc.var_vol²_idxs var²_idxs = so.var²_idxs to = constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,shockvar_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,so.shockvar²_cols] + 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,cc.shockvar_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -1493,10 +848,10 @@ function calculate_loglikelihood(::Val{:inversion}, shockvar³2_idxs = to.shockvar³2_idxs shockvar³_idxs = to.shockvar³_idxs - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_idxs] |> collect - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,to.shockvar³2_cols] |> collect + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx,:] 𝐒³⁻ᵛ = nnz(𝐒³⁻ᵛ) / length(𝐒³⁻ᵛ) > .1 ? collect(𝐒³⁻ᵛ) : 𝐒³⁻ᵛ @@ -1516,7 +871,6 @@ function calculate_loglikelihood(::Val{:inversion}, kron_buffer = ws.kron_buffer kron_buffer² = ws.kron_buffer² J = ℒ.I(T.nExo) - II = ℒ.I(T.nExo^2) kron_buffer2 = ws.kron_buffer2 kron_buffer3 = ws.kron_buffer3 kron_buffer4 = ws.kron_buffer4 @@ -1536,9 +890,9 @@ function calculate_loglikelihood(::Val{:inversion}, copyto!(state_vol, 1, state[1], 1, n_past) state_vol[end] = one(R) ℒ.kron!(kron_buffer_state, J, state_vol) - x_kron_II!(kron_buffer4sv, state_vol) - ℒ.kron!(kron_buffer2ss, state[1], state[1]) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state_vol) + # The compressed shock-shock-state contraction is assembled below. + compressed_kron²_power!(kron_buffer2ss, state[1]) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) # Use workspaces for augmented state kron operations kron_aug_state₁ = ws.kronaug_state @@ -1583,7 +937,11 @@ function calculate_loglikelihood(::Val{:inversion}, ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter failed during pruned third-order warmup") end @@ -1610,8 +968,8 @@ function calculate_loglikelihood(::Val{:inversion}, aug_state₃[n_past + 1] = zero(R) fill!(view(aug_state₃, n_past + 2:n_past + 1 + n_exo), zero(R)) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) ℒ.mul!(state¹⁻, 𝐒⁻¹, aug_state₁) @@ -1619,8 +977,8 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(state²⁻, 𝐒⁻², kron_aug_state₁, 1/2, 1) ℒ.mul!(state³⁻, 𝐒⁻¹, aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(state³⁻, 𝐒⁻², kron_aug_state₁, 1, 1) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) + ℒ.mul!(state³⁻, 𝐒⁻², kron_aug_state₁, 1.0, 1) ℒ.mul!(state³⁻, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) end @@ -1647,15 +1005,15 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(shock_independent, 𝐒¹⁻, state³⁻, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kron_buffer2ss, state¹⁻, state²⁻) + compressed_kron²!(kron_buffer2ss, state¹⁻, state²⁻) ℒ.mul!(shock_independent, 𝐒²⁻, kron_buffer2ss, -1, 1) - ℒ.kron!(kronstate_vol³, kronstate_vol, state¹⁻_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) @@ -1671,17 +1029,24 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(𝐒ⁱ, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) ℒ.axpy!(1, 𝐒¹ᵉ, 𝐒ⁱ) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) + # The compressed shock-shock-state contraction is assembled below. copyto!(𝐒ⁱ²ᵉ, 𝐒²ᵉ) ℒ.rdiv!(𝐒ⁱ²ᵉ, 2) - ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) # x, jacc, matchd = find_shocks(Val(:fixed_point), state isa Vector{Float64} ? [state] : state, 𝐒, data_in_deviations[:,i], observables, T) @@ -1844,9 +1209,9 @@ function calculate_loglikelihood(::Val{:inversion}, # println("COBYLA: $(ℒ.norm(x3-x) / max(ℒ.norm(x3), ℒ.norm(x))), $(ℒ.norm(x3)-ℒ.norm(x))") # end - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) - ℒ.kron!(kron_buffer3, kron_buffer2, x) + compressed_kron³!(kron_buffer3, x, x, J) ℒ.mul!(jacc, 𝐒ⁱ²ᵉ, kron_buffer2) @@ -1892,9 +1257,9 @@ function calculate_loglikelihood(::Val{:inversion}, fill!(view(aug_state₃, n_past + 2:n_past + 1 + n_exo), zero(R)) # kron_aug_state₁ = ℒ.kron(aug_state₁, aug_state₁) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) # res = 𝐒[1][cond_var_idx,:] * aug_state₁ + 𝐒[1][cond_var_idx,:] * aug_state₂ + 𝐒[2][cond_var_idx,:] * kron_aug_state₁ / 2 + 𝐒[1][cond_var_idx,:] * aug_state₃ + 𝐒[2][cond_var_idx,:] * ℒ.kron(aug_state₁̂, aug_state₂) + 𝐒[3][cond_var_idx,:] * ℒ.kron(kron_aug_state₁,aug_state₁) / 6 - data_in_deviations[:,i] # println("Match with data: $res") @@ -1911,9 +1276,9 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(state³⁻, 𝐒⁻¹, aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(state³⁻, 𝐒⁻², kron_aug_state₁, 1, 1) + ℒ.mul!(state³⁻, 𝐒⁻², kron_aug_state₁, 1.0, 1) ℒ.mul!(state³⁻, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) end @@ -1969,16 +1334,18 @@ function calculate_loglikelihood(::Val{:inversion}, var_vol²_idxs = cc.var_vol²_idxs var²_idxs = so.var²_idxs to = constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -1994,10 +1361,10 @@ function calculate_loglikelihood(::Val{:inversion}, shockvar³2_idxs = to.shockvar³2_idxs shockvar³_idxs = to.shockvar³_idxs - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_idxs] - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,to.shockvar³2_cols] + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx,:] 𝐒³⁻ᵛ = nnz(𝐒³⁻ᵛ) / length(𝐒³⁻ᵛ) > .1 ? collect(𝐒³⁻ᵛ) : 𝐒³⁻ᵛ @@ -2018,8 +1385,6 @@ function calculate_loglikelihood(::Val{:inversion}, kron_buffer4 = ws.kron_buffer4 - II = sparse(ℒ.I(T.nExo^2)) - # Use workspace buffers for state/estimation temporaries state_vol = ws.state_vol kronstate_vol = ws.kronstate_vol @@ -2059,7 +1424,11 @@ function calculate_loglikelihood(::Val{:inversion}, ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter failed during third-order warmup") end @@ -2072,11 +1441,11 @@ function calculate_loglikelihood(::Val{:inversion}, aug_state[n_past + 1] = one(R) copyto!(aug_state, n_past + 2, view(warmup_shocks, :, w), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(state, 𝐒⁻¹, aug_state) ℒ.mul!(state, 𝐒⁻², kronaug_state, 1/2, 1) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(state, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) end @@ -2100,22 +1469,29 @@ function calculate_loglikelihood(::Val{:inversion}, ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kronstate_vol³, state¹⁻_vol, kronstate_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) # 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * kron(I, sv) + 𝐒³⁻ᵉ² * kron(kron(I, sv), sv) / 2 ℒ.kron!(kron_buffer_state, J, state¹⁻_vol) copyto!(𝐒ⁱ, 𝐒¹ᵉ) ℒ.mul!(𝐒ⁱ, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) + # The compressed shock-shock-state contraction is assembled below. copyto!(𝐒ⁱ²ᵉ, 𝐒²ᵉ); ℒ.rdiv!(𝐒ⁱ²ᵉ, 2) - ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) fill!(init_guess, zero(R)) @@ -2266,9 +1642,9 @@ function calculate_loglikelihood(::Val{:inversion}, # # end # jacc = -(𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * kron(I,x) + 3 * 𝐒ⁱ³ᵉ * kron(I, kron(x,x))) - ℒ.kron!(kron_buffer2, J, x) - ℒ.kron!(kron_buffer, x, x) - ℒ.kron!(kron_buffer3, J, kron_buffer) + compressed_kron²!(kron_buffer2, x, J) + compressed_kron²_power!(kron_buffer, x) + compressed_kron³!(kron_buffer3, x, x, J) copyto!(jacc, 𝐒ⁱ) ℒ.mul!(jacc, 𝐒ⁱ²ᵉ, kron_buffer2, 2, 1) ℒ.mul!(jacc, 𝐒ⁱ³ᵉ, kron_buffer3, 3, 1) @@ -2295,8 +1671,8 @@ function calculate_loglikelihood(::Val{:inversion}, copyto!(aug_state, n_past + 2, x, 1, n_exo) # state = 𝐒⁻¹ * aug_state + 𝐒⁻² * kron(aug,aug)/2 + 𝐒⁻³ * kron(kron(aug,aug),aug)/6 - ℒ.kron!(kronaug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(state, 𝐒⁻¹, aug_state) ℒ.mul!(state, 𝐒⁻², kronaug_state, 1/2, 1) ℒ.mul!(state, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) @@ -2530,6 +1906,7 @@ end cond_var_idx = indexin(observables,sort(union(T.aux,T.var,T.exo_present))) computational_constants = ensure_computational_constants!(𝓂.constants) + cc = computational_constants so = ensure_conditional_forecast_constants!(𝓂.constants) # s_in_s⁺ = computational_constants.s_in_s shock²_idxs = computational_constants.shock²_idxs @@ -2544,10 +1921,10 @@ end 𝐒¹⁻ᵛ = 𝐒₁[cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒₁[cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒₂[cond_var_idx,var_vol²_idxs] + 𝐒²⁻ᵛ = 𝐒₂[cond_var_idx,cc.var_vol²_cols] # 𝐒²⁻ = 𝐒₂[cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒₂[cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒₂[cond_var_idx,shock²_idxs] + 𝐒²⁻ᵉ = 𝐒₂[cond_var_idx,so.shockvar²_cols] + 𝐒²ᵉ = 𝐒₂[cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒₂[T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -2631,7 +2008,7 @@ end aug_state[n_past + 1] = 1.0 copyto!(aug_state, n_past + 2, view(warmup_shocks, :, w), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(full_state, 𝐒₁, aug_state) ℒ.mul!(full_state, 𝐒₂, kronaug_state, 1/2, 1) @@ -2650,7 +2027,7 @@ end ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) # shock_independent = data_in_deviations[:,i] - (𝐒¹⁻ᵛ * state¹⁻_vol + 𝐒²⁻ᵛ * ℒ.kron(state¹⁻_vol, state¹⁻_vol) / 2) @@ -2755,7 +2132,7 @@ end # end # jacc = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(T.nExo), x) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jacc, 𝐒ⁱ²ᵉ, kron_buffer2) @@ -2772,7 +2149,7 @@ end # println("Match with data: $res") # state = 𝐒⁻¹ * aug_state + 𝐒⁻² * ℒ.kron(aug_state, aug_state) / 2 - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(full_state, 𝐒₁, aug_state) ℒ.mul!(full_state, 𝐒₂, kronaug_state, 1/2 ,1) @@ -2825,6 +2202,7 @@ end cond_var_idx = indexin(observables,sort(union(T.aux,T.var,T.exo_present))) computational_constants = ensure_computational_constants!(𝓂.constants) + cc = computational_constants so = ensure_conditional_forecast_constants!(𝓂.constants) sv_in_s⁺ = computational_constants.s_in_s⁺ @@ -2838,10 +2216,10 @@ end 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx, var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx, so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -2947,7 +2325,7 @@ end ℒ.mul!(state[1], 𝐒[1], aug_state₁) state₁ .= state[1][T.past_not_future_and_mixed_idx] - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state[2], 𝐒[1], aug_state₂) ℒ.mul!(state[2], 𝐒[2], kronaug_state₁, 1/2, 1) state₂ .= state[2][T.past_not_future_and_mixed_idx] @@ -2971,7 +2349,7 @@ end ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) @@ -3094,7 +2472,7 @@ end ℒ.mul!(state[1], 𝐒[1], aug_state₁) state₁ .= state[1][T.past_not_future_and_mixed_idx] - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) # ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂) # ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) ℒ.mul!(state[2], 𝐒[1], aug_state₂) @@ -3220,22 +2598,25 @@ end cond_var_idx = indexin(observables,sort(union(T.aux,T.var,T.exo_present))) computational_constants = ensure_computational_constants!(𝓂.constants) + cc = computational_constants so = ensure_conditional_forecast_constants!(𝓂.constants; third_order = true) shock²_idxs = computational_constants.shock²_idxs shockvar²_idxs = so.shockvar²_idxs var_vol²_idxs = computational_constants.var_vol²_idxs var²_idxs = so.var²_idxs to = 𝓂.constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -3251,10 +2632,10 @@ end shockvar³2_idxs = to.shockvar³2_idxs shockvar³_idxs = to.shockvar³_idxs - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_idxs] - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,to.shockvar³2_cols] + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx,:] 𝐒³⁻ᵛ = nnz(𝐒³⁻ᵛ) / length(𝐒³⁻ᵛ) > .1 ? collect(𝐒³⁻ᵛ) : 𝐒³⁻ᵛ @@ -3280,8 +2661,6 @@ end kron_buffer4 = ws.kron_buffer4 - II = to.I_exo2 - state¹⁻_vol = ws.state_vol kronstate_vol = ws.kronstate_vol kronstate_vol³ = ws.kronstate_vol³ @@ -3327,7 +2706,11 @@ end ws, computational_constants.I_aug, computational_constants.I_state_vol, - computational_constants.I_exo) + computational_constants.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched @error "Inversion filter (3rd) failed during joint warmup" @@ -3341,8 +2724,8 @@ end aug_state[n_past + 1] = 1.0 copyto!(aug_state, n_past + 2, xw, 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(full_state, 𝐒[1], aug_state) ℒ.mul!(full_state, 𝐒[2], kronaug_state, 1/2, 1) ℒ.mul!(full_state, 𝐒[3], kron_kron_aug_state, 1/6, 1) @@ -3360,22 +2743,29 @@ end ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kronstate_vol³, kronstate_vol, state¹⁻_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) copyto!(𝐒ⁱ, 𝐒¹ᵉ) ℒ.kron!(kron_buffer_state, J, state¹⁻_vol) ℒ.mul!(𝐒ⁱ, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) + # The compressed shock-shock-state contraction is assembled below. copyto!(𝐒ⁱ²ᵉ, 𝐒²ᵉ) ℒ.rdiv!(𝐒ⁱ²ᵉ, 2) - ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) # x, jacc, matchd = find_shocks(Val(:fixed_point), state isa Vector{Float64} ? [state] : state, 𝐒, data_in_deviations[:,i], observables, T) @@ -3555,8 +2945,8 @@ end # println("Match with data: $res") # state = 𝐒⁻¹ * aug_state + 𝐒⁻² * ℒ.kron(aug_state, aug_state) / 2 + 𝐒⁻³ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 - ℒ.kron!(kronaug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(full_state, 𝐒[1], aug_state) ℒ.mul!(full_state, 𝐒[2], kronaug_state, 1/2, 1) ℒ.mul!(full_state, 𝐒[3], kron_kron_aug_state, 1/6, 1) @@ -3611,6 +3001,7 @@ end cond_var_idx = indexin(observables,sort(union(T.aux,T.var,T.exo_present))) computational_constants = ensure_computational_constants!(𝓂.constants) + cc = computational_constants so = ensure_conditional_forecast_constants!(𝓂.constants; third_order = true) s_in_s⁺ = computational_constants.s_in_s e_in_s⁺ = computational_constants.e_in_s⁺ @@ -3623,17 +3014,19 @@ end var²_idxs = so.var²_idxs to = 𝓂.constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,shockvar_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,so.shockvar²_cols] + 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,cc.shockvar_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -3648,10 +3041,10 @@ end shockvar³2_idxs = to.shockvar³2_idxs shockvar³_idxs = to.shockvar³_idxs - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_idxs] - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,to.shockvar³2_cols] + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx,:] 𝐒³⁻ᵛ = nnz(𝐒³⁻ᵛ) / length(𝐒³⁻ᵛ) > .1 ? collect(𝐒³⁻ᵛ) : 𝐒³⁻ᵛ @@ -3675,8 +3068,6 @@ end kron_buffer² = ws.kron_buffer² - II = to.I_exo2 - J = ℒ.I(T.nExo) kron_buffer2 = ws.kron_buffer2 @@ -3735,7 +3126,11 @@ end ws, computational_constants.I_aug, computational_constants.I_state_vol, - computational_constants.I_exo) + computational_constants.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched @error "Inversion filter (pruned 3rd) failed during joint warmup" @@ -3762,15 +3157,15 @@ end aug_state₃[n_past + 1] = 0.0 fill!(view(aug_state₃, n_past + 2:length(aug_state₃)), 0.0) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) ℒ.mul!(state[1], 𝐒[1], aug_state₁) ℒ.mul!(state[2], 𝐒[1], aug_state₂) ℒ.mul!(state[2], 𝐒[2], kron_aug_state₁, 1/2, 1) ℒ.mul!(state[3], 𝐒[1], aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(state[3], 𝐒[2], kron_aug_state₁, 1, 1) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) + ℒ.mul!(state[3], 𝐒[2], kron_aug_state₁, 1.0, 1) ℒ.mul!(state[3], 𝐒[3], kron_kron_aug_state₁, 1/6, 1) state₁ .= state[1][T.past_not_future_and_mixed_idx] @@ -3796,13 +3191,13 @@ end ℒ.mul!(shock_independent, 𝐒¹⁻, state₃, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kron_buffer2ss, state₁, state₂) + compressed_kron²!(kron_buffer2ss, state₁, state₂) ℒ.mul!(shock_independent, 𝐒²⁻, kron_buffer2ss, -1, 1) - ℒ.kron!(kronstate_vol³, kronstate_vol, state¹⁻_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) copyto!(𝐒ⁱ, 𝐒¹ᵉ) @@ -3810,13 +3205,19 @@ end ℒ.mul!(𝐒ⁱ, 𝐒²⁻ᵛᵉ, kron_buffer_state) ℒ.kron!(kron_buffer_state, J, state¹⁻_vol) ℒ.mul!(𝐒ⁱ, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) copyto!(𝐒ⁱ²ᵉ, 𝐒²ᵉ) ℒ.rdiv!(𝐒ⁱ²ᵉ, 2) - ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) # x, jacc, matchd = find_shocks(Val(:fixed_point), state isa Vector{Float64} ? [state] : state, 𝐒, data_in_deviations[:,i], observables, T) @@ -4013,8 +3414,8 @@ end aug_state₃[n_past + 1] = 0.0 fill!(view(aug_state₃, n_past + 2:length(aug_state₃)), 0.0) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) # res = 𝐒[1][cond_var_idx,:] * aug_state₁ + 𝐒[1][cond_var_idx,:] * aug_state₂ + 𝐒[2][cond_var_idx,:] * kron_aug_state₁ / 2 + 𝐒[1][cond_var_idx,:] * aug_state₃ + 𝐒[2][cond_var_idx,:] * ℒ.kron(aug_state₁̂, aug_state₂) + 𝐒[3][cond_var_idx,:] * ℒ.kron(kron_aug_state₁,aug_state₁) / 6 - data_in_deviations[:,i] # println("Match with data: $res") @@ -4026,8 +3427,8 @@ end ℒ.mul!(state[2], 𝐒[1], aug_state₂) ℒ.mul!(state[2], 𝐒[2], kron_aug_state₁, 1/2, 1) ℒ.mul!(state[3], 𝐒[1], aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(state[3], 𝐒[2], kron_aug_state₁, 1, 1) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) + ℒ.mul!(state[3], 𝐒[2], kron_aug_state₁, 1.0, 1) ℒ.mul!(state[3], 𝐒[3], kron_kron_aug_state₁, 1/6, 1) state₁ .= state[1][T.past_not_future_and_mixed_idx] @@ -4228,11 +3629,11 @@ function second_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, aug_state[n_past + 1] = one(R) copyto!(aug_state, n_past + 2, view(warmup_shocks, :, i), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(state_next, 𝐒⁻¹, aug_state) ℒ.mul!(state_next, 𝐒⁻², kronaug_state, 1/2, 1) - jac_aug = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state, I_aug) Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug[:, 1:n_past] Fu = 𝐒⁻¹[:, n_past+2:end] + 𝐒⁻² * jac_aug[:, n_past+2:end] @@ -4250,23 +3651,23 @@ function second_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, fill!(y_pred, zero(R)) ℒ.mul!(y_pred, 𝐒¹⁻ᵛ, state_vol) - ℒ.kron!(kronstate_vol, state_vol, state_vol) + compressed_kron²_power!(kronstate_vol, state_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵛ, kronstate_vol, 1/2, 1) ℒ.mul!(y_pred, 𝐒¹ᵉ, final_shock, 1, 1) ℒ.kron!(kron_shock_state, final_shock, state_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵉ, kron_shock_state, 1, 1) - ℒ.kron!(kron_shock_shock, final_shock, final_shock) + compressed_kron²_power!(kron_shock_shock, final_shock) ℒ.mul!(y_pred, 𝐒²ᵉ, kron_shock_shock, 1/2, 1) - jac_state_vol = (ℒ.kron(I_state_vol, state_vol) + ℒ.kron(state_vol, I_state_vol)) / 2 + jac_state_vol = compressed_kron²(state_vol, I_state_vol) jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * jac_state_vol + 𝐒²⁻ᵉ * ℒ.kron(final_shock, I_state_vol) copyto!(jac_x, 𝐒¹ᵉ) ℒ.kron!(kron_I_state, I_exo, state_vol) ℒ.mul!(jac_x, 𝐒²⁻ᵉ, kron_I_state, 1, 1) - jac_x += 𝐒²ᵉ * (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 + jac_x += 𝐒²ᵉ * compressed_kron²(final_shock, I_exo) jac = zeros(R, n_obs, n_z) ℒ.mul!(jac, jac_y_state[:, 1:n_past], ds_dz) @@ -4423,13 +3824,13 @@ function pruned_second_order_warmup_observation_and_jacobian(state10::AbstractVe aug_state₂[n_past + 1] = zero(R) fill!(view(aug_state₂, n_past + 2:n_past + 1 + n_exo), zero(R)) - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state₁_next, 𝐒⁻¹, aug_state₁) ℒ.mul!(state₂_next, 𝐒⁻¹, aug_state₂) ℒ.mul!(state₂_next, 𝐒⁻², kronaug_state₁, 1/2, 1) - jac_aug = (ℒ.kron(I_aug, aug_state₁) + ℒ.kron(aug_state₁, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state₁, I_aug) A11 = 𝐒⁻¹[:, 1:n_past] B1 = 𝐒⁻¹[:, n_past+2:end] A22 = 𝐒⁻¹[:, 1:n_past] @@ -4459,24 +3860,24 @@ function pruned_second_order_warmup_observation_and_jacobian(state10::AbstractVe fill!(y_pred, zero(R)) ℒ.mul!(y_pred, 𝐒¹⁻ᵛ, state₁_vol) ℒ.mul!(y_pred, 𝐒¹⁻, state₂, 1, 1) - ℒ.kron!(kronstate₁_vol, state₁_vol, state₁_vol) + compressed_kron²_power!(kronstate₁_vol, state₁_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵛ, kronstate₁_vol, 1/2, 1) ℒ.mul!(y_pred, 𝐒¹ᵉ, final_shock, 1, 1) ℒ.kron!(kron_shock_state, final_shock, state₁_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵉ, kron_shock_state, 1, 1) - ℒ.kron!(kron_shock_shock, final_shock, final_shock) + compressed_kron²_power!(kron_shock_shock, final_shock) ℒ.mul!(y_pred, 𝐒²ᵉ, kron_shock_shock, 1/2, 1) - jac_state_vol = (ℒ.kron(I_state_vol, state₁_vol) + ℒ.kron(state₁_vol, I_state_vol)) / 2 + jac_state_vol = compressed_kron²(state₁_vol, I_state_vol) jac_y_s1 = 𝐒¹⁻ᵛ[:, 1:n_past] + 𝐒²⁻ᵛ * jac_state_vol[:, 1:n_past] + 𝐒²⁻ᵉ * ℒ.kron(final_shock, I_state_vol)[:, 1:n_past] jac_y_s2 = 𝐒¹⁻ copyto!(jac_x, 𝐒¹ᵉ) ℒ.kron!(kron_I_state, I_exo, state₁_vol) ℒ.mul!(jac_x, 𝐒²⁻ᵉ, kron_I_state, 1, 1) - jac_x += 𝐒²ᵉ * (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 + jac_x += 𝐒²ᵉ * compressed_kron²(final_shock, I_exo) jac = zeros(R, n_obs, n_z) ℒ.mul!(jac, jac_y_s1, ds1_dz) @@ -4599,7 +4000,11 @@ function third_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, ws::inversion_workspace{R}, I_aug::AbstractMatrix{<:Real}, I_state_vol::AbstractMatrix{<:Real}, - I_exo::AbstractMatrix{<:Real}) where R <: Real + I_exo::AbstractMatrix{<:Real}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing) where R <: Real n_past = length(state0) n_exo = size(warmup_shocks, 1) n_warm = size(warmup_shocks, 2) @@ -4618,8 +4023,6 @@ function third_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, kron_aug3 = ws.kron_kron_aug_state kron_shock_state = ws.kron_shock_state kron_shock_shock = ws.kron_buffer - kron_shock_state2 = ws.kron_shock_state2 - kron_shock2_state = ws.kron_shock2_state kron_shock3 = ws.kron_buffer² kron_I_state = ws.kron_buffer_state @@ -4634,18 +4037,25 @@ function third_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, aug_state[n_past + 1] = one(R) copyto!(aug_state, n_past + 2, view(warmup_shocks, :, i), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(state_next, 𝐒⁻¹, aug_state) ℒ.mul!(state_next, 𝐒⁻², kronaug_state, 1/2, 1) - ℒ.kron!(kron_aug3, kronaug_state, aug_state) + compressed_kron³_power!(kron_aug3, aug_state) ℒ.mul!(state_next, 𝐒⁻³, kron_aug3, 1/6, 1) - jac_aug2 = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 - jac_aug3 = ℒ.kron(ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug), aug_state) + ℒ.kron(kronaug_state, I_aug) + jac_aug2 = compressed_kron²(aug_state, I_aug) + jac_aug3 = compressed_kron³(aug_state, aug_state, I_aug) - Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2[:, 1:n_past] + 𝐒⁻³ * (jac_aug3[:, 1:n_past] / 6) - Fu = 𝐒⁻¹[:, n_past+2:end] + 𝐒⁻² * jac_aug2[:, n_past+2:end] + 𝐒⁻³ * (jac_aug3[:, n_past+2:end] / 6) + # The weights here are the forward ones times the derivative of the + # compressed power, not the forward ones. Differentiating the two lines + # above gives 2·compressed_kron²(aug, ∂aug) and 3·compressed_kron³(aug, + # aug, ∂aug), so ½ becomes 1 and ⅙ becomes ½ — where the uncompressed + # form divided by 6, because its own Jacobian carried no such factor. + # `test/test_compressed_kron.jl` pins both identities against finite + # differences. + Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2[:, 1:n_past] + 𝐒⁻³ * (jac_aug3[:, 1:n_past] / 2) + Fu = 𝐒⁻¹[:, n_past+2:end] + 𝐒⁻² * jac_aug2[:, n_past+2:end] + 𝐒⁻³ * (jac_aug3[:, n_past+2:end] / 2) ℒ.mul!(ds_tmp, Fs, ds_dz) copyto!(ds_dz, ds_tmp) @@ -4659,13 +4069,27 @@ function third_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, state_vol[end] = one(R) final_shock = view(warmup_shocks, :, n_warm) + # Compressed triple coordinates mixing shocks and states. The model's + # `third_order_indices` already holds these, and every caller in the filters + # passes them in; the fallback is for direct calls without the model's + # constants in scope. The pullback resolves the same sets the same way. + shock_offset = n_past + 1 + if isnothing(shock_state_state_indices) + shock_state_state_indices, shock_state_state_rows = + compressed_shock_state_state_index_map(n_state_vol, n_exo) + end + if isnothing(shock_shock_state_indices) + shock_shock_state_indices, shock_shock_state_rows = + compressed_shock_shock_state_index_map(n_state_vol, n_exo) + end + fill!(y_pred, zero(R)) ℒ.mul!(y_pred, 𝐒¹⁻ᵛ, state_vol) - ℒ.kron!(kronstate_vol, state_vol, state_vol) + compressed_kron²_power!(kronstate_vol, state_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵛ, kronstate_vol, 1/2, 1) - ℒ.kron!(kronstate_vol3, state_vol, kronstate_vol) + compressed_kron³_power!(kronstate_vol3, state_vol) ℒ.mul!(y_pred, 𝐒³⁻ᵛ, kronstate_vol3, 1/6, 1) ℒ.mul!(y_pred, 𝐒¹ᵉ, final_shock, 1, 1) @@ -4673,35 +4097,57 @@ function third_order_warmup_observation_and_jacobian(state0::AbstractVector{R}, ℒ.kron!(kron_shock_state, final_shock, state_vol) ℒ.mul!(y_pred, 𝐒²⁻ᵉ, kron_shock_state, 1, 1) - ℒ.kron!(kron_shock_state2, kron_shock_state, state_vol) + kron_shock_state2 = compressed_triple_shock_state_state(final_shock, state_vol, + shock_offset, + shock_state_state_indices; + index_rows = shock_state_state_rows) ℒ.mul!(y_pred, 𝐒³⁻ᵉ², kron_shock_state2, 1/2, 1) - ℒ.kron!(kron_shock_shock, final_shock, final_shock) + compressed_kron²_power!(kron_shock_shock, final_shock) ℒ.mul!(y_pred, 𝐒²ᵉ, kron_shock_shock, 1/2, 1) - ℒ.kron!(kron_shock2_state, kron_shock_shock, state_vol) + kron_shock2_state = compressed_triple_shock_shock_state(final_shock, state_vol, + shock_offset, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) ℒ.mul!(y_pred, 𝐒³⁻ᵉ, kron_shock2_state, 1/2, 1) - ℒ.kron!(kron_shock3, final_shock, kron_shock_shock) + compressed_kron³_power!(kron_shock3, final_shock) ℒ.mul!(y_pred, 𝐒³ᵉ, kron_shock3, 1/6, 1) - jac_state2 = ℒ.kron(I_state_vol, state_vol) + ℒ.kron(state_vol, I_state_vol) - jac_state3 = ℒ.kron(jac_state2, state_vol) + ℒ.kron(kronstate_vol, I_state_vol) + jac_state2 = compressed_kron²(state_vol, I_state_vol) + jac_state3 = compressed_kron³(state_vol, state_vol, I_state_vol) - jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * (jac_state2 / 2) + 𝐒³⁻ᵛ * (jac_state3 / 6) + jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * jac_state2 + 𝐒³⁻ᵛ * (jac_state3 / 2) jac_y_state += 𝐒²⁻ᵉ * ℒ.kron(final_shock, I_state_vol) - jac_y_state += 𝐒³⁻ᵉ² * (ℒ.kron(ℒ.kron(final_shock, I_state_vol), state_vol) + ℒ.kron(ℒ.kron(final_shock, state_vol), I_state_vol)) / 2 - jac_y_state += 𝐒³⁻ᵉ * ℒ.kron(kron_shock_shock, I_state_vol) / 2 + jac_y_state += 𝐒³⁻ᵉ² * compressed_triple_shock_state_to_state(final_shock, + state_vol, + shock_offset, + shock_state_state_indices; + index_rows = shock_state_state_rows) + jac_y_state += 𝐒³⁻ᵉ * compressed_triple_shock_shock_state_to_state(final_shock, + state_vol, + shock_offset, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) / 2 copyto!(jac_x, 𝐒¹ᵉ) ℒ.kron!(kron_I_state, I_exo, state_vol) ℒ.mul!(jac_x, 𝐒²⁻ᵉ, kron_I_state, 1, 1) - jac_x += 𝐒³⁻ᵉ² * ℒ.kron(ℒ.kron(I_exo, state_vol), state_vol) / 2 - jac_x += 𝐒²ᵉ * (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 - jac_x += 𝐒³⁻ᵉ * (ℒ.kron(I_exo, kron_shock_state) + ℒ.kron(final_shock, kron_I_state)) / 2 - - jac_x3 = ℒ.kron(ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo), final_shock) + ℒ.kron(kron_shock_shock, I_exo) - jac_x += 𝐒³ᵉ * (jac_x3 / 6) + jac_x += 𝐒³⁻ᵉ² * compressed_triple_state_pair_to_shock(kronstate_vol, + n_aug, + shock_offset, + n_exo, + shock_state_state_indices; + index_rows = shock_state_state_rows) + jac_x += 𝐒²ᵉ * compressed_kron²(final_shock, I_exo) + jac_x += 𝐒³⁻ᵉ * compressed_triple_state_shock_to_shock(state_vol, + final_shock, + shock_offset, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + + jac_x += 𝐒³ᵉ * (compressed_kron³(final_shock, final_shock, I_exo) / 2) jac = zeros(R, n_obs, n_z) ℒ.mul!(jac, jac_y_state[:, 1:n_past], ds_dz) @@ -4729,6 +4175,10 @@ function solve_third_order_joint_warmup_shocks_with_jacobian(state0::AbstractVec I_aug::AbstractMatrix{<:Real}, I_state_vol::AbstractMatrix{<:Real}, I_exo::AbstractMatrix{<:Real}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing, max_iter::Int = 80, tol::Real = 1e-10) where R <: Real n_exo = size(𝐒¹ᵉ, 2) @@ -4764,13 +4214,20 @@ function solve_third_order_joint_warmup_shocks_with_jacobian(state0::AbstractVec ws, I_aug, I_state_vol, - I_exo) + I_exo, + shock_state_state_indices = shock_state_state_indices, + shock_state_state_rows = shock_state_state_rows, + shock_shock_state_indices = shock_shock_state_indices, + shock_shock_state_rows = shock_shock_state_rows) copyto!(y, y_new) copyto!(jac, jac_new) r .= target .- y residual = ℒ.norm(r) / max(ℒ.norm(target), ℒ.norm(y), 1.0) - residual < tol && (matched = true; break) + if residual < tol + matched = true + break + end JJt = jac * jac' JJt_lu = ℒ.lu(JJt, check = false) @@ -5001,10 +4458,10 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx, var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx, so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -5076,7 +4533,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂) - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -5097,7 +4554,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s copyto!(shock_independent, view(data_in_deviations, :, i)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) ℒ.kron!(kron_buffer3, J, state¹⁻_vol) @@ -5125,7 +4582,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s end if i > presample_periods jacc_v = view(jacc_v_buf, 1:m, :) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jacc_v, 𝐒ⁱ²ᵉ_v, kron_buffer2) ℒ.axpby!(1, 𝐒ⁱ_v, 2, jacc_v) logabsdets += m == n_exo ? ℒ.logabsdet(jacc_v)[1] : ℒ.logabsdet(jacc_v * jacc_v')[1] / 2 @@ -5146,7 +4603,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_s ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂) - ℒ.kron!(kronaug_state₁, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_state₁, aug_state₁) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -5190,9 +4647,9 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -5253,7 +4710,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o aug_state[length(st) + 1] = one(R) copyto!(aug_state, length(st) + 2, view(warmup_shocks, :, w), 1) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(st, 𝐒⁻¹, aug_state) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) end @@ -5274,7 +4731,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o copyto!(shock_independent, view(data_in_deviations, :, i)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) ℒ.kron!(kron_buffer3, J, state¹⁻_vol) @@ -5302,7 +4759,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o end if i > presample_periods jacc_v = view(jacc_v_buf, 1:m, :) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jacc_v, 𝐒ⁱ²ᵉ_v, kron_buffer2) ℒ.axpby!(1, 𝐒ⁱ_v, 2, jacc_v) logabsdets += m == n_exo ? ℒ.logabsdet(jacc_v)[1] : ℒ.logabsdet(jacc_v * jacc_v')[1] / 2 @@ -5318,7 +4775,7 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:second_o aug_state[length(st) + 1] = one(R) copyto!(aug_state, length(st) + 2, x, 1) - ℒ.kron!(kronaug_state, aug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) ℒ.mul!(st, 𝐒⁻¹, aug_state) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) end @@ -5361,6 +4818,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t var_vol²_idxs = cc.var_vol²_idxs var²_idxs = so.var²_idxs to = constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows var_vol³_idxs = to.var_vol³_idxs shock³_idxs = to.shock³_idxs shockvar³2_idxs = to.shockvar³2_idxs @@ -5371,16 +4830,16 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx, var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx, shockvar_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx, so.var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx, cc.shockvar_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx, var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx, shockvar³2_idxs] |> collect - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx, shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx, shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx, to.shockvar³2_cols] |> collect + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -5399,7 +4858,6 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t st3 = convert(Vector{R}, state[3][T.past_not_future_and_mixed_idx]) J = ℒ.I(n_exo) - II = ℒ.I(n_exo^2) 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 state_vol = ws.state_vol @@ -5456,7 +4914,11 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter (pruned 3rd, missing) failed during warmup") end @@ -5483,8 +4945,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t aug_state₃[n_past + 1] = zero(R) fill!(view(aug_state₃, n_past + 2:n_past + 1 + n_exo), zero(R)) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) ℒ.mul!(st1, 𝐒⁻¹, aug_state₁) @@ -5492,8 +4954,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t ℒ.mul!(st2, 𝐒⁻², kron_aug_state₁, 1/2, 1) ℒ.mul!(st3, 𝐒⁻¹, aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(st3, 𝐒⁻², kron_aug_state₁, 1, 1) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) + ℒ.mul!(st3, 𝐒⁻², kron_aug_state₁, 1.0, 1) ℒ.mul!(st3, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) end @@ -5516,11 +4978,11 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, st2, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, st3, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kron_buffer2ss, st1, st2) + compressed_kron²!(kron_buffer2ss, st1, st2) ℒ.mul!(shock_independent, 𝐒²⁻, kron_buffer2ss, -1, 1) - ℒ.kron!(kronstate_vol³, kronstate_vol, state¹⁻_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) copyto!(state²⁻_vol, 1, st2, 1); state²⁻_vol[end] = zero(R) @@ -5528,13 +4990,19 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵛᵉ, kron_buffer_state) ℒ.kron!(kron_buffer_state, J, state¹⁻_vol) ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ_full, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) ℒ.axpy!(1, 𝐒¹ᵉ, 𝐒ⁱ_full) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) copyto!(𝐒ⁱ²ᵉ_full, 𝐒²ᵉ); ℒ.rdiv!(𝐒ⁱ²ᵉ_full, 2) - ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) if m == 0 x = x_zero @@ -5558,8 +5026,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t return on_failure_loglikelihood end if i > presample_periods - ℒ.kron!(kron_buffer2, J, x) - ℒ.kron!(kron_buffer3, kron_buffer2, x) + compressed_kron²!(kron_buffer2, x, J) + compressed_kron³!(kron_buffer3, x, x, J) jacc_v = view(jacc_v_buf, 1:m, :) ℒ.mul!(jacc_v, 𝐒ⁱ²ᵉ_v, kron_buffer2) ℒ.mul!(jacc_v, 𝐒ⁱ³ᵉ_v, kron_buffer3, 3, 2) @@ -5578,14 +5046,14 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:pruned_t copyto!(aug_state₂, 1, st2, 1, n_past); aug_state₂[n_past+1] = zero(R); fill!(view(aug_state₂, n_past+2:n_past+1+n_exo), zero(R)) copyto!(aug_state₃, 1, st3, 1, n_past); aug_state₃[n_past+1] = zero(R); fill!(view(aug_state₃, n_past+2:n_past+1+n_exo), zero(R)) - ℒ.kron!(kron_aug_state₁, aug_state₁, aug_state₁) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁) + compressed_kron²_power!(kron_aug_state₁, aug_state₁) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁) ℒ.mul!(st1, 𝐒⁻¹, aug_state₁) ℒ.mul!(st2, 𝐒⁻¹, aug_state₂); ℒ.mul!(st2, 𝐒⁻², kron_aug_state₁, 1/2, 1) ℒ.mul!(st3, 𝐒⁻¹, aug_state₃) - ℒ.kron!(kron_aug_state₁, aug_state₁̂, aug_state₂) - ℒ.mul!(st3, 𝐒⁻², kron_aug_state₁, 1, 1) + compressed_kron²!(kron_aug_state₁, aug_state₁̂, aug_state₂) + ℒ.mul!(st3, 𝐒⁻², kron_aug_state₁, 1.0, 1) ℒ.mul!(st3, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) end @@ -5625,6 +5093,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or shockvar²_idxs = so.shockvar²_idxs var_vol²_idxs = cc.var_vol²_idxs to = constants.third_order + shock_shock_state_indices = to.shock_shock_state_idxs + shock_shock_state_rows = to.shock_shock_state_rows var_vol³_idxs = to.var_vol³_idxs shock³_idxs = to.shock³_idxs shockvar³2_idxs = to.shockvar³2_idxs @@ -5634,14 +5104,14 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, var_vol²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx, shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx, cc.var_vol²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx, so.shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx, cc.shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx, :] - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx, var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx, shockvar³2_idxs] |> collect - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx, shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx, shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,to.var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx, to.shockvar³2_cols] |> collect + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,to.shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,to.shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx, :] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -5656,7 +5126,6 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or st = convert(Vector{R}, state[T.past_not_future_and_mixed_idx]) J = ℒ.I(n_exo) - II = sparse(ℒ.I(n_exo^2)) 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 state_vol = ws.state_vol @@ -5708,7 +5177,11 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = to.shock_state_state_idxs, + shock_state_state_rows = to.shock_state_state_rows, + shock_shock_state_indices = to.shock_shock_state_idxs, + shock_shock_state_rows = to.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter (3rd, missing) failed during warmup") end @@ -5721,8 +5194,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or aug_state[n_past + 1] = one(R) copyto!(aug_state, n_past + 2, view(warmup_shocks, :, w), 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(st, 𝐒⁻¹, aug_state) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) ℒ.mul!(st, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) @@ -5745,20 +5218,26 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or copyto!(shock_independent, view(data_in_deviations, :, i)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate_vol, -1/2, 1) - ℒ.kron!(kronstate_vol³, state¹⁻_vol, kronstate_vol) + compressed_kron³_power!(kronstate_vol³, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kronstate_vol³, -1/6, 1) ℒ.kron!(kron_buffer_state, J, state¹⁻_vol) copyto!(𝐒ⁱ_full, 𝐒¹ᵉ) ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵉ, kron_buffer_state, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_buffer_state, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate_vol) ℒ.mul!(𝐒ⁱ_full, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) - x_kron_II!(kron_buffer4sv, state¹⁻_vol) copyto!(𝐒ⁱ²ᵉ_full, 𝐒²ᵉ); ℒ.rdiv!(𝐒ⁱ²ᵉ_full, 2) - ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_past + 1 + n_exo, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) if m == 0 x = x_zero @@ -5782,9 +5261,9 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or return on_failure_loglikelihood end if i > presample_periods - ℒ.kron!(kron_buffer2, J, x) - ℒ.kron!(kron_buffer, x, x) - ℒ.kron!(kron_buffer3, J, kron_buffer) + compressed_kron²!(kron_buffer2, x, J) + compressed_kron²_power!(kron_buffer, x) + compressed_kron³!(kron_buffer3, x, x, J) jacc_v = view(jacc_v_buf, 1:m, :) copyto!(jacc_v, 𝐒ⁱ_v) ℒ.mul!(jacc_v, 𝐒ⁱ²ᵉ_v, kron_buffer2, 2, 1) @@ -5800,8 +5279,8 @@ function calculate_loglikelihood_with_missing(::Val{:inversion}, ::Val{:third_or end copyto!(aug_state, 1, st, 1, n_past); aug_state[n_past+1] = one(R); copyto!(aug_state, n_past+2, x, 1, n_exo) - ℒ.kron!(kronaug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kronaug_state, aug_state) + compressed_kron²_power!(kronaug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(st, 𝐒⁻¹, aug_state) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) ℒ.mul!(st, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) diff --git a/src/filter/particle.jl b/src/filter/particle.jl index 3deb3a2b2..44acc1c19 100644 --- a/src/filter/particle.jl +++ b/src/filter/particle.jl @@ -32,7 +32,8 @@ # # The structural shocks are i.i.d. standard normal (their standard deviations are # baked into the solution matrices 𝐒), and the state transition is the -# perturbation solution's `state_update` (first order through pruned third order). +# perturbation solution (first order through pruned third order), evaluated for +# the whole swarm at once — see "Batched transitions" below. # # Three variants are provided, each selected by its own `filter` value: # :bootstrap_particle — sequential importance resampling, i.e. the bootstrap @@ -42,6 +43,16 @@ # :auxiliary_particle — auxiliary particle filter of Pitt & Shephard (1999) # :tempered_particle — tempered particle filter of Herbst & Schorfheide (2019) # +# Which one to use is not a matter of taste once the observation is informative. +# With as many observables as shocks and a small H — the usual DSGE setting — the +# bootstrap proposal draws shocks from the prior and only then looks at the data, +# so almost all of its weight lands on a handful of particles. Its likelihood +# estimate is still unbiased, but its *filtered moments* (what +# `get_estimated_shocks` and friends report) are then averages over an effective +# sample of a few particles and vary wildly from seed to seed. The tempered +# filter exists precisely to fix that and is the variant to reach for whenever +# estimates, rather than a likelihood, are what is wanted. +# # The particle filter is a stochastic likelihood estimator and is **not** # differentiable (resampling is discontinuous); it is intended for use with # gradient-free samplers (e.g. Pigeons slice sampling, nested sampling). @@ -84,12 +95,15 @@ # of N, which avoids paying the resampling noise in periods that do not need it. effective_sample_size(W::AbstractVector{<:Real}) = 1.0 / sum(abs2, W) +# In-place resampling: ancestor indices are written into `idx`; `bins` is a +# cumulative-weight scratch used by the multinomial/residual schemes. Buffer +# reuse for the index/cumulative arrays follows LowLevelParticleFilters.jl. + # Walk N equally spaced points u₀, u₀+1/N, … through the cumulative weights, with # a single random offset u₀ ∈ [0, 1/N). A particle of weight Wᵢ spans Wᵢ·N spacings # so it is picked either ⌊N·Wᵢ⌋ or ⌈N·Wᵢ⌉ times — never far from its expectation. -function systematic_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) +function systematic_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) - idxs = Vector{Int}(undef, N) u0 = rand(rng) / N c = W[1] i = 1 @@ -99,17 +113,16 @@ function systematic_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{ i += 1 c += W[i] end - idxs[j] = i + idx[j] = i end - return idxs + return idx end # Split [0,1) into N strata of width 1/N and draw one independent uniform inside # each. Guarantees at most one draw per stratum (so counts stay close to N·Wᵢ) # while keeping the draws independent, unlike the systematic scheme. -function stratified_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) +function stratified_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) - idxs = Vector{Int}(undef, N) c = W[1] i = 1 @inbounds for j in 1:N @@ -118,74 +131,124 @@ function stratified_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{ i += 1 c += W[i] end - idxs[j] = i + idx[j] = i end - return idxs + return idx end # N independent draws from the categorical distribution defined by W, via binary # search on the cumulative weights. Simplest and noisiest: nothing prevents a # particle with weight 1/N from being drawn three times or not at all. -function multinomial_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) +function multinomial_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) - c = cumsum(W) - c[end] = one(eltype(c)) # guard against round-off so rand() ≤ c[end] - idxs = Vector{Int}(undef, N) + cumsum!(bins, W) + bins[N] = one(eltype(bins)) # guard against round-off @inbounds for j in 1:N - idxs[j] = searchsortedfirst(c, rand(rng)) + idx[j] = searchsortedfirst(bins, rand(rng)) end - return idxs + return idx end # Deterministic part first: particle i gets ⌊N·Wᵢ⌋ guaranteed copies, which carry # no randomness at all. Only the leftover R = N - Σ⌊N·Wᵢ⌋ slots are drawn, from # the renormalised fractional weights. Cuts the variance of the integer part to # zero, which helps most when a handful of particles dominate. -function residual_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}) +function residual_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) N = length(W) - idxs = Vector{Int}(undef, N) - counts = floor.(Int, N .* W) k = 0 @inbounds for i in 1:N - for _ in 1:counts[i] + ni = floor(Int, N * W[i]) + for _ in 1:ni k += 1 - idxs[k] = i + idx[k] = i end end R = N - k if R > 0 - resid = N .* W .- counts - s = sum(resid) + s = 0.0 + @inbounds for i in 1:N + bins[i] = N * W[i] - floor(N * W[i]) + s += bins[i] + end if s <= 0 # numerical degeneracy: fall back to multinomial - c = cumsum(W); c[end] = one(eltype(c)) - @inbounds for _ in 1:R - k += 1 - idxs[k] = searchsortedfirst(c, rand(rng)) - end + cumsum!(bins, W) else - resid ./= s - c = cumsum(resid); c[end] = one(eltype(c)) - @inbounds for _ in 1:R - k += 1 - idxs[k] = searchsortedfirst(c, rand(rng)) + @inbounds for i in 1:N + bins[i] /= s end + cumsum!(bins, bins) + end + bins[N] = one(eltype(bins)) + @inbounds for _ in 1:R + k += 1 + idx[k] = searchsortedfirst(bins, rand(rng)) end end - return idxs + return idx end -function particle_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) +@inline function particle_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) if scheme == :systematic - return systematic_resample_indices(rng, W) + systematic_resample_indices!(idx, rng, W) elseif scheme == :stratified - return stratified_resample_indices(rng, W) + stratified_resample_indices!(idx, rng, W) elseif scheme == :multinomial - return multinomial_resample_indices(rng, W) + multinomial_resample_indices!(idx, bins, rng, W) elseif scheme == :residual - return residual_resample_indices(rng, W) + residual_resample_indices!(idx, bins, rng, W) else error("Unknown resampling scheme `:$scheme`. Choose from `:systematic`, `:stratified`, `:multinomial`, `:residual`.") end + return idx +end + +# Allocating convenience form (used by the tests and by anything that just wants +# a set of ancestor indices without keeping scratch around). +function particle_resample_indices(rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) + N = length(W) + return particle_resample_indices!(Vector{Int}(undef, N), Vector{Float64}(undef, N), rng, W, scheme) +end + +# Normalise exp(logw) into `W` and return log Σ exp(logw). Returns `-Inf` (and +# leaves `W` untouched) when every entry is impossible or the sum is not finite, +# which is how the filters detect a period no particle can explain. +@inline function normalise_log_weights!(W::Vector{Float64}, logw::Vector{Float64}) + m = -Inf + @inbounds for p in eachindex(logw) + lp = logw[p] + m = lp > m ? lp : m + end + isfinite(m) || return -Inf + s = 0.0 + @inbounds for p in eachindex(logw) + s += exp(logw[p] - m) + end + (s > 0 && isfinite(s)) || return -Inf + ls = m + log(s) + @inbounds for p in eachindex(logw) + W[p] = exp(logw[p] - ls) + end + return ls +end + +# Same, but for weights that carry a prior `W` (the bootstrap/auxiliary update +# Wₚ ∝ Wₚ·p(yₜ|xₜᵖ)): returns log Σₚ Wₚ·exp(logdensₚ) and renormalises `W`. +@inline function reweight_log_weights!(W::Vector{Float64}, logdens::Vector{Float64}) + m = -Inf + @inbounds for p in eachindex(logdens) + lp = logdens[p] + m = lp > m ? lp : m + end + isfinite(m) || return -Inf + s = 0.0 + @inbounds for p in eachindex(logdens) + s += W[p] * exp(logdens[p] - m) + end + (s > 0 && isfinite(s)) || return -Inf + @inbounds for p in eachindex(logdens) + W[p] = W[p] * exp(logdens[p] - m) / s + end + return m + log(s) end @@ -265,45 +328,9 @@ function particle_initial_cloud_factor(Σ::Matrix{Float64}, scaling::Float64) end end -# Build the initial particle cloud. Each particle carries the same representation -# `state_update` consumes: a flat `Vector` for non-pruned orders, or a -# `Vector{Vector}` (first-order + higher-order components) for pruned orders. The -# first-order part is randomised around the initial mean with covariance -# `scaling·Σ`; higher-order pruned components are initialised deterministically. -function initialise_particles(rng::Random.AbstractRNG, state, pruning::Bool, - L::Matrix{Float64}, n_particles::Int, nVars::Int) - if pruning - mean1 = Vector{Float64}(state[1]) - rest = [Vector{Float64}(state[c]) for c in 2:length(state)] - return [vcat([mean1 .+ L * randn(rng, nVars)], [copy(r) for r in rest]) for _ in 1:n_particles] - else - mean1 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) - return [mean1 .+ L * randn(rng, nVars) for _ in 1:n_particles] - end -end - -# Full (summed) model state a particle reports to the measurement equation. -@inline particle_full_state(p, pruning::Bool) = pruning ? sum(p) : p - -# Log Gaussian measurement density of the observed rows for one particle's -# predicted observables, with diagonal measurement-error variances `me_var`. -# `rows` indexes the observed observables at the current period; returns -Inf on -# a non-finite prediction. -@inline function particle_log_measurement_density(full::AbstractVector, data_col, observables_index, - me_var, rows, log2pi::Float64) - q = 0.0 - @inbounds for r in rows - f = full[observables_index[r]] - isfinite(f) || return -Inf - v = data_col[r] - f - q += v * v / me_var[r] + log2pi + log(me_var[r]) - end - return -0.5 * q -end - # ── Non-diagonal measurement error ─────────────────────────────────────────── -# Everything above takes `me_var`, the diagonal of H, and reads it elementwise — +# Everything below takes `me_var`, the diagonal of H, and reads it elementwise — # the fast path, and the default. A correlated H needs the same two quantities, # vᵀH⁻¹v and log det H, but restricted to the rows observed in the period. Both # come from one Cholesky factor of H[rows, rows], so we factorise once per @@ -365,21 +392,6 @@ end return nothing end -# vᵀH⁻¹v over the observed rows. `Inf` on a non-finite prediction, matching the -# diagonal version's contract (an impossible particle gets zero weight). -# vᵀH⁻¹v = ‖L⁻¹v‖², so one forward substitution gives both the solve and the norm. -@inline function particle_quadratic_form(full::AbstractVector, data_col, observables_index, - me::DenseMeasurementError, rows) - me_sync!(me, rows) - v = me.buf - @inbounds for (k, r) in enumerate(rows) - f = full[observables_index[r]] - isfinite(f) || return Inf - v[k] = data_col[r] - f - end - return dense_me_quadform!(v, me.last_L) -end - # In-place forward substitution L y = v, returning ‖y‖². @inline function dense_me_quadform!(v::Vector{Float64}, L::Matrix{Float64}) q = 0.0 @@ -395,26 +407,14 @@ end return q end -@inline function particle_measurement_logZ(me::DenseMeasurementError, rows, log2pi::Float64) - me_sync!(me, rows) - return -0.5 * (length(rows) * log2pi + me.last_logdet) -end - -@inline function particle_log_measurement_density(full::AbstractVector, data_col, observables_index, - me::DenseMeasurementError, rows, log2pi::Float64) - q = particle_quadratic_form(full, data_col, observables_index, me, rows) - isfinite(q) || return -Inf - return particle_measurement_logZ(me, rows, log2pi) - 0.5 * q -end - # The diagonal of H: what the auxiliary filter's first-stage preview needs (it # only has to be a rough predictive variance — the second-stage reweighting is # exact whatever the preview used). me_diagonal(me_var::AbstractVector) = me_var me_diagonal(me::DenseMeasurementError) = ℒ.diag(me.H) -# Elementwise reciprocals for the batched first-order path; the dense filter -# keeps its factorisation instead and is passed through unchanged. +# Elementwise reciprocals for the batched scoring kernels; the dense filter keeps +# its factorisation instead and is passed through unchanged. me_inverse_diagonal(me_var::AbstractVector) = 1.0 ./ me_var me_inverse_diagonal(me::DenseMeasurementError) = me @@ -434,178 +434,624 @@ function assert_positive_measurement_error(me::DenseMeasurementError) end -# ── Allocation-free higher-order transitions ───────────────────────────────── -# In-place transitions for the nonlinear orders, mirroring the closures built by -# `parse_algorithm_to_state_update` but writing into a preallocated `out` with -# preallocated `aug`/kron scratch and BLAS `mul!`/`kron!` (no per-call heap -# allocation). The pruned orders reuse `pruned_state_update_{2nd,3rd}_order!`. -# `𝐒[1]` here is the augmented first-order matrix (constant column inserted). +# ── Measurement scoring on a batched cloud ─────────────────────────────────── +# The cloud is stored column-wise (see "Batched transitions"), so every kernel +# below reads particle `p` out of column `p` of the full-state matrix `F`. + +# Quadratic form eᵀH⁻¹e over the observed rows for particle column `p`, with +# diagonal H (`inv_me_var` holds the reciprocal variances). Returns `Inf` on a +# non-finite prediction, i.e. an impossible particle. +@inline function particle_quadform_col(F::AbstractMatrix{Float64}, p::Int, data_col, observables_index, inv_me_var, rows) + q = 0.0 + @inbounds for k in eachindex(rows) + r = rows[k] + f = F[observables_index[r], p] + isfinite(f) || return Inf + v = data_col[r] - f + q += v * v * inv_me_var[r] + end + return q +end + +# Same, for a correlated H: gather the innovation, then one triangular solve. +# vᵀH⁻¹v = ‖L⁻¹v‖², so the forward substitution gives both the solve and the norm. +@inline function particle_quadform_col(F::AbstractMatrix{Float64}, p::Int, data_col, observables_index, + me::DenseMeasurementError, rows) + me_sync!(me, rows) + v = me.buf + @inbounds for k in eachindex(rows) + r = rows[k] + f = F[observables_index[r], p] + isfinite(f) || return Inf + v[k] = data_col[r] - f + end + return dense_me_quadform!(v, me.last_L) +end + +# Log normalising constant of the Gaussian measurement density over the observed +# rows: -½(dₒ·log2π + log det H[rows, rows]). +@inline function particle_measurement_logZ(me_var::AbstractVector, rows, log2pi::Float64) + z = 0.0 + @inbounds for r in rows + z += log2pi + log(me_var[r]) + end + return -0.5 * z +end + +@inline function particle_measurement_logZ(me::DenseMeasurementError, rows, log2pi::Float64) + me_sync!(me, rows) + return -0.5 * (length(rows) * log2pi + me.last_logdet) +end + +# log p(yₜ | xₜᵖ) for every column of `F`, written into `logdens`. Kept serial: +# with a correlated H these share the one innovation buffer inside +# `DenseMeasurementError`, and at a handful of observables per particle they are +# nowhere near the cost of a transition anyway. +function score_cloud!(logdens::Vector{Float64}, F::Matrix{Float64}, data_col, observables_index, + me_var, inv_me_var, rows, log2pi::Float64) + logZ = particle_measurement_logZ(me_var, rows, log2pi) + @inbounds for p in eachindex(logdens) + q = particle_quadform_col(F, p, data_col, observables_index, inv_me_var, rows) + logdens[p] = isfinite(q) ? logZ - 0.5 * q : -Inf + end + return logdens +end -# Gather aug = [state[past]; const; shock] into the preallocated `aug` (explicit -# loops avoid the SubArray allocation of `state[past_idx]`). -@inline function fill_aug!(aug, state, past_idx, shock, const_val) - n_past = length(past_idx) - @inbounds for i in 1:n_past - aug[i] = state[past_idx[i]] +# eᵀH⁻¹e for every column of `F`, written into `dv` (what the tempering schedule +# and the Metropolis mutation work with). +function quadform_cloud!(dv::Vector{Float64}, F::Matrix{Float64}, data_col, observables_index, + inv_me_var, rows) + @inbounds for p in eachindex(dv) + dv[p] = particle_quadform_col(F, p, data_col, observables_index, inv_me_var, rows) end - @inbounds aug[n_past + 1] = const_val - @inbounds for e in eachindex(shock) - aug[n_past + 1 + e] = shock[e] + return dv +end + + +# ── Batched transitions ────────────────────────────────────────────────────── +# A particle is a state vector; a *cloud* is an `nVars × N` matrix whose columns +# are particles. Pruned solutions carry the state in several components, so a +# cloud is an `NTuple{K, Matrix{Float64}}` — K = 1 at first, second and third +# order, 2 at pruned second order, 3 at pruned third order. +# +# Storing the swarm this way is what makes the filters fast. The perturbation +# transition is, for every order, a set of matrix-vector products against the +# augmented state [xₜ₋₁[past]; 1; εₜ] and its compressed Kronecker powers. Laid +# out column-wise those become matrix-*matrix* products over the whole swarm, so +# one period costs a handful of `gemm` calls instead of N `gemv` calls: the same +# arithmetic at BLAS-3 efficiency, with the augmented and Kronecker scratch +# allocated once. +# +# The scratch is sized to a fixed column block rather than to N, so its memory +# does not grow with the particle count (the compressed cube of the augmented +# state has n(n+1)(n+2)/6 rows, which for a medium DSGE is already thousands). + +# Number of pruned state components per algorithm, as a `Val` so `ntuple` stays +# type-stable at the call site. +pruned_components(::Val{:first_order}) = Val(1) +pruned_components(::Val{:second_order}) = Val(1) +pruned_components(::Val{:third_order}) = Val(1) +pruned_components(::Val{:pruned_second_order}) = Val(2) +pruned_components(::Val{:pruned_third_order}) = Val(3) + +n_components(::Val{K}) where {K} = K::Int + +# Block sizing, from `default_options.jl`: +# DEFAULT_PARTICLE_SCRATCH_BYTES upper bound on the memory the +# augmented/Kronecker scratch may occupy in total across all threads. It is +# what caps the column-block size at third order, where the compressed cube of +# the augmented state already has thousands of rows. +# DEFAULT_PARTICLE_MIN_BLOCK below this the `gemm` calls stop +# amortising their own overhead. +# DEFAULT_PARTICLE_PARALLEL_MIN_WORK below this much arithmetic per sweep +# (scratch rows × particles) the task overhead outweighs the parallelism and +# the swarm is propagated on the calling thread, so small models with few +# particles behave exactly as if none of this existed. + +# The perturbation solution in the form the batched kernels want: `𝐒₁` is always +# the *augmented* first-order matrix (nVars × (nPast+1+nExo), constant column +# included — zero at first order, where the solution has no constant term), and +# `𝐒₂`/`𝐒₃` are the compressed second/third-order matrices, densified because +# they are more than half full for a typical DSGE and are multiplied by tall +# dense blocks. +struct ParticleTransition + 𝐒₁::Matrix{Float64} + 𝐒₂::Matrix{Float64} + 𝐒₃::Matrix{Float64} + past_idx::Vector{Int} + nVars::Int + nPast::Int + nExo::Int + naug::Int +end + +# First order is the only case that has to build its own augmented `𝐒₁`: the +# solution has no constant term, so the constant column is inserted as zeros. +function build_particle_transition(::Val{:first_order}, 𝐒, T) + nVars = T.nVars + nPast = T.nPast_not_future_and_mixed + nExo = T.nExo + naug = nPast + 1 + nExo + empty = Matrix{Float64}(undef, nVars, 0) + + S = Matrix{Float64}(𝐒 isa AbstractMatrix ? 𝐒 : 𝐒[1]) + 𝐒₁ = zeros(Float64, nVars, naug) + @views 𝐒₁[:, 1:nPast] .= S[:, 1:nPast] + @views 𝐒₁[:, nPast+2:naug] .= S[:, nPast+1:end] # constant column stays zero + return ParticleTransition(𝐒₁, empty, empty, T.past_not_future_and_mixed_idx, nVars, nPast, nExo, naug) +end + +# Why 𝐒₂/𝐒₃ are densified, when the solution stores them sparse. +# +# In the *compressed* basis they are not sparse. Compression merges each set of +# symmetric duplicate columns into one, so a compressed column is zero only when +# every uncompressed column it stands for was, and the surviving matrix is mostly +# full. Measured on the solutions themselves: +# +# 𝐒₂ shape density 𝐒₃ shape density +# FS2000 18 x 28 0.48 18 x 84 0.28 +# Gali 23 x 36 0.78 23 x 120 0.73 +# SW03 54 x 435 0.87 54 x 4495 0.84 +# SW07 66 x 595 0.62 66 x 7140 0.51 +# +# and dense `gemm` beats `SparseMatrixCSC` multiplication over the whole of that +# range. Against a swarm block of 512 / 4096 columns, sparse costs 3.2-7.9x more +# with BLAS on one thread and 2.6-26x more on four. The crossover — measured by +# thinning a 66 x 7140 matrix — sits near 1 % density: at 5 % sparse is still +# 1.35x slower single-threaded, and only at 1 % does it win (0.32x), which no +# model here comes close to. The shape is what does it: the operand is short and +# very wide, so the dense kernel is entirely cache-blocked BLAS while the CSC +# kernel is single-threaded scattered accumulation. +# +# If a model ever does produce a genuinely sparse compressed 𝐒₃, this is the one +# place to branch on `nnz`; the propagation code below needs no change, since +# `mul!` dispatches on the type it is handed. +function build_particle_transition(::Val{algo}, 𝐒, T) where {algo} + nVars = T.nVars + nPast = T.nPast_not_future_and_mixed + nExo = T.nExo + naug = nPast + 1 + nExo + empty = Matrix{Float64}(undef, nVars, 0) + + 𝐒₁ = Matrix{Float64}(𝐒[1]) + 𝐒₂ = length(𝐒) >= 2 ? Matrix{Float64}(𝐒[2]) : empty + 𝐒₃ = length(𝐒) >= 3 ? Matrix{Float64}(𝐒[3]) : empty + return ParticleTransition(𝐒₁, 𝐒₂, 𝐒₃, T.past_not_future_and_mixed_idx, nVars, nPast, nExo, naug) +end + +# Blocked augmented/Kronecker scratch for one worker task. Buffers an algorithm +# does not use are left empty rather than absent, so the type is the same at +# every order and the filters never dispatch on the scratch. +# aug1 augmented first-order state [x¹[past]; 1; ε] +# aug2 same for the second pruned component (constant and shock slots zeroed) +# aug3 same for the third pruned component +# augĥ aug1 with the constant slot zeroed (pruned third-order cross term) +# kk compressed aug1 ⊗ aug1 +# kk2 compressed augĥ ⊗ aug2 +# kkk compressed aug1 ⊗ aug1 ⊗ aug1 +struct ScratchSlot + aug1::Matrix{Float64} + aug2::Matrix{Float64} + aug3::Matrix{Float64} + augĥ::Matrix{Float64} + kk::Matrix{Float64} + kk2::Matrix{Float64} + kkk::Matrix{Float64} +end + +no_buffer() = Matrix{Float64}(undef, 0, 0) + +# Rows of a compressed square/cube of an `n`-vector. +@inline n_pair_rows(n::Int) = n * (n + 1) ÷ 2 +@inline n_triple_rows(n::Int) = n * (n + 1) * (n + 2) ÷ 6 + +@inline scratch_buffer(n::Int, blk::Int) = Matrix{Float64}(undef, n, blk) + +function build_scratch_slot(::Val{:first_order}, naug::Int, blk::Int) + return ScratchSlot(scratch_buffer(naug, blk), no_buffer(), no_buffer(), no_buffer(), + no_buffer(), no_buffer(), no_buffer()) +end + +function build_scratch_slot(::Val{:second_order}, naug::Int, blk::Int) + return ScratchSlot(scratch_buffer(naug, blk), no_buffer(), no_buffer(), no_buffer(), + scratch_buffer(n_pair_rows(naug), blk), no_buffer(), no_buffer()) +end + +function build_scratch_slot(::Val{:third_order}, naug::Int, blk::Int) + return ScratchSlot(scratch_buffer(naug, blk), no_buffer(), no_buffer(), no_buffer(), + scratch_buffer(n_pair_rows(naug), blk), no_buffer(), + scratch_buffer(n_triple_rows(naug), blk)) +end + +function build_scratch_slot(::Val{:pruned_second_order}, naug::Int, blk::Int) + return ScratchSlot(scratch_buffer(naug, blk), scratch_buffer(naug, blk), no_buffer(), no_buffer(), + scratch_buffer(n_pair_rows(naug), blk), no_buffer(), no_buffer()) +end + +function build_scratch_slot(::Val{:pruned_third_order}, naug::Int, blk::Int) + return ScratchSlot(scratch_buffer(naug, blk), scratch_buffer(naug, blk), + scratch_buffer(naug, blk), scratch_buffer(naug, blk), + scratch_buffer(n_pair_rows(naug), blk), scratch_buffer(n_pair_rows(naug), blk), + scratch_buffer(n_triple_rows(naug), blk)) +end + +# Scratch rows the algorithm needs per column, which is what sets the memory cost +# of a block. +scratch_rows(::Val{:first_order}, naug::Int) = naug +scratch_rows(::Val{:second_order}, naug::Int) = naug + n_pair_rows(naug) +scratch_rows(::Val{:third_order}, naug::Int) = naug + n_pair_rows(naug) + n_triple_rows(naug) +scratch_rows(::Val{:pruned_second_order}, naug::Int) = 2 * naug + n_pair_rows(naug) +scratch_rows(::Val{:pruned_third_order}, naug::Int) = 4 * naug + 2 * n_pair_rows(naug) + n_triple_rows(naug) + +# The transition scratch, one slot per worker task. Column blocks read disjoint +# columns of the current cloud and write disjoint columns of the next one, so the +# swarm can be propagated in parallel and the result is bit-identical whatever +# the thread count — every random draw happens outside this loop. The block size +# is chosen to give each task roughly one block while keeping the total scratch +# inside `DEFAULT_PARTICLE_SCRATCH_BYTES`. +struct BatchScratch + slots::Vector{ScratchSlot} + blk::Int +end + +function build_batch_scratch(::Val{algo}, naug::Int, n_particles::Int)::BatchScratch where {algo} + rows = scratch_rows(Val(algo), naug) + nt = rows * n_particles >= DEFAULT_PARTICLE_PARALLEL_MIN_WORK ? max(Threads.nthreads(), 1) : 1 + blk_mem = max(DEFAULT_PARTICLE_MIN_BLOCK, DEFAULT_PARTICLE_SCRATCH_BYTES ÷ (8 * rows * nt)) + blk = clamp(cld(n_particles, nt), DEFAULT_PARTICLE_MIN_BLOCK, blk_mem) + blk = min(blk, n_particles) + n_slots = max(1, min(nt, cld(n_particles, blk))) + slots = ScratchSlot[build_scratch_slot(Val(algo), naug, blk) for _ in 1:n_slots] + return BatchScratch(slots, blk) +end + +# aug[:, j] = [X[past_idx, cols[j]]; const_val; with_shocks ? E[:, cols[j]] : 0]. +@inline function fill_aug_block!(aug::AbstractMatrix{Float64}, X::Matrix{Float64}, past_idx::Vector{Int}, + E::Matrix{Float64}, cols::UnitRange{Int}, const_val::Float64, with_shocks::Bool) + nPast = length(past_idx) + nExo = size(E, 1) + @inbounds for (j, p) in enumerate(cols) + for i in 1:nPast + aug[i, j] = X[past_idx[i], p] + end + aug[nPast + 1, j] = const_val + if with_shocks + for e in 1:nExo + aug[nPast + 1 + e, j] = E[e, p] + end + else + for e in 1:nExo + aug[nPast + 1 + e, j] = 0.0 + end + end end return aug end -# out = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug), aug = [state[past]; 1; shock]. -function nonpruned_state_update_2nd_order!(out, state, past_idx, shock, aug, kk, 𝐒) - fill_aug!(aug, state, past_idx, shock, 1.0) - ℒ.kron!(kk, aug, aug) - ℒ.mul!(out, 𝐒[1], aug) - ℒ.mul!(out, 𝐒[2], kk, 0.5, 1.0) - return out -end - -# out = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug) + ⅙ 𝐒₃·(aug⊗aug⊗aug). -function nonpruned_state_update_3rd_order!(out, state, past_idx, shock, aug, kk, kkk, 𝐒) - fill_aug!(aug, state, past_idx, shock, 1.0) - ℒ.kron!(kk, aug, aug) - ℒ.kron!(kkk, kk, aug) - ℒ.mul!(out, 𝐒[1], aug) - ℒ.mul!(out, 𝐒[2], kk, 0.5, 1.0) - ℒ.mul!(out, 𝐒[3], kkk, 1 / 6, 1.0) - return out -end - -# Allocation-free pruned updates (explicit past-gather; components zero the -# constant slot and, for the higher-order parts, the shock slots). -function pf_pruned_2nd!(new_s1, new_s2, s1, s2, past_idx, shock, aug1, aug2, kk, 𝐒) - n_past = length(past_idx) - fill_aug!(aug1, s1, past_idx, shock, 1.0) - @inbounds for i in 1:n_past - aug2[i] = s2[past_idx[i]] - end - @inbounds aug2[n_past + 1] = 0.0 - @inbounds for e in eachindex(shock) - aug2[n_past + 1 + e] = 0.0 - end - ℒ.kron!(kk, aug1, aug1) - ℒ.mul!(new_s1, 𝐒[1], aug1) - ℒ.mul!(new_s2, 𝐒[1], aug2) - ℒ.mul!(new_s2, 𝐒[2], kk, 0.5, 1.0) - return nothing +# One column block of the transition, written into `Xn`. `X` is the current +# cloud, `E` the shocks. Each method mirrors the corresponding closure built by +# `parse_algorithm_to_state_update`, with `aug = [x[past]; 1; ε]`. +# +# Every operand is a `view` into a preallocated buffer. Slicing whole columns of +# a `Matrix` gives a contiguous `SubArray`, which is a `StridedMatrix`, so `mul!` +# reaches BLAS `gemm` on it directly — no copy of the block, and no allocation: +# `@allocated` over a statically dispatched `propagate_block!` and +# `propagate_cloud!` is 0 bytes at every order, at any block size. +# +# Method bodies: +# +# :second_order x⁺ = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug) +# :third_order x⁺ = 𝐒₁·aug + ½ 𝐒₂·(aug⊗aug) + ⅙ 𝐒₃·(aug⊗aug⊗aug) +# :pruned_second_order x¹⁺ = 𝐒₁·aug¹, x²⁺ = 𝐒₁·aug² + ½ 𝐒₂·(aug¹⊗aug¹) +# :pruned_third_order adds x³⁺ = 𝐒₁·aug³ + 𝐒₂·(aug¹̂⊗aug²) + ⅙ 𝐒₃·(aug¹⊗aug¹⊗aug¹) +# +# where the higher pruned components zero the constant and shock slots of their +# augmented vector, and aug¹̂ is aug¹ with the constant slot zeroed. +function propagate_block!(::Val{:first_order}, tr::ParticleTransition, scr, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}, cols::UnitRange{Int}) where {K} + b = length(cols) + a1 = view(scr.aug1, :, 1:b) + o1 = view(Xn[1], :, cols) + + fill_aug_block!(a1, X[1], tr.past_idx, E, cols, 1.0, true) + ℒ.mul!(o1, tr.𝐒₁, a1) # x⁺ = 𝐒₁·aug + return Xn end -function pf_pruned_3rd!(new_s1, new_s2, new_s3, s1, s2, s3, past_idx, shock, - aug1, aug1̂, aug2, aug3, k11, k12̂, k111, 𝐒) - n_past = length(past_idx) - fill_aug!(aug1, s1, past_idx, shock, 1.0) - fill_aug!(aug1̂, s1, past_idx, shock, 0.0) - @inbounds for i in 1:n_past - aug2[i] = s2[past_idx[i]] - aug3[i] = s3[past_idx[i]] - end - @inbounds aug2[n_past + 1] = 0.0 - @inbounds aug3[n_past + 1] = 0.0 - @inbounds for e in eachindex(shock) - aug2[n_past + 1 + e] = 0.0 - aug3[n_past + 1 + e] = 0.0 - end - ℒ.kron!(k11, aug1, aug1) - ℒ.kron!(k12̂, aug1̂, aug2) - ℒ.kron!(k111, k11, aug1) - ℒ.mul!(new_s1, 𝐒[1], aug1) - ℒ.mul!(new_s2, 𝐒[1], aug2) - ℒ.mul!(new_s2, 𝐒[2], k11, 0.5, 1.0) - ℒ.mul!(new_s3, 𝐒[1], aug3) - ℒ.mul!(new_s3, 𝐒[2], k12̂, 1.0, 1.0) - ℒ.mul!(new_s3, 𝐒[3], k111, 1 / 6, 1.0) - return nothing +function propagate_block!(::Val{:second_order}, tr::ParticleTransition, scr, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}, cols::UnitRange{Int}) where {K} + b = length(cols) + a1 = view(scr.aug1, :, 1:b) + kk = view(scr.kk, :, 1:b) + o1 = view(Xn[1], :, cols) + + fill_aug_block!(a1, X[1], tr.past_idx, E, cols, 1.0, true) + compressed_kron²_power_columns!(kk, a1) + ℒ.mul!(o1, tr.𝐒₁, a1) # x⁺ = 𝐒₁·aug + ℒ.mul!(o1, tr.𝐒₂, kk, 0.5, 1.0) # x⁺ += ½ 𝐒₂·(aug⊗aug) + return Xn end -# Preallocated kron/aug scratch for one particle, sized per algorithm. -function build_higher_scratch(::Val{:second_order}, nPast::Int, nExo::Int) - naug = nPast + 1 + nExo - (aug = Vector{Float64}(undef, naug), kk = Vector{Float64}(undef, naug^2)) +function propagate_block!(::Val{:third_order}, tr::ParticleTransition, scr, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}, cols::UnitRange{Int}) where {K} + b = length(cols) + a1 = view(scr.aug1, :, 1:b) + kk = view(scr.kk, :, 1:b) + kkk = view(scr.kkk, :, 1:b) + o1 = view(Xn[1], :, cols) + + fill_aug_block!(a1, X[1], tr.past_idx, E, cols, 1.0, true) + compressed_kron²_power_columns!(kk, a1) + compressed_kron³_power_columns!(kkk, a1) + ℒ.mul!(o1, tr.𝐒₁, a1) # x⁺ = 𝐒₁·aug + ℒ.mul!(o1, tr.𝐒₂, kk, 0.5, 1.0) # x⁺ += ½ 𝐒₂·(aug⊗aug) + ℒ.mul!(o1, tr.𝐒₃, kkk, 1 / 6, 1.0) # x⁺ += ⅙ 𝐒₃·(aug⊗aug⊗aug) + return Xn end -function build_higher_scratch(::Val{:third_order}, nPast::Int, nExo::Int) - naug = nPast + 1 + nExo - (aug = Vector{Float64}(undef, naug), kk = Vector{Float64}(undef, naug^2), kkk = Vector{Float64}(undef, naug^3)) + +function propagate_block!(::Val{:pruned_second_order}, tr::ParticleTransition, scr, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}, cols::UnitRange{Int}) where {K} + b = length(cols) + past_idx = tr.past_idx + a1 = view(scr.aug1, :, 1:b) + a2 = view(scr.aug2, :, 1:b) + kk = view(scr.kk, :, 1:b) + o1 = view(Xn[1], :, cols) + o2 = view(Xn[2], :, cols) + + fill_aug_block!(a1, X[1], past_idx, E, cols, 1.0, true) + fill_aug_block!(a2, X[2], past_idx, E, cols, 0.0, false) + compressed_kron²_power_columns!(kk, a1) + ℒ.mul!(o1, tr.𝐒₁, a1) # x¹⁺ = 𝐒₁·aug¹ + ℒ.mul!(o2, tr.𝐒₁, a2) # x²⁺ = 𝐒₁·aug² + ℒ.mul!(o2, tr.𝐒₂, kk, 0.5, 1.0) # x²⁺ += ½ 𝐒₂·(aug¹⊗aug¹) + return Xn end -function build_higher_scratch(::Val{:pruned_second_order}, nPast::Int, nExo::Int) - naug = nPast + 1 + nExo - (aug1 = Vector{Float64}(undef, naug), aug2 = Vector{Float64}(undef, naug), - kk = Vector{Float64}(undef, naug^2), zero_shock = zeros(Float64, nExo)) + +function propagate_block!(::Val{:pruned_third_order}, tr::ParticleTransition, scr, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}, cols::UnitRange{Int}) where {K} + b = length(cols) + past_idx = tr.past_idx + a1 = view(scr.aug1, :, 1:b) + a2 = view(scr.aug2, :, 1:b) + a3 = view(scr.aug3, :, 1:b) + aĥ = view(scr.augĥ, :, 1:b) + kk = view(scr.kk, :, 1:b) + kk2 = view(scr.kk2, :, 1:b) + kkk = view(scr.kkk, :, 1:b) + o1 = view(Xn[1], :, cols) + o2 = view(Xn[2], :, cols) + o3 = view(Xn[3], :, cols) + + fill_aug_block!(a1, X[1], past_idx, E, cols, 1.0, true) + fill_aug_block!(aĥ, X[1], past_idx, E, cols, 0.0, true) + fill_aug_block!(a2, X[2], past_idx, E, cols, 0.0, false) + fill_aug_block!(a3, X[3], past_idx, E, cols, 0.0, false) + compressed_kron²_power_columns!(kk, a1) + compressed_kron²_columns!(kk2, aĥ, a2) + compressed_kron³_power_columns!(kkk, a1) + ℒ.mul!(o1, tr.𝐒₁, a1) # x¹⁺ = 𝐒₁·aug¹ + ℒ.mul!(o2, tr.𝐒₁, a2) # x²⁺ = 𝐒₁·aug² + ℒ.mul!(o2, tr.𝐒₂, kk, 0.5, 1.0) # x²⁺ += ½ 𝐒₂·(aug¹⊗aug¹) + ℒ.mul!(o3, tr.𝐒₁, a3) # x³⁺ = 𝐒₁·aug³ + ℒ.mul!(o3, tr.𝐒₂, kk2, 1.0, 1.0) # x³⁺ += 𝐒₂·(aug¹̂⊗aug²) + ℒ.mul!(o3, tr.𝐒₃, kkk, 1 / 6, 1.0) # x³⁺ += ⅙ 𝐒₃·(aug¹⊗aug¹⊗aug¹) + return Xn end -function build_higher_scratch(::Val{:pruned_third_order}, nPast::Int, nExo::Int) - naug = nPast + 1 + nExo - (aug1 = Vector{Float64}(undef, naug), aug1̂ = Vector{Float64}(undef, naug), - aug2 = Vector{Float64}(undef, naug), aug3 = Vector{Float64}(undef, naug), - k11 = Vector{Float64}(undef, naug^2), k12̂ = Vector{Float64}(undef, naug^2), - k111 = Vector{Float64}(undef, naug^3), zero_shock = zeros(Float64, nExo)) + +# Push the whole swarm one period forward, block by block, one task per scratch +# slot. Blocks are handed out in contiguous chunks so the assignment — and hence +# the result — does not depend on how the scheduler interleaves them. +# +# Why this is threaded at all, when most of a block is `mul!`. `𝐒₂`/`𝐒₃` are +# dense (see `build_particle_transition` for why that is the right call, and for +# where the sparse crossover lies), so those are `gemm` calls and BLAS is already +# threading them. What BLAS cannot touch is the other part of a +# block: `fill_aug_block!` and the compressed Kronecker kernels are plain +# sequential Julia loops, and they are not a rounding error — measured per block +# at 10 000 particles they are ~26 % of the time at pruned second order and ~37 % +# at pruned third, where the cube of the augmented state is the dominant buffer. +# +# So the two layers overlap, and how much this buys depends entirely on what BLAS +# is doing. On a 4-thread machine, spawning over blocks against a 4-thread BLAS is +# worth nothing at first order (the guard below keeps it single-tasked anyway), +# ~1.1x at pruned second order and ~1.2-1.4x at pruned third. Against a +# single-threaded BLAS the same code is worth 2.6-2.9x. +# +# That second number is the one that matters, because a particle filter's usual +# job is supplying a likelihood to a sampler, and samplers are run with BLAS +# pinned to one thread so the chains do not fight over cores. Keeping the +# block-level parallelism means the filter still scales in exactly that setting, +# and costs nothing measurable in the setting where BLAS is doing the work +# instead. +function propagate_cloud!(::Val{algo}, tr::ParticleTransition, bs::BatchScratch, + Xn::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, + E::Matrix{Float64}) where {algo, K} + N = size(E, 2) + blk = bs.blk + n_blocks = cld(N, blk) + n_tasks = min(length(bs.slots), n_blocks) + + if n_tasks <= 1 + for b in 1:n_blocks + propagate_block!(Val(algo), tr, bs.slots[1], Xn, X, E, block_cols(b, blk, N)) + end + return Xn + end + + per_task = cld(n_blocks, n_tasks) + @sync for i in 1:n_tasks + first_block = (i - 1) * per_task + 1 + first_block > n_blocks && break + last_block = min(i * per_task, n_blocks) + slot = bs.slots[i] + Threads.@spawn for b in first_block:last_block + propagate_block!(Val(algo), tr, slot, Xn, X, E, block_cols(b, blk, N)) + end + end + return Xn end -# In-place propagation dispatch: writes the next state into `out`. -@inline higher_propagate!(::Val{:second_order}, out, state, shock, past_idx, 𝐒, scr) = - nonpruned_state_update_2nd_order!(out, state, past_idx, shock, scr.aug, scr.kk, 𝐒) -@inline higher_propagate!(::Val{:third_order}, out, state, shock, past_idx, 𝐒, scr) = - nonpruned_state_update_3rd_order!(out, state, past_idx, shock, scr.aug, scr.kk, scr.kkk, 𝐒) -@inline higher_propagate!(::Val{:pruned_second_order}, out, state, shock, past_idx, 𝐒, scr) = - pf_pruned_2nd!(out[1], out[2], state[1], state[2], past_idx, shock, scr.aug1, scr.aug2, scr.kk, 𝐒) -@inline higher_propagate!(::Val{:pruned_third_order}, out, state, shock, past_idx, 𝐒, scr) = - pf_pruned_3rd!(out[1], out[2], out[3], state[1], state[2], state[3], past_idx, shock, scr.aug1, scr.aug1̂, scr.aug2, scr.aug3, scr.k11, scr.k12̂, scr.k111, 𝐒) +@inline block_cols(b::Int, blk::Int, N::Int) = ((b - 1) * blk + 1):min(b * blk, N) + +# `DEFAULT_PARTICLE_COPY_CHUNK` and `DEFAULT_PARTICLE_COPY_MAX_TASKS` set the +# columns per task for the pure-copy passes (the resampling gather and the +# Metropolis accept). Those are memory-bound rather than arithmetic-bound, so +# they saturate after a handful of threads and want far coarser chunks than the +# transition does — splitting them finely just buys task overhead. +# +# Run `f` over a fixed partition of `1:N` into contiguous column ranges, in +# parallel once there are enough of them to be worth it. The partition does not +# depend on the scheduler, and neither does the result: every use below writes +# into columns its own range owns. +@inline function foreach_column_chunk(f::F, N::Int) where {F} + nt = min(max(Threads.nthreads(), 1), DEFAULT_PARTICLE_COPY_MAX_TASKS, N ÷ DEFAULT_PARTICLE_COPY_CHUNK) + if nt <= 1 + f(1:N) + return nothing + end + per = cld(N, nt) + @sync for i in 1:nt + lo = (i - 1) * per + 1 + lo > N && break + hi = min(i * per, N) + Threads.@spawn f(lo:hi) + end + return nothing +end -# Deep-copy a particle (flat vector or vector-of-components) into a preallocated slot. -@inline copy_particle!(dst::AbstractVector{Float64}, src::AbstractVector{Float64}) = copyto!(dst, src) -@inline function copy_particle!(dst::AbstractVector{<:AbstractVector}, src::AbstractVector{<:AbstractVector}) - @inbounds for c in eachindex(dst) - copyto!(dst[c], src[c]) +# The full model state each particle reports to the measurement equation: the +# single component at non-pruned orders (no copy), the sum of the components at +# pruned orders (written into `F`). +@inline full_states!(F::Matrix{Float64}, X::NTuple{1,Matrix{Float64}}) = X[1] +@inline function full_states!(F::Matrix{Float64}, X::NTuple{K,Matrix{Float64}}) where {K} + copyto!(F, X[1]) + @inbounds for k in 2:K + Xk = X[k] + for i in eachindex(F) + F[i] += Xk[i] + end end - return dst + return F end -# A zeroed particle with the same shape as `template` (for the second pool). -zeros_like_particle(template::AbstractVector{Float64}) = zeros(Float64, length(template)) -zeros_like_particle(template::AbstractVector{<:AbstractVector}) = [zeros(Float64, length(c)) for c in template] +# Take `n_groups` clouds of `K` components each out of the workspace pools. +function cloud_group(pools::Vector{Matrix{Float64}}, group::Int, ::Val{K}) where {K} + off = (group - 1) * K + return ntuple(k -> pools[off + k], Val(K)) +end + +# Y[:, j] = X[:, idx[j]] for every component (the resampling gather). This moves +# as many bytes per stage as the transition does arithmetic, so it is chunked +# across threads the same way. +function gather_cloud!(Y::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}, idx::Vector{Int}) where {K} + foreach_column_chunk(length(idx)) do cols + @inbounds for k in 1:K + Yk = Y[k] + Xk = X[k] + nrow = size(Xk, 1) + for j in cols + a = idx[j] + for i in 1:nrow + Yk[i, j] = Xk[i, a] + end + end + end + end + return Y +end -# Concretely-typed initial particle cloud (avoids the type-unstable `Union` that -# `initialise_particles` returns because its `pruning` branch is a runtime Bool). -# Dispatching on `Val{algo}` fixes the element type per specialization so the hot -# loop is allocation-free. The RNG draw order matches `initialise_particles`. -function init_higher_particles(::Union{Val{:second_order},Val{:third_order}}, rng, state, L, n_particles, nVars) - mean0 = Vector{Float64}(state) - return Vector{Float64}[mean0 .+ L * randn(rng, nVars) for _ in 1:n_particles] +@inline function copy_cloud!(Y::NTuple{K,Matrix{Float64}}, X::NTuple{K,Matrix{Float64}}) where {K} + @inbounds for k in 1:K + copyto!(Y[k], X[k]) + end + return Y end -function init_higher_particles(::Val{:pruned_second_order}, rng, state, L, n_particles, nVars) - m1 = Vector{Float64}(state[1]); m2 = Vector{Float64}(state[2]) - return Vector{Vector{Float64}}[[m1 .+ L * randn(rng, nVars), copy(m2)] for _ in 1:n_particles] + +# Copy column `src` of `X` into column `dst` of `Y` (contiguous, allocation-free). +@inline function copy_col!(Y::AbstractMatrix{Float64}, dst::Int, X::AbstractMatrix{Float64}, src::Int) + @inbounds for i in axes(X, 1) + Y[i, dst] = X[i, src] + end + return Y end -function init_higher_particles(::Val{:pruned_third_order}, rng, state, L, n_particles, nVars) - m1 = Vector{Float64}(state[1]); m2 = Vector{Float64}(state[2]); m3 = Vector{Float64}(state[3]) - return Vector{Vector{Float64}}[[m1 .+ L * randn(rng, nVars), copy(m2), copy(m3)] for _ in 1:n_particles] + +@inline function copy_cloud_col!(Y::NTuple{K,Matrix{Float64}}, dst::Int, X::NTuple{K,Matrix{Float64}}, src::Int) where {K} + @inbounds for k in 1:K + copy_col!(Y[k], dst, X[k], src) + end + return Y end -# Concrete pool type per algorithm, used to type-assert the initial cloud so the -# large kwarg method body doesn't lose the element type (which would send the -# function-barrier call through dynamic dispatch). -particle_pool_type(::Union{Val{:second_order},Val{:third_order}}) = Vector{Vector{Float64}} -particle_pool_type(::Union{Val{:pruned_second_order},Val{:pruned_third_order}}) = Vector{Vector{Vector{Float64}}} +# The initial state as a plain vector per pruned component. +state_components(state) = state isa AbstractVector{<:AbstractVector} ? + [Vector{Float64}(c) for c in state] : [Vector{Float64}(state)] -# Full model state a particle reports to the measurement, without allocation: -# the state itself for non-pruned orders, the sum of components (into `full_buf`) -# for pruned orders. Dispatches on the particle representation (type-stable). -@inline measurement_full(p::AbstractVector{Float64}, full_buf) = p -@inline function measurement_full(p::AbstractVector{<:AbstractVector}, full_buf) - fill!(full_buf, 0.0) - @inbounds for c in eachindex(p) - pc = p[c] - for i in eachindex(full_buf) - full_buf[i] += pc[i] +# Put the deterministic initial state into every column of the cloud. +function fill_cloud_from_state!(X::NTuple{K,Matrix{Float64}}, state) where {K} + comps = state_components(state) + @inbounds for k in 1:K + Xk = X[k] + mk = k <= length(comps) ? comps[k] : nothing + for p in axes(Xk, 2), i in axes(Xk, 1) + Xk[i, p] = mk === nothing ? 0.0 : mk[i] end end - return full_buf + return X +end + +# Seed the cloud: the first-order component is drawn around the initial mean with +# covariance L Lᵀ, the higher pruned components are set deterministically (they +# have no unconditional spread of their own at the start of the sample). +function init_cloud!(X::NTuple{K,Matrix{Float64}}, rng::Random.AbstractRNG, state, + L::Matrix{Float64}, Z::Matrix{Float64}) where {K} + fill_cloud_from_state!(X, state) + Random.randn!(rng, Z) + ℒ.mul!(X[1], L, Z, 1.0, 1.0) # x¹ = mean + L·z + return X +end + +# Allocate a cloud outside the workspace (used by the shock decomposition, which +# runs a handful of deterministic trajectories rather than a particle swarm). +alloc_cloud(::Val{K}, nVars::Int, N::Int) where {K} = ntuple(_ -> zeros(Float64, nVars, N), Val(K)) + +# Common setup every `run_particle_filter` method needs. +function particle_filter_setup(::Val{algo}, 𝐒, T, 𝓂, measurement_error, n_particles, + initial_state_prior_scaling_factor, initial_covariance, opts) where {algo} + me_var = build_particle_measurement_error(measurement_error) + assert_positive_measurement_error(me_var) + + tr = build_particle_transition(Val(algo), 𝐒, T) + scr = build_batch_scratch(Val(algo), tr.naug, n_particles) + + Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) + L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + + pws = ensure_particle_workspace!(𝓂.workspaces, T.nVars, T.nExo, n_particles) + + return me_var, me_inverse_diagonal(me_var), tr, scr, L, pws end # ── Bootstrap (sequential importance resampling) particle filter ───────────── +# +# One iteration of the loop is the textbook predict / weight / resample cycle: +# +# 1. PREDICT draw a fresh shock for every particle and push the swarm through +# the model's state transition. The cloud now represents +# p(xₜ | y₁..ₜ₋₁). +# 2. WEIGHT score each particle by how well it explains today's observation, +# p(yₜ | xₜ). Averaging those scores over the (weighted) cloud is an +# unbiased estimate of the period's likelihood contribution, which +# is what gets accumulated into `loglik`. +# 3. RESAMPLE if the weights have become too uneven, replace the weighted cloud +# by an equally weighted one so the next predict step spends its +# particles where the probability mass actually is. function run_particle_filter(::Val{algo}, ::Val{:bootstrap}, @@ -626,132 +1072,66 @@ function run_particle_filter(::Val{algo}, presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) - - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - - past_idx = T.past_not_future_and_mixed_idx - 𝐒f = [Matrix{Float64}(S) for S in 𝐒] - scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) - - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - particles = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) - particles2 = [zeros_like_particle(particles[1]) for _ in 1:n_particles] - - return bootstrap_higher_loop(Val(algo), particles, particles2, 𝐒f, scr, past_idx, - nVars, nExo, nT, presample_periods, observables_index, - data_in_deviations, obs_idx_per_t, has_missing, me_var, - resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) + nT = size(data_in_deviations, 2) + + me_var, inv_me_var, tr, scr, L, pws = + particle_filter_setup(Val(algo), 𝐒, T, 𝓂, measurement_error, n_particles, + particle_initial_state_scaling, initial_covariance, opts) + + K = pruned_components(Val(algo)) + nK = n_components(K) + pools = ensure_particle_pools!(pws, 2 * nK + 1) + X = cloud_group(pools, 1, K) + X_scratch = cloud_group(pools, 2, K) + Fbuf = pools[2 * nK + 1] + + init_cloud!(X, particle_rng, state, L, Fbuf) + + return bootstrap_loop!(Val(algo), tr, scr, X, X_scratch, Fbuf, pws.E, pws.W, pws.logdens, + pws.idx, pws.bins, nT, normalize_presample_periods(presample_periods, nT), + observables_index, data_in_deviations, obs_idx_per_t, has_missing, + me_var, inv_me_var, particle_resampling, particle_resampling_threshold, + particle_rng, Float64(on_failure_loglikelihood), log(2π)) end -# The bootstrap recursion, period by period. One iteration of the loop below is -# the textbook predict / weight / resample cycle: -# -# 1. PREDICT draw a fresh shock for every particle and push it through the -# model's state transition. The cloud now represents p(xₜ | y₁..ₜ₋₁). -# 2. WEIGHT score each particle by how well it explains today's observation, -# p(yₜ | xₜ). Averaging those scores over the (weighted) cloud is an -# unbiased estimate of the period's likelihood contribution, which -# is what gets accumulated into `loglik`. -# 3. RESAMPLE if the weights have become too uneven, replace the weighted cloud -# by an equally weighted one so the next predict step spends its -# particles where the probability mass actually is. -# -# Arguments (the ones that are not self-evident): -# particles / particles2 two pools of the same shape, used as a double buffer: -# we always write the propagated cloud into the spare -# pool and then swap, which avoids allocating per period. -# 𝐒f perturbation solution matrices, already densified. -# scr preallocated kron/augmented-state scratch for the -# nonlinear transition (see `build_higher_scratch`). -# past_idx positions of the predetermined states inside a state -# vector — what the transition actually reads. -# me_var per-observable measurement-error variances. -# rows which observables are actually observed this period -# (all of them unless the data has holes). -# -# Function barrier: `particles`/`particles2` arrive with a concrete element type, -# so the hot loop specialises and runs allocation-free (the enclosing kwarg method -# body is too large for inference to keep the pool types). -function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, past_idx, - nVars, nExo, nT, presample_periods, observables_index, - data_in_deviations, obs_idx_per_t, has_missing, me_var, - resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) where {algo} - n_particles = length(particles) - shock = Vector{Float64}(undef, nExo) # one draw of structural shocks, reused - full_buf = Vector{Float64}(undef, nVars) # summed state of a pruned particle - W = fill(1.0 / n_particles, n_particles) # normalised importance weights - logdens = Vector{Float64}(undef, n_particles) # log p(yₜ | xₜ) per particle - idx = Vector{Int}(undef, n_particles) # ancestor indices from resampling - bins = Vector{Float64}(undef, n_particles) # cumulative-weight scratch - loglik = 0.0 +# Function barrier: the enclosing kwarg method body is too large for inference to +# keep the cloud types, so the hot loop lives here and runs allocation-free. +function bootstrap_loop!(::Val{algo}, tr, scr, X::NTuple{K,Matrix{Float64}}, X_scratch::NTuple{K,Matrix{Float64}}, + Fbuf, E, W, logdens, idx, bins, nT, presample_periods, observables_index, + data_in_deviations, obs_idx_per_t, has_missing, me_var, inv_me_var, + resampling, resampling_threshold, rng, on_failure_loglikelihood, log2pi) where {algo, K} + n_particles = size(E, 2) + fill!(W, 1.0 / n_particles) + loglik = 0.0 for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view data_in_deviations[:, t] - if isempty(rows) - # No observation this period: propagate only, weights unchanged. - @inbounds for p in 1:n_particles - Random.randn!(rng, shock) - higher_propagate!(Val(algo), particles2[p], particles[p], shock, past_idx, 𝐒f, scr) - end - particles, particles2 = particles2, particles - continue - end + # 1. PREDICT + Random.randn!(rng, E) + propagate_cloud!(Val(algo), tr, scr, X_scratch, X, E) + X, X_scratch = X_scratch, X - # 1. PREDICT + 2. SCORE, fused into one pass over the cloud: draw this - # particle's shocks, push it one period forward, and evaluate how well - # the resulting state explains today's observation. - @inbounds for p in 1:n_particles - Random.randn!(rng, shock) - higher_propagate!(Val(algo), particles2[p], particles[p], shock, past_idx, 𝐒f, scr) - full = measurement_full(particles2[p], full_buf) - logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) - end - particles, particles2 = particles2, particles # propagated cloud becomes current + isempty(rows) && continue # nothing observed: weights unchanged - # The period's likelihood contribution is log Σₚ Wₚ·p(yₜ|xₜᵖ). Factor out - # the largest log-density first (log-sum-exp) so the exponentials cannot - # underflow to zero when every particle fits the data poorly. - m = maximum(logdens) - if !isfinite(m) - # every particle is impossible (or the model blew up): give up cleanly - return Float64(on_failure_loglikelihood) - end - - s = 0.0 - @inbounds for p in 1:n_particles - s += W[p] * exp(logdens[p] - m) - end - if s <= 0 || !isfinite(s) - return Float64(on_failure_loglikelihood) - end - - ll_t = m + log(s) - if t > presample_periods # presample periods only warm the cloud up - loglik += ll_t - end - - # Bayes update of the weights: Wₚ ∝ Wₚ · p(yₜ|xₜᵖ), normalised by `s`. - @inbounds for p in 1:n_particles - W[p] = W[p] * exp(logdens[p] - m) / s + # 2. WEIGHT. The period's likelihood contribution is log Σₚ Wₚ·p(yₜ|xₜᵖ); + # `reweight_log_weights!` factors out the largest log-density first + # (log-sum-exp) so the exponentials cannot underflow to zero when every + # particle fits the data poorly. + F = full_states!(Fbuf, X) + score_cloud!(logdens, F, data_col, observables_index, me_var, inv_me_var, rows, log2pi) + ll_t = reweight_log_weights!(W, logdens) + isfinite(ll_t) || return on_failure_loglikelihood + + if t > presample_periods # presample periods only warm the cloud up + loglik += ll_t end # 3. RESAMPLE, but only once the cloud has actually degenerated. Doing it @@ -759,41 +1139,13 @@ function bootstrap_higher_loop(::Val{algo}, particles, particles2, 𝐒f, scr, p # would leave all the weight on a single particle within a few periods. if effective_sample_size(W) < resampling_threshold * n_particles particle_resample_indices!(idx, bins, rng, W, resampling) - @inbounds for j in 1:n_particles - copy_particle!(particles2[j], particles[idx[j]]) - end - particles, particles2 = particles2, particles - fill!(W, 1.0 / n_particles) # survivors are equally likely again + gather_cloud!(X_scratch, X, idx) + X, X_scratch = X_scratch, X + fill!(W, 1.0 / n_particles) # survivors are equally likely again end end - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) -end - - -# ── Shared measurement helpers for the auxiliary and tempered filters ──────── - -# Quadratic form eᵀH⁻¹e over the observed rows, with diagonal H (variances -# `me_var`). Returns Inf on a non-finite prediction (an impossible particle). -@inline function particle_quadratic_form(full::AbstractVector, data_col, observables_index, me_var, rows) - q = 0.0 - @inbounds for r in rows - f = full[observables_index[r]] - isfinite(f) || return Inf - v = data_col[r] - f - q += v * v / me_var[r] - end - return q -end - -# Log normalising constant of the Gaussian measurement density over the observed -# rows: -½(dₒ·log2π + Σ log me_var[r]). -@inline function particle_measurement_logZ(me_var, rows, log2pi::Float64) - z = 0.0 - @inbounds for r in rows - z += log2pi + log(me_var[r]) - end - return -0.5 * z + return isfinite(loglik) ? loglik : on_failure_loglikelihood end @@ -825,6 +1177,26 @@ end # evaluation per particle per period, so with a weak signal (large measurement # error) the plain bootstrap filter is the better trade. +# How far each observable can plausibly move in one period, used to spread the +# preview density above. +# +# The preview scores an ancestor at its zero-shock prediction. Judging it with the +# measurement-error variance alone would be far too strict: next period's shock +# will move the observable too, and an ancestor should not be discarded for +# missing the data by an amount a normal shock could easily cover. So the spread +# used here is "how much next period's shock moves this observable" plus "how +# noisily it is measured" — the first term being the row's own shock loading, the +# variance of the observable under a unit-normal shock draw. +# +# Getting this wrong is a matter of efficiency rather than correctness (the second +# stage divides the preview back out either way), but too tight a spread makes the +# preview reject almost every ancestor and collapses the cloud. +function auxiliary_predictive_variance(𝓂::ℳ, T, observables_index, me_var) + nPast = T.nPast_not_future_and_mixed + S₁ = 𝓂.caches.first_order_solution_matrix + return Float64[sum(abs2, @view S₁[observables_index[i], nPast+1:end]) for i in eachindex(observables_index)] .+ me_diagonal(me_var) +end + function run_particle_filter(::Val{algo}, ::Val{:auxiliary}, observables_index::Vector{Int}, @@ -844,65 +1216,45 @@ function run_particle_filter(::Val{algo}, presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) - - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - - past_idx = T.past_not_future_and_mixed_idx - 𝐒f = [Matrix{Float64}(S) for S in 𝐒] - scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) - - # One-step-ahead predictive variance of each observable due to the structural - # shocks (diagonal of Cₒ BBᵀ Cₒᵀ from the first-order shock loading), used to - # scale the auxiliary first-stage weights. Evaluating the predictive density - # at the transition mean alone would be near-degenerate when the observable is - # shock-driven; inflating by the shock spread keeps the proxy well-conditioned. - nPast = T.nPast_not_future_and_mixed - S₁cache = 𝓂.caches.first_order_solution_matrix - pred_var = Float64[sum(abs2, @view S₁cache[observables_index[i], nPast+1:end]) for i in eachindex(observables_index)] .+ me_diagonal(me_var) + nT = size(data_in_deviations, 2) + + me_var, inv_me_var, tr, scr, L, pws = + particle_filter_setup(Val(algo), 𝐒, T, 𝓂, measurement_error, n_particles, + particle_initial_state_scaling, initial_covariance, opts) + + pred_var = auxiliary_predictive_variance(𝓂, T, observables_index, me_var) + + K = pruned_components(Val(algo)) + nK = n_components(K) + pools = ensure_particle_pools!(pws, 3 * nK + 1) + X = cloud_group(pools, 1, K) + X_scratch = cloud_group(pools, 2, K) + anc = cloud_group(pools, 3, K) + Fbuf = pools[3 * nK + 1] + + init_cloud!(X, particle_rng, state, L, Fbuf) + + return auxiliary_loop!(Val(algo), tr, scr, X, X_scratch, anc, Fbuf, pws.E, pws.W, pws.logdens, + pws.logw, pws.lam, pws.idx, pws.bins, nT, + normalize_presample_periods(presample_periods, nT), + observables_index, data_in_deviations, obs_idx_per_t, has_missing, + me_var, inv_me_var, pred_var, 1.0 ./ pred_var, particle_resampling, + particle_rng, Float64(on_failure_loglikelihood), log(2π)) +end - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - particles = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) - particles2 = [zeros_like_particle(particles[1]) for _ in 1:n_particles] - mu_particle = zeros_like_particle(particles[1]) - - return auxiliary_higher_loop(Val(algo), particles, particles2, mu_particle, 𝐒f, scr, past_idx, - nVars, nExo, nT, presample_periods, observables_index, - data_in_deviations, obs_idx_per_t, has_missing, me_var, pred_var, - resampling, rng, on_failure_loglikelihood, log2pi) -end - -# Function barrier for the auxiliary loop (see `bootstrap_higher_loop`). -function auxiliary_higher_loop(::Val{algo}, particles, particles2, mu_particle, 𝐒f, scr, past_idx, - nVars, nExo, nT, presample_periods, observables_index, - data_in_deviations, obs_idx_per_t, has_missing, me_var, pred_var, - resampling, rng, on_failure_loglikelihood, log2pi) where {algo} - n_particles = length(particles) - zero_shock = zeros(Float64, nExo) - shock = Vector{Float64}(undef, nExo) - full_buf = Vector{Float64}(undef, nVars) - W = fill(1.0 / n_particles, n_particles) - logg̃ = Vector{Float64}(undef, n_particles) # first-stage predictive log-density - logw = Vector{Float64}(undef, n_particles) # second-stage log-weight - λ = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) +function auxiliary_loop!(::Val{algo}, tr, scr, X::NTuple{K,Matrix{Float64}}, X_scratch::NTuple{K,Matrix{Float64}}, + anc::NTuple{K,Matrix{Float64}}, Fbuf, E, W, logg̃, logw, lam, idx, bins, + nT, presample_periods, observables_index, data_in_deviations, obs_idx_per_t, + has_missing, me_var, inv_me_var, pred_var, inv_pred_var, resampling, rng, + on_failure_loglikelihood, log2pi) where {algo, K} + n_particles = size(E, 2) + fill!(W, 1.0 / n_particles) loglik = 0.0 for t in 1:nT @@ -911,479 +1263,535 @@ function auxiliary_higher_loop(::Val{algo}, particles, particles2, mu_particle, # First stage: predictive density at the transition mean (zero shock), # spread by the shock-induced predictive variance `pred_var`. - @inbounds for p in 1:n_particles - higher_propagate!(Val(algo), mu_particle, particles[p], zero_shock, past_idx, 𝐒f, scr) - μ = measurement_full(mu_particle, full_buf) - logg̃[p] = particle_log_measurement_density(μ, data_col, observables_index, pred_var, rows, log2pi) - end + fill!(E, 0.0) + propagate_cloud!(Val(algo), tr, scr, X_scratch, X, E) + Fμ = full_states!(Fbuf, X_scratch) + score_cloud!(logg̃, Fμ, data_col, observables_index, pred_var, inv_pred_var, rows, log2pi) - # First-stage (auxiliary) weights λ ∝ W · g̃, and κ = Σ W·g̃. - mλ = -Inf - @inbounds for p in 1:n_particles - lλ = log(W[p]) + logg̃[p] - mλ = lλ > mλ ? lλ : mλ - end - if !isfinite(mλ) - return Float64(on_failure_loglikelihood) - end - sλ = 0.0 - @inbounds for p in 1:n_particles - sλ += exp(log(W[p]) + logg̃[p] - mλ) - end - logκ = mλ + log(sλ) - @inbounds for p in 1:n_particles - λ[p] = exp(log(W[p]) + logg̃[p] - logκ) - end + # First-stage (auxiliary) weights λ ∝ W·g̃, with κ = Σ W·g̃. + copyto!(lam, W) + logκ = reweight_log_weights!(lam, logg̃) + isfinite(logκ) || return on_failure_loglikelihood # Resample ancestors ∝ λ, propagate with fresh shocks, second-stage weight # w = g(yₜ|xₜ) / g̃(ancestor). - particle_resample_indices!(idx, bins, rng, λ, resampling) + particle_resample_indices!(idx, bins, rng, lam, resampling) + gather_cloud!(anc, X, idx) + Random.randn!(rng, E) + propagate_cloud!(Val(algo), tr, scr, X_scratch, anc, E) + F = full_states!(Fbuf, X_scratch) + score_cloud!(logw, F, data_col, observables_index, me_var, inv_me_var, rows, log2pi) @inbounds for j in 1:n_particles - a = idx[j] - Random.randn!(rng, shock) - higher_propagate!(Val(algo), particles2[j], particles[a], shock, past_idx, 𝐒f, scr) - full = measurement_full(particles2[j], full_buf) - logw[j] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) - logg̃[a] + logw[j] -= logg̃[idx[j]] end - particles, particles2 = particles2, particles + X, X_scratch = X_scratch, X - mw = maximum(logw) - if !isfinite(mw) - return Float64(on_failure_loglikelihood) - end - sw = 0.0 - @inbounds for j in 1:n_particles - sw += exp(logw[j] - mw) - end - if sw <= 0 || !isfinite(sw) - return Float64(on_failure_loglikelihood) - end + logsw = normalise_log_weights!(W, logw) + isfinite(logsw) || return on_failure_loglikelihood - ll_t = logκ + (mw + log(sw) - log(n_particles)) + ll_t = logκ + logsw - log(n_particles) if t > presample_periods loglik += ll_t end - - logsw = mw + log(sw) - @inbounds for j in 1:n_particles - W[j] = exp(logw[j] - logsw) - end end - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) + return isfinite(loglik) ? loglik : on_failure_loglikelihood end -# ── Tempered particle filter (Herbst & Schorfheide, 2019) ──────────────────── -# Within each period the measurement information is introduced gradually through -# a bridging sequence 0 = φ₀ < φ₁ < … < φ_N = 1 (the measurement covariance is -# inflated to H/φ). Each stage reweights by the tempered density increment, -# resamples, and mutates the particles' shocks with a random-walk Metropolis step -# targeting the stage-φ posterior. This dramatically lowers the variance of the -# likelihood estimate relative to the bootstrap filter at equal particle count. - -# Inefficiency ratio N·Σ(wᵖ)² / (Σwᵖ)² for the incremental weights -# wᵖ = exp(-(φ-φ_old)/2 · dᵖ). Increasing in φ, equal to 1 at φ = φ_old. -function tempered_inefficiency(φ::Float64, φ_old::Float64, d::Vector{Float64}, n_particles::Int) - Δ = (φ - φ_old) / 2 - maxla = -Inf - @inbounds for p in 1:n_particles - la = isfinite(d[p]) ? -Δ * d[p] : -Inf - maxla = la > maxla ? la : maxla - end - isfinite(maxla) || return Inf - S1 = 0.0 - S2 = 0.0 - @inbounds for p in 1:n_particles - if isfinite(d[p]) - e = exp(-Δ * d[p] - maxla) - S1 += e - S2 += e * e - end - end - return S1 > 0 ? n_particles * S2 / (S1 * S1) : Inf +# ── Guided (conditionally optimal proposal) particle filter ────────────────── +# +# The observation of this filter is that a DSGE usually has as many structural +# shocks as observables, and a measurement error that is small next to the data. +# Given the ancestor xₜ₋₁, the observation then very nearly *determines* εₜ, and +# the conditional +# +# p(εₜ | xₜ₋₁, yₜ) ∝ N(εₜ; 0, I) · N(yₜ; C·g(xₜ₋₁, εₜ), H) +# +# is available in closed form. Linearising the observed transition in the shock, +# C·g(xₜ₋₁, ε) ≈ mₚ + Bₒ ε with Bₒ the first-order shock loading on the observed +# rows and mₚ the zero-shock prediction, gives +# +# p(εₜ | xₜ₋₁, yₜ) = N(μₚ, M⁻¹), M = I + BₒᵀH⁻¹Bₒ, μₚ = M⁻¹BₒᵀH⁻¹rₚ, +# p(yₜ | xₜ₋₁) = N(yₜ; mₚ, H + BₒBₒᵀ), +# +# with rₚ = yₜ - mₚ. Two things are worth noticing. M does not depend on the +# particle — only on the model and H — so it is factorised once per missing-data +# pattern, and μₚ is one small matrix product away from the residual. And the +# predictive density needed to *choose* ancestors is closed form too, so the +# filter can be fully adapted in the sense of Pitt & Shephard: pick ancestors by +# how well they explain yₜ before drawing any shock, then draw the shock from its +# own conditional. +# +# Why the ancestors are *not* preselected. Being able to score ancestors before +# drawing a shock is tempting: kill the hopeless ones early and spend the whole +# cloud on the promising ones, which is what "full adaptation" in the sense of +# Pitt & Shephard means. That was implemented and measured, and it made things +# worse, for a reason worth stating. +# +# The score λ is not the real predictive density, only the Gaussian approximation +# to it. Where the model fits, the two agree. Where it does not — a crisis period, +# an ancestor far out in the tail — the approximation is optimistic: it claims the +# ancestor can explain the observation much better than it actually can. Selecting +# on λ then hands most of the cloud to exactly the particles whose score is least +# trustworthy. The correction weight that follows does mark them down, but by then +# the selection has happened and there is nothing left to correct: the cloud is +# already made of copies of one bad ancestor. +# +# What makes this pathological rather than merely inefficient is that it does not +# improve with `n_particles`. A larger cloud reaches further into the tail, so it +# finds more of the ancestors the approximation flatters — measured on a euro-area +# model, a tenfold increase in particles left the worst period's effective sample +# size at a handful of particles and made the run-to-run spread of the estimates +# visibly *worse*. The usual remedy for a noisy particle filter does not apply. +# +# Resampling once on the combined weight removes the failure mode outright, and it +# is worth seeing why it is free rather than a trade-off. Draw the shock first and +# form a single weight afterwards: +# +# wⱼ = N(εⱼ;0,I)·p(yₜ|g(xₜ₋₁,εⱼ)) / q(εⱼ) +# = exp(logZ - ½(‖εⱼ‖² + rⱼᵀH⁻¹rⱼ - ‖zⱼ‖²)). +# +# The proposal q contains λ, so λ appears in the numerator and the denominator and +# divides out exactly before anything is selected. An over-confident λ therefore +# costs nothing — it never gets a vote on which particles survive. It is kept only +# as a diagnostic. +# +# What this buys. The bootstrap proposal ignores yₜ when drawing εₜ, and the +# tempered filter recovers the lost information by running a within-period MCMC — +# tens of transition evaluations per period. Here the same information is used +# directly: two transition evaluations per period, one at ε = 0 to get the +# residual and one at the drawn shock. And the importance weight +# +# ωⱼ = N(εⱼ;0,I)·p(yₜ|g(xₜ₋₁,εⱼ)) / (λ^{a(j)}·q(εⱼ)) +# +# is *identically one* when the transition is linear in the shock: every term +# involving εⱼ cancels. At pruned second order it is one up to the curvature the +# linearisation misses, which is exactly the residual the perturbation itself +# treats as small. So the weights barely vary, the cloud barely degenerates, and +# there is only one resampling per period rather than one per tempering stage. +# +# References. The conditionally optimal importance function is Doucet, Godsill & +# Andrieu (2000); full adaptation is Pitt & Shephard (1999). Building the proposal +# from a local Gaussian approximation is the "unscented"/"optimised" particle +# filter family (van der Merwe, Doucet, de Freitas & Wan, 2000; Andreasen, 2013, +# for DSGE), and solving for the shock that explains the observation before +# sampling around it is the implicit particle filter of Chorin, Morzfeld & Tu +# (2010) from geophysical data assimilation. + +# Everything the proposal needs, precomputed per missing-data pattern: the +# observed shock loading, the Cholesky factor of M (`U`, upper, M = UᵀU), its +# inverse (so `Uinv·z` has covariance M⁻¹), the map `K` from residual to +# conditional mean, and the log normalisation of p(yₜ|xₜ₋₁). +# +# Every buffer is allocated once at the full observable count and refilled in +# place by `rebuild_guided_proposal!` whenever the missing-data pattern changes. +# `d` is how many observables are live; `Bo`, `HinvBo` and `K` use only their +# first `d` rows (columns for `K`), the rest are stale. +# +# Everything here is sized by model dimensions — one row per shock, one per +# observable — so a rebuild was never expensive. The preallocation is worth having +# because with ragged data the pattern can change every period, and it keeps that +# case down to a constant handful of bytes per rebuild (the factorization +# wrapper) instead of a fresh set of matrices; it is not expected to save +# measurable time in the common case of one rebuild per run. +mutable struct GuidedProposal + const U::Matrix{Float64} # nExo × nExo, upper Cholesky factor of M = I + BₒᵀH⁻¹Bₒ + const Uinv::Matrix{Float64} # nExo × nExo, U⁻¹; Uinv·Uinvᵀ = M⁻¹ + const Minv::Matrix{Float64} # nExo × nExo, M⁻¹, for the Newton refinement below + const K::Matrix{Float64} # nExo × nObs, M⁻¹BₒᵀH⁻¹ in columns 1:d, so μ = K·r + const Bo::Matrix{Float64} # nObs × nExo, observed shock loading in rows 1:d + const HinvBo::Matrix{Float64} # nObs × nExo, H⁻¹Bₒ in rows 1:d + const M::Matrix{Float64} # nExo × nExo, overwritten by its own Cholesky factor + logZ::Float64 # -½(d·log2π + log|H| + log|M|) + d::Int # live observables end -# Next tempering level in (φ_old, 1] targeting inefficiency `r_star` by bisection. -function tempered_next_phi(φ_old::Float64, d::Vector{Float64}, r_star::Float64, n_particles::Int) - if tempered_inefficiency(1.0, φ_old, d, n_particles) <= r_star - return 1.0 - end - lo = φ_old - hi = 1.0 - for _ in 1:100 - mid = 0.5 * (lo + hi) - if tempered_inefficiency(mid, φ_old, d, n_particles) < r_star - lo = mid - else - hi = mid - end - hi - lo < 1e-8 && break - end - return 0.5 * (lo + hi) +function GuidedProposal(nObs::Int, nExo::Int) + return GuidedProposal(Matrix{Float64}(undef, nExo, nExo), Matrix{Float64}(undef, nExo, nExo), + Matrix{Float64}(undef, nExo, nExo), Matrix{Float64}(undef, nExo, nObs), + Matrix{Float64}(undef, nObs, nExo), Matrix{Float64}(undef, nObs, nExo), + Matrix{Float64}(undef, nExo, nExo), 0.0, 0) end -function run_particle_filter(::Val{algo}, - ::Val{:tempered}, - observables_index::Vector{Int}, - 𝐒, - data_in_deviations::AbstractMatrix, - constants::constants, - state, - 𝓂::ℳ, - measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, - obs_idx_per_t::Vector{Vector{Int}}, - has_missing::Bool; - n_particles::Int = DEFAULT_N_PARTICLES, - particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, - particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, - particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, - particle_rng::Random.AbstractRNG = Random.default_rng(), - presample_periods::Int = 0, - initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, - on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, - opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} - T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) - - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - - r_star = Float64(tempering_target_ratio) - c = Float64(tempering_mh_scale) - n_mh = tempering_mh_steps - max_stages = tempering_max_stages - - past_idx = T.past_not_future_and_mixed_idx - 𝐒f = [Matrix{Float64}(S) for S in 𝐒] - scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) - - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - - # Double-buffered particle pools (ancestors and states + resample scratch). - anc = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars)::particle_pool_type(Val(algo)) - return tempered_higher_loop(Val(algo), anc, 𝐒f, scr, past_idx, nVars, nExo, nT, - presample_periods, observables_index, data_in_deviations, - obs_idx_per_t, has_missing, me_var, resampling, r_star, c, - n_mh, max_stages, rng, on_failure_loglikelihood, log2pi) -end - -# Function barrier for the tempered loop (see `bootstrap_higher_loop`). -function tempered_higher_loop(::Val{algo}, anc0, 𝐒f, scr, past_idx, nVars, nExo, nT, - presample_periods, observables_index, data_in_deviations, - obs_idx_per_t, has_missing, me_var, resampling, r_star, c, - n_mh, max_stages, rng, on_failure_loglikelihood, log2pi) where {algo} - # `anc0` is captured (read-only) by the pool comprehensions below; the pools - # that get swapped are separate locals, so nothing captured is ever reassigned - # (which would force Julia to `Core.Box` it and make the loop type-unstable). - anc0 = anc0::particle_pool_type(Val(algo)) - n_particles = length(anc0) - anc = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] - st = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] - anc2 = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] - st2 = [zeros_like_particle(anc0[1]) for _ in 1:n_particles] - @inbounds for p in 1:n_particles - copy_particle!(anc[p], anc0[p]) - end - sh = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] - sh2 = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] - dv = Vector{Float64}(undef, n_particles) - dv2 = Vector{Float64}(undef, n_particles) - - sprop = zeros_like_particle(anc0[1]) - eprop = Vector{Float64}(undef, nExo) - full_buf = Vector{Float64}(undef, nVars) - logw = Vector{Float64}(undef, n_particles) - Wn = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) - loglik = 0.0 - - for t in 1:nT - rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) - data_col = @view data_in_deviations[:, t] - d_obs = length(rows) - - # Bootstrap proposal: propagate every ancestor (`anc`) with a fresh shock. - @inbounds for p in 1:n_particles - Random.randn!(rng, sh[p]) - end - @inbounds for p in 1:n_particles - higher_propagate!(Val(algo), st[p], anc[p], sh[p], past_idx, 𝐒f, scr) - dv[p] = particle_quadratic_form(measurement_full(st[p], full_buf), data_col, observables_index, me_var, rows) - end - if all(!isfinite, dv) - return Float64(on_failure_loglikelihood) - end - - period_ll = 0.0 - φ_old = 0.0 - stage = 0 - while φ_old < 1.0 - 1e-12 && stage < max_stages - stage += 1 - φ_new = tempered_next_phi(φ_old, dv, r_star, n_particles) +# The live blocks. `K` is what the filters multiply the residual by, so it is the +# one that has to be sliced at every use. +@inline proposal_K(gp::GuidedProposal) = view(gp.K, :, 1:gp.d) - if φ_old == 0.0 - logZ = particle_measurement_logZ(me_var, rows, log2pi) - @inbounds for p in 1:n_particles - logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dv[p] - end - else - lr = 0.5 * d_obs * (log(φ_new) - log(φ_old)) - @inbounds for p in 1:n_particles - logw[p] = lr - 0.5 * (φ_new - φ_old) * dv[p] - end - end - - m = maximum(logw) - if !isfinite(m) - return Float64(on_failure_loglikelihood) - end - s = 0.0 - @inbounds for p in 1:n_particles - s += exp(logw[p] - m) - end - if s <= 0 || !isfinite(s) - return Float64(on_failure_loglikelihood) - end - period_ll += m + log(s) - log(n_particles) - - logsw = m + log(s) - @inbounds for p in 1:n_particles - Wn[p] = exp(logw[p] - logsw) - end - particle_resample_indices!(idx, bins, rng, Wn, resampling) - @inbounds for j in 1:n_particles - a = idx[j] - copy_particle!(anc2[j], anc[a]); copyto!(sh2[j], sh[a]); copy_particle!(st2[j], st[a]); dv2[j] = dv[a] - end - anc, anc2 = anc2, anc - sh, sh2 = sh2, sh - st, st2 = st2, st - dv, dv2 = dv2, dv - - # Mutation: random-walk Metropolis on the shocks, targeting the - # stage-φ posterior π(ε) ∝ N(ε;0,I) · exp(-φ/2 · e(ε)ᵀH⁻¹e(ε)). - @inbounds for p in 1:n_particles - shp = sh[p] - for _ in 1:n_mh - Random.randn!(rng, eprop) - esq_old = 0.0 - esq_new = 0.0 - for e in 1:nExo - ep = shp[e] + c * eprop[e] - eprop[e] = ep - esq_new += ep * ep - esq_old += shp[e] * shp[e] - end - higher_propagate!(Val(algo), sprop, anc[p], eprop, past_idx, 𝐒f, scr) - dprop = particle_quadratic_form(measurement_full(sprop, full_buf), data_col, observables_index, me_var, rows) - logα = -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) - if log(rand(rng)) < logα - copyto!(shp, eprop) - copy_particle!(st[p], sprop) - dv[p] = dprop - end - end - end - - φ_old = φ_new - end - - if t > presample_periods - loglik += period_ll - end - # Carry the filtered states forward as next period's ancestors (copy so the - # `anc`/`st` pool identities stay stable for inference). - @inbounds for p in 1:n_particles - copy_particle!(anc[p], st[p]) - end +# How many Newton steps refine the proposal's centre +# (`DEFAULT_GUIDED_NEWTON_STEPS`). +# +# `μ = K·r(0)` is the mode only when the observed transition is linear in the +# shock. At pruned second order it is not, and a mis-centred proposal in seven +# dimensions against a target this tight is expensive. Each Newton step +# re-evaluates the *true* residual at the current centre and moves towards the mode +# of the exact conditional, +# ε ← ε + M⁻¹(BₒᵀH⁻¹r(ε) - ε), +# which is the Gauss-Newton iteration on -½‖ε‖² - ½r(ε)ᵀH⁻¹r(ε). Finding the mode +# and sampling around it with the Laplace covariance is the implicit particle +# filter of Chorin, Morzfeld & Tu (2010). +# +# Two is measured to be the right number on mid-sized models, and the reason it is +# not a speed/accuracy trade-off is worth knowing: a Newton step costs one batched +# transition, but so does a bridging stage, and a badly centred proposal needs more +# bridging stages. Skipping the refinement therefore buys no time — it just moves +# the same work somewhere less useful. Going from none to two improves the +# estimates and gets slightly *faster*; past two the centre has stopped moving and +# the extra transitions are wasted. +# +# Relation to the inversion filter. +# This iteration is the inversion filter's shock-finding problem with the shock +# prior left in. The inversion filter solves r(ε) = 0 — the shock that reproduces +# the observation exactly — while this solves the regularised version, maximising +# -½‖ε‖² - ½r(ε)ᵀH⁻¹r(ε): the shock that best explains the observation *and* is +# plausible under its own N(0,I). Drop the prior and let the measurement error +# vanish and the two coincide, because M = I + BₒᵀH⁻¹Bₒ → BₒᵀH⁻¹Bₒ and +# K = M⁻¹BₒᵀH⁻¹ → Bₒ⁺. Checked numerically: ‖K·r - Bₒ⁺r‖ falls as O(σ²) — +# 2e-1, 2e-3, 2e-7 at σ² = 1e-2, 1e-4, 1e-8 — for square Bₒ and for both +# rectangular shapes. At first order the transition is linear in ε, μ = K·r(0) is +# already the mode, and no Newton step moves it. +# +# The starting point is ε = 0 for every particle: one transition with no shock +# gives r(0) = yₜ - ŷ(xₜ₋₁ᵖ, 0), the part of the observation this ancestor cannot +# explain by itself, and μ = K·r(0) is the linear-Gaussian answer to it. Note the +# *per particle*: each ancestor gets its own residual and its own centre, which is +# what a proposal has to be and what a single inversion solve is not. +# +# It does not fail the way the inversion filter fails. `find_shocks` returns +# `matched = false` — and the likelihood becomes `on_failure_loglikelihood`, +# usually -Inf — when the Jacobian is singular, when there are fewer shocks than +# observables so no exact solution exists, or when its Newton iteration does not +# converge. None of those apply here: M is positive definite by construction +# (that is what the `I` from the prior buys), so this solve always succeeds, at +# any shape, and a period no shock can reproduce exactly is an ordinary period +# with a nonzero residual rather than a failure. What degrades instead is +# efficiency — a badly located mode gives heavy-tailed weights and a low ESS, +# which is what the bridging above is for. The guided filter does bail to +# `on_failure_loglikelihood`, but on a different condition: a non-finite +# incremental weight in the bridge, meaning the *whole cloud* scored zero against +# the observation. That is degeneracy of the sample, not insolvability of a +# system, and more particles or more bridging stages address it. +# +# Which is also why the cheap-inversion-then-the-rest route is not taken. Beyond +# needing measurement error to be zero and shocks to outnumber observables, a +# particle filter needs a density to draw from and evaluate, not a point: the +# covariance M⁻¹ would have to be built anyway, and it is the expensive part. +# M depends only on 𝐒₁ and H, so it is common to the whole cloud and factorised +# once per missing-data pattern; the per-particle work is then two batched gemms. +# N independent nonlinear inversion solves per period, one per ancestor, would +# give exactly that up. And a per-particle failure would not be neutral — it +# would drop the ancestors whose states make inversion hard, which is selection +# on the state, not on the data. + +# Width of the proposal, as a multiple of the Laplace scale +# (`DEFAULT_GUIDED_PROPOSAL_SCALE`). Kept at one, and the measurement that says so +# is worth recording. +# +# `M⁻¹` is the curvature of a *linearised* problem, so it is a guess at the true +# conditional's spread. Importance sampling is not symmetric in that error — a +# proposal wider than the target has bounded weights, one narrower can have infinite +# weight variance — so deliberately over-dispersing looks like cheap insurance +# against the crisis periods where this filter degenerates (Hesterberg's defensive +# importance sampling, 1995). Measured on the euro-area problem it is not: at scales +# 1.0 / 1.5 / 2.5 the seed dispersion went 0.091 / 0.090 / 0.138 and the mean weight +# ESS fell 0.235 / 0.174 / 0.015, while the *worst* period's effective sample size +# stayed at one particle in four thousand at every scale. +# +# That last number is the informative one: widening the proposal does not find the +# missing mass at all. The crisis-period failure is that the conditional's mode is +# somewhere the Gaussian approximation does not put it, not that the Gaussian is too +# narrow — so it wants a remedy that *relocates* (annealing from this proposal to the +# true conditional, or a block move over several periods), not one that inflates. + +# Observed rows of the first-order shock loading, into the proposal's own buffer. +function observed_shock_loading!(Bo::AbstractMatrix{Float64}, S₁::AbstractMatrix, + observables_index::Vector{Int}, nPast::Int, rows) + @inbounds for (k, r) in enumerate(rows) + Bo[k, :] .= @view S₁[observables_index[r], nPast+1:end] end - - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) + return Bo end - -# ── Optimised first-order (linear) fast paths ──────────────────────────────── -# For the linear state space the transition is xₜ = A·xₜ₋₁[past] + B·εₜ. These -# `::Val{:first_order}` methods replace the type-unstable, per-call-allocating -# `state_update` closure with a typed, BLAS-backed (`mul!`), fully preallocated -# implementation. Particle pools are double-buffered so resampling and the -# tempered Metropolis mutation run in place, with no heap allocation in the hot -# loop. (Higher orders use the generic methods above.) Buffer-reuse for the -# resampling index/cumulative arrays follows LowLevelParticleFilters.jl. - -# In-place resampling: ancestor indices are written into `idx`; `bins` is a -# cumulative-weight scratch used by the multinomial/residual schemes. -function systematic_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) - N = length(W) - u0 = rand(rng) / N - c = W[1]; i = 1 - @inbounds for j in 1:N - u = u0 + (j - 1) / N - while u > c && i < N; i += 1; c += W[i]; end - idx[j] = i +# H⁻¹Bₒ, for either measurement-error form, written into `HinvBo`. Returns log|H|. +function guided_hinv_loading!(HinvBo::AbstractMatrix{Float64}, Bo::AbstractMatrix{Float64}, + me_var::AbstractVector, rows) + @inbounds for (k, r) in enumerate(rows) + HinvBo[k, :] .= @view(Bo[k, :]) ./ me_var[r] end - return idx + return sum(log(me_var[r]) for r in rows) end -function stratified_resample_indices!(idx::Vector{Int}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) - N = length(W) - c = W[1]; i = 1 - @inbounds for j in 1:N - u = (j - 1 + rand(rng)) / N - while u > c && i < N; i += 1; c += W[i]; end - idx[j] = i - end - return idx +function guided_hinv_loading!(HinvBo::AbstractMatrix{Float64}, Bo::AbstractMatrix{Float64}, + me::DenseMeasurementError, rows) + me_sync!(me, rows) + Lo = ℒ.LowerTriangular(me.last_L) + copyto!(HinvBo, Bo) + ℒ.ldiv!(Lo, HinvBo) # L⁻¹Bₒ + ℒ.ldiv!(Lo', HinvBo) # H⁻¹Bₒ = L⁻ᵀL⁻¹Bₒ + return me.last_logdet end -function multinomial_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) - N = length(W) - cumsum!(bins, W); bins[N] = one(eltype(bins)) - @inbounds for j in 1:N - idx[j] = searchsortedfirst(bins, rand(rng)) - end - return idx +# Refill `gp` for the missing-data pattern `rows`, reusing its buffers throughout. +# +# The solve is a Cholesky of M = I + BₒᵀH⁻¹Bₒ, which is nExo × nExo — one row and +# column per structural shock. `cholesky!`/`ldiv!` are the direct LAPACK calls at +# that size; routing them through a `LinearSolve` cache the way the +# stochastic-steady-state Newton solves do would add the cache indirection without +# reaching a different kernel, so it is deliberately not done here. +function rebuild_guided_proposal!(gp::GuidedProposal, S₁::AbstractMatrix, + observables_index::Vector{Int}, nPast::Int, + me_var, rows, log2pi::Float64) + d = length(rows) + gp.d = d + Bo = view(gp.Bo, 1:d, :) + HinvBo = view(gp.HinvBo, 1:d, :) + + observed_shock_loading!(Bo, S₁, observables_index, nPast, rows) + logdetH = guided_hinv_loading!(HinvBo, Bo, me_var, rows) + + copyto!(gp.M, ℒ.I) + ℒ.mul!(gp.M, Bo', HinvBo, 1.0, 1.0) # M = I + BₒᵀH⁻¹Bₒ + F = ℒ.cholesky!(ℒ.Symmetric(gp.M)) # overwrites gp.M with its factor + + copyto!(gp.U, F.U) + logdetM = 2 * sum(log, ℒ.diag(gp.U)) + + copyto!(gp.Uinv, ℒ.I) + ℒ.ldiv!(ℒ.UpperTriangular(gp.U), gp.Uinv) # U⁻¹, so Uinv·Uinvᵀ = M⁻¹ + ℒ.mul!(gp.Minv, gp.Uinv, gp.Uinv') + + K = view(gp.K, :, 1:d) + ℒ.transpose!(K, HinvBo) + ℒ.ldiv!(F, K) # K = M⁻¹BₒᵀH⁻¹ + + gp.logZ = -0.5 * (d * log2pi + logdetH + logdetM) + return gp end -function residual_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}) - N = length(W) - k = 0 - @inbounds for i in 1:N - ni = floor(Int, N * W[i]) - for _ in 1:ni; k += 1; idx[k] = i; end +# Annealing from the proposal to the truth. +# +# The guided proposal is a Gaussian approximation, and in the periods this model +# can barely explain it is the wrong Gaussian — not too narrow (widening it was +# measured and does not help) but centred somewhere the true conditional's mass is +# not. A single importance-weighting step then has heavy-tailed weights and the +# cloud collapses onto a handful of particles however many were started with. +# +# The fix is to reach the truth gradually instead of in one step. Bridge +# +# γ_β(ε) ∝ q(ε)^(1-β) · π̃(ε)^β , β: 0 → 1, π̃(ε) = N(ε;0,I)·N(yₜ; ŷ(ε), H), +# +# reweighting, resampling and mutating along the way. At β = 0 the particles are +# exact draws from q, at β = 1 they target the conditional. Writing +# +# L(ε) = log π̃(ε) - log q(ε) = logZ - ½‖ε‖² - ½·e(ε)ᵀH⁻¹e(ε) + ½(ε-μ)ᵀM(ε-μ), +# +# the incremental weight between two levels is exp((β' - β)·L), so the same +# inefficiency-targeting schedule the tempered filter uses picks the steps — with +# `-L` in place of its quadratic form. The one-step case is exactly the plain +# guided filter, so this is a strict generalisation: where the proposal is good the +# schedule jumps straight to β = 1 and costs nothing extra, and only the awkward +# periods pay for more stages. +# +# This is annealed importance sampling (Neal, 2001) started from a Laplace +# approximation rather than from the prior, which is how SMC samplers are usually +# initialised; the difference from the tempered filter is only *what* it bridges +# from, and that is what makes it cheap. + +# ‖U(ε - μ)‖² = (ε-μ)ᵀM(ε-μ) for column `p`, with `U` the upper Cholesky factor of M. +@inline function mahalanobis_M(E::Matrix{Float64}, Mu::Matrix{Float64}, U::Matrix{Float64}, p::Int) + n = size(E, 1) + acc = 0.0 + @inbounds for i in 1:n + v = 0.0 + for j in i:n + v += U[i, j] * (E[j, p] - Mu[j, p]) + end + acc += v * v end - R = N - k - if R > 0 - s = 0.0 - @inbounds for i in 1:N - bins[i] = N * W[i] - floor(N * W[i]); s += bins[i] - end - if s <= 0 - cumsum!(bins, W) - else - @inbounds for i in 1:N; bins[i] /= s; end - cumsum!(bins, bins) + return acc +end + +# L(ε) for every particle, given the measurement quadratic forms `dv`. +function guided_bridge_gap!(Lvec::Vector{Float64}, E::Matrix{Float64}, Mu::Matrix{Float64}, + dv::Vector{Float64}, gp::GuidedProposal) + nExo = size(E, 1) + @inbounds for p in eachindex(Lvec) + d = dv[p] + if !isfinite(d) + Lvec[p] = -Inf + continue end - bins[N] = one(eltype(bins)) - @inbounds for _ in 1:R - k += 1; idx[k] = searchsortedfirst(bins, rand(rng)) + esq = 0.0 + for e in 1:nExo + esq += E[e, p] * E[e, p] end + Lvec[p] = gp.logZ - 0.5 * esq - 0.5 * d + 0.5 * mahalanobis_M(E, Mu, gp.U, p) end - return idx + return Lvec end -@inline function particle_resample_indices!(idx::Vector{Int}, bins::Vector{Float64}, rng::Random.AbstractRNG, W::AbstractVector{<:Real}, scheme::Symbol) - if scheme == :systematic - systematic_resample_indices!(idx, rng, W) - elseif scheme == :stratified - stratified_resample_indices!(idx, rng, W) - elseif scheme == :multinomial - multinomial_resample_indices!(idx, bins, rng, W) - elseif scheme == :residual - residual_resample_indices!(idx, bins, rng, W) - else - error("Unknown resampling scheme `:$scheme`. Choose from `:systematic`, `:stratified`, `:multinomial`, `:residual`.") +# One Metropolis sweep against γ_β, preconditioned by M⁻¹ (the same factor the +# proposal uses, which is the right shape at both ends of the bridge). Returns the +# acceptance rate. +function guided_anneal_mutate!(::Val{algo}, tr, scr, gp, St::NTuple{K,Matrix{Float64}}, + parts_proposed::NTuple{K,Matrix{Float64}}, anc::NTuple{K,Matrix{Float64}}, + Fbuf, E, Eprop, Z, R, Mu, dv, dprop, accept, + c::Float64, β::Float64, data_col, observables_index, + inv_me_var, rows, rng) where {algo, K} + n_particles = size(E, 2) + nExo = size(E, 1) + + Random.randn!(rng, Z) + ℒ.mul!(Eprop, gp.Uinv, Z, c, 0.0) # ε' - ε = c·U⁻¹z + @inbounds for i in eachindex(Eprop) + Eprop[i] += E[i] end - return idx -end -# Typed, preallocated first-order transition xₜ = A·xₜ₋₁[past] + B·εₜ. -# Particles are stored as the columns of an nVars × N matrix so that the whole -# swarm is propagated with two BLAS gemm calls (Xₜ = A·Xₜ₋₁ + B·Eₜ) instead of -# N small gemv calls. `A` is the full nVars × nVars one-step transition (zero -# outside the predetermined-state columns), `B` the nVars × nExo shock loading. -struct LinearParticleTransition - A::Matrix{Float64} - B::Matrix{Float64} -end + propagate_cloud!(Val(algo), tr, scr, parts_proposed, anc, Eprop) + residual_cloud!(R, full_states!(Fbuf, parts_proposed), data_col, observables_index, rows) -function build_linear_particle_transition(𝐒::AbstractMatrix, T) - nVars = T.nVars - nPast = T.nPast_not_future_and_mixed - S₁ = Matrix{Float64}(𝐒) - A = zeros(Float64, nVars, nVars) - @views A[:, T.past_not_future_and_mixed_idx] .= S₁[:, 1:nPast] - B = Matrix{Float64}(@view S₁[:, nPast+1:end]) - return LinearParticleTransition(A, B) -end + accepted = 0 + @inbounds for p in 1:n_particles + # log γ_β = (1-β)·log q + β·log π̃, so the ratio needs both ends of the + # bridge at both shocks: the proposal's Mahalanobis form under M for q, + # and ‖ε‖² together with the measurement quadratic form for π̃. + measurement_proposed = residual_quadform(R, p, inv_me_var, rows) + dprop[p] = measurement_proposed + isfinite(measurement_proposed) || continue + shock_norm²_current = 0.0 + shock_norm²_proposed = 0.0 + for e in 1:nExo + current = E[e, p] + proposed = Eprop[e, p] + shock_norm²_current += current * current + shock_norm²_proposed += proposed * proposed + end + proposal_form_current = mahalanobis_M(E, Mu, gp.U, p) + proposal_form_proposed = mahalanobis_M(Eprop, Mu, gp.U, p) + logα = -0.5 * (1 - β) * (proposal_form_proposed - proposal_form_current) - + 0.5 * β * (shock_norm²_proposed - shock_norm²_current) - + 0.5 * β * (measurement_proposed - dv[p]) + acc = log(rand(rng)) < logα + accept[p] = acc + if acc + dv[p] = measurement_proposed + accepted += 1 + else + accept[p] = false + end + end -# X₂ = A·X + B·E for the whole swarm at once (two gemm). Columns are particles. -@inline function propagate_batch!(X2::Matrix{Float64}, tr::LinearParticleTransition, X::Matrix{Float64}, E::Matrix{Float64}) - ℒ.mul!(X2, tr.A, X) - ℒ.mul!(X2, tr.B, E, 1.0, 1.0) - return X2 + @inbounds for p in 1:n_particles + if accept[p] + copy_col!(E, p, Eprop, p) + copy_cloud_col!(St, p, parts_proposed, p) + end + end + + return accepted / n_particles end -# Base = A·Anc (shock-independent part of the transition, one gemm; reused across -# Metropolis proposals which only vary the shock B·E term). -@inline function base_batch!(Base::Matrix{Float64}, tr::LinearParticleTransition, Anc::Matrix{Float64}) - ℒ.mul!(Base, tr.A, Anc) - return Base +# yₜ - (predicted observables) for every column of `F`, into `R` (d × N). +function residual_cloud!(R::Matrix{Float64}, F::Matrix{Float64}, data_col, observables_index, rows) + @inbounds for p in axes(R, 2) + for k in eachindex(rows) + r = rows[k] + f = F[observables_index[r], p] + R[k, p] = isfinite(f) ? data_col[r] - f : NaN + end + end + return R end -# Quadratic form eᵀH⁻¹e over the observed rows for particle column `p`. -@inline function linear_quadform_col(X::Matrix{Float64}, p::Int, data_col, observables_index, inv_me_var, rows) +# rᵀH⁻¹r for column `p` of a residual block. +@inline function residual_quadform(R::Matrix{Float64}, p::Int, inv_me_var::AbstractVector, rows) q = 0.0 @inbounds for k in eachindex(rows) - r = rows[k] - f = X[observables_index[r], p] - isfinite(f) || return Inf - v = data_col[r] - f - q += v * v * inv_me_var[r] + v = R[k, p] + isfinite(v) || return Inf + q += v * v * inv_me_var[rows[k]] end return q end -# Same, for a correlated H: gather the innovation, then one triangular solve. -@inline function linear_quadform_col(X::Matrix{Float64}, p::Int, data_col, observables_index, - me::DenseMeasurementError, rows) +@inline function residual_quadform(R::Matrix{Float64}, p::Int, me::DenseMeasurementError, rows) me_sync!(me, rows) v = me.buf @inbounds for k in eachindex(rows) - r = rows[k] - f = X[observables_index[r], p] - isfinite(f) || return Inf - v[k] = data_col[r] - f + x = R[k, p] + isfinite(x) || return Inf + v[k] = x end return dense_me_quadform!(v, me.last_L) end -# Copy column `src` of `X` into column `dst` of `Y` (contiguous, allocation-free). -@inline function copy_col!(Y::Matrix{Float64}, dst::Int, X::Matrix{Float64}, src::Int) - @inbounds for i in axes(X, 1) - Y[i, dst] = X[i, src] +# One Gauss-Newton step towards the mode: μ ← μ + M⁻¹(BₒᵀH⁻¹r(μ) - μ), batched. +# `R` holds the residuals at the current `Mu`; `Tmp` is nExo × N scratch. +# +# This does not need as many observables as shocks, or the reverse. The step is +# taken in shock space throughout: `K` is nExo × d and `M` is nExo × nExo however +# many observables `d` are live. Nor can it break down when d < nExo — M is +# I + BₒᵀH⁻¹Bₒ, a positive-definite matrix plus the identity, so it is invertible +# even when Bₒ has a large null space and the observation pins down only some +# directions of the shock. The unidentified directions simply keep their prior: +# where Bₒ says nothing, M is the identity there and the step leaves μ at zero. +# That is exactly the case the inversion filter cannot handle at all, and it is +# the reason the particle filters accept fewer shocks than observables as well as +# more. +function guided_newton_step!(Mu::Matrix{Float64}, R::Matrix{Float64}, Tmp::Matrix{Float64}, + gp::GuidedProposal) + ℒ.mul!(Tmp, proposal_K(gp), view(R, 1:gp.d, :)) # M⁻¹BₒᵀH⁻¹r(μ) + ℒ.mul!(Tmp, gp.Minv, Mu, -1.0, 1.0) # ... - M⁻¹μ + @inbounds for i in eachindex(Mu) + Mu[i] += Tmp[i] end - return Y + return Mu end -# Draw the initial cloud into the columns of X (nVars × N): X = mean0 .+ L·Z. -function init_linear_particles!(X::Matrix{Float64}, rng::Random.AbstractRNG, - mean0::AbstractVector{Float64}, L::Matrix{Float64}, Z::Matrix{Float64}) - Random.randn!(rng, Z) - ℒ.mul!(X, L, Z) - @inbounds for p in axes(X, 2), i in axes(X, 1) - X[i, p] += mean0[i] +# The first stage's predictive density, by Laplace approximation at the mode: +# p(yₜ|xₜ₋₁) ≈ (2π)^(-d/2)|H|^(-1/2)|M|^(-1/2)·exp(-½‖μ‖² - ½r(μ)ᵀH⁻¹r(μ)). +# `R` must hold the residuals evaluated at `Mu`. When the transition is linear in +# the shock this reduces exactly to N(yₜ; mₚ, H + BₒBₒᵀ). +# +# Where the Laplace approximation sits in the algorithm, and where it does not. +# It does two jobs, and the reported likelihood is not one of them: +# +# 1. It shapes the proposal. Expanding log p(εₜ|xₜ₋₁,yₜ) to second order about +# its mode gives a Gaussian with mean μ (the mode `guided_newton_step!` +# refines towards) and covariance M⁻¹. That Gaussian is q, the distribution +# the shocks are actually drawn from. A wrong Laplace approximation makes q +# a poor fit — more bridging stages, a lower ESS — but never a wrong answer. +# +# 2. It is the λ this function returns, kept only as a diagnostic. λ appears in +# both the numerator and the denominator of the importance weight and +# divides out exactly (see the derivation above `GuidedProposal`), which is +# what stops an over-confident approximation from steering the resampling. +# +# The likelihood the filter reports is the ordinary particle-filter estimate: +# log of the weighted average of the exact p(yₜ|xₜᵖ) over the cloud, accumulated +# across the bridging stages. Nothing in it is Gaussian by assumption. So the +# approximation controls the filter's *efficiency*, and its errors show up as +# variance rather than as bias. +function guided_lambda!(logλ::Vector{Float64}, Mu::Matrix{Float64}, R::Matrix{Float64}, + gp::GuidedProposal, inv_me_var, rows) + nExo = size(Mu, 1) + @inbounds for p in eachindex(logλ) + qμ = residual_quadform(R, p, inv_me_var, rows) + if !isfinite(qμ) + logλ[p] = -Inf + continue + end + msq = 0.0 + for e in 1:nExo + msq += Mu[e, p] * Mu[e, p] + end + logλ[p] = gp.logZ - 0.5 * msq - 0.5 * qμ end - return X + return logλ end -function run_particle_filter(::Val{:first_order}, - ::Val{:bootstrap}, +function run_particle_filter(::Val{algo}, + ::Val{:guided}, observables_index::Vector{Int}, 𝐒, data_in_deviations::AbstractMatrix, @@ -1401,224 +1809,409 @@ function run_particle_filter(::Val{:first_order}, presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, - opts::CalculationOptions = merge_calculation_options())::Float64 + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_GUIDED_MH_STEPS, + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) - - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - inv_me_var = me_inverse_diagonal(me_var) - - tr = build_linear_particle_transition(𝐒, T) - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) + nT = size(data_in_deviations, 2) + + me_var, inv_me_var, tr, scr, L, pws = + particle_filter_setup(Val(algo), 𝐒, T, 𝓂, measurement_error, n_particles, + particle_initial_state_scaling, initial_covariance, opts) + + K = pruned_components(Val(algo)) + nK = n_components(K) + pools = ensure_particle_pools!(pws, 4 * nK + 1) + X = cloud_group(pools, 1, K) + X_scratch = cloud_group(pools, 2, K) + anc = cloud_group(pools, 3, K) + anc_scratch = cloud_group(pools, 4, K) + Fbuf = pools[4 * nK + 1] + + init_cloud!(X, particle_rng, state, L, Fbuf) + + return guided_loop!(Val(algo), tr, scr, X, X_scratch, anc, anc_scratch, Fbuf, pws.E, pws.E2, pws.Eprop, + pws.W, pws.logdens, pws.logw, pws.lam, pws.idx, pws.bins, nT, + normalize_presample_periods(presample_periods, nT), + observables_index, data_in_deviations, obs_idx_per_t, has_missing, + me_var, inv_me_var, + 𝓂.caches.first_order_solution_matrix, T.nPast_not_future_and_mixed, + particle_resampling, Float64(particle_resampling_threshold), + particle_mh_steps, Float64(particle_mh_scale), + Float64(particle_target_ratio), particle_max_stages, particle_rng, + Float64(on_failure_loglikelihood), log(2π)) +end - # Cloud and scratch come from the model's workspace, so a sampler that calls - # this thousands of times pays for them once (see `ensure_particle_workspace!`). - pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) - X, X2, Z = pws.X, pws.X2, pws.Anc - E = pws.E - W, logdens, idx, bins = pws.W, pws.logdens, pws.idx, pws.bins +function guided_loop!(::Val{algo}, tr, scr, X::NTuple{K,Matrix{Float64}}, X_scratch::NTuple{K,Matrix{Float64}}, + anc::NTuple{K,Matrix{Float64}}, anc_scratch::NTuple{K,Matrix{Float64}}, + Fbuf, E, Mu, Tmp, W, logλ, logw, lam, + idx, bins, nT, presample_periods, observables_index, data_in_deviations, + obs_idx_per_t, has_missing, me_var, inv_me_var, S₁, nPast, resampling, + resampling_threshold, n_mh, mh_scale, r_star, max_stages, + rng, on_failure_loglikelihood, log2pi) where {algo, K} + n_particles = size(E, 2) + nExo = size(E, 1) + R = Matrix{Float64}(undef, length(observables_index), n_particles) + Z = Matrix{Float64}(undef, nExo, n_particles) + Eprop = Matrix{Float64}(undef, nExo, n_particles) + mu_scratch = Matrix{Float64}(undef, nExo, n_particles) + dv = Vector{Float64}(undef, n_particles) + dprop = Vector{Float64}(undef, n_particles) + Lvec = Vector{Float64}(undef, n_particles) + negL = Vector{Float64}(undef, n_particles) + accept = Vector{Bool}(undef, n_particles) + c = mh_scale fill!(W, 1.0 / n_particles) + loglik = 0.0 - mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) - init_linear_particles!(X, rng, mean0, L, Z) + gp_rows = Int[] + gp = GuidedProposal(length(observables_index), nExo) + rebuild_guided_proposal!(gp, S₁, observables_index, nPast, me_var, eachindex(observables_index), log2pi) - loglik = 0.0 for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view data_in_deviations[:, t] - Random.randn!(rng, E) - propagate_batch!(X2, tr, X, E) - X, X2 = X2, X - - isempty(rows) && continue - - logZ = particle_measurement_logZ(me_var, rows, log2pi) - @inbounds for p in 1:n_particles - logdens[p] = logZ - 0.5 * linear_quadform_col(X, p, data_col, observables_index, inv_me_var, rows) + if isempty(rows) + Random.randn!(rng, E) + propagate_cloud!(Val(algo), tr, scr, X_scratch, X, E) + X, X_scratch = X_scratch, X + continue end - - m = maximum(logdens) - if !isfinite(m) - return Float64(on_failure_loglikelihood) + if !same_rows(gp_rows, rows) + rebuild_guided_proposal!(gp, S₁, observables_index, nPast, me_var, rows, log2pi) + gp_rows = collect(Int, rows) end - s = 0.0 - @inbounds for p in 1:n_particles - s += W[p] * exp(logdens[p] - m) + + # Solve for the shock that best explains yₜ from each ancestor: one + # transition at ε = 0 for the residual, then Gauss-Newton steps that use the + # true residual rather than the linearisation. + fill!(E, 0.0) + propagate_cloud!(Val(algo), tr, scr, X_scratch, X, E) + residual_cloud!(R, full_states!(Fbuf, X_scratch), data_col, observables_index, rows) + ℒ.mul!(Mu, proposal_K(gp), view(R, 1:length(rows), :)) + for _ in 1:DEFAULT_GUIDED_NEWTON_STEPS + propagate_cloud!(Val(algo), tr, scr, X_scratch, X, Mu) + residual_cloud!(R, full_states!(Fbuf, X_scratch), data_col, observables_index, rows) + guided_newton_step!(Mu, R, Tmp, gp) end - if s <= 0 || !isfinite(s) - return Float64(on_failure_loglikelihood) + + # Draw from the conditional, εⱼ = μⱼ + U⁻¹zⱼ, and weight. + Random.randn!(rng, Z) + ℒ.mul!(E, gp.Uinv, Z, DEFAULT_GUIDED_PROPOSAL_SCALE, 0.0) + @inbounds for i in eachindex(E) + E[i] += Mu[i] end + copy_cloud!(anc, X) + propagate_cloud!(Val(algo), tr, scr, X_scratch, anc, E) + copy_cloud!(X, X_scratch) + residual_cloud!(R, full_states!(Fbuf, X), data_col, observables_index, rows) - ll_t = m + log(s) - if t > presample_periods - loglik += ll_t + @inbounds for j in 1:n_particles + dv[j] = residual_quadform(R, j, inv_me_var, rows) end + guided_bridge_gap!(Lvec, E, Mu, dv, gp) - @inbounds for p in 1:n_particles - W[p] = W[p] * exp(logdens[p] - m) / s + # Walk from the proposal to the conditional; one step where the proposal is + # good, more only where it is not. + ll_t = 0.0 + β_old = 0.0 + stage = 0 + @inbounds for j in 1:n_particles + negL[j] = -Lvec[j] end + while β_old < 1.0 - 1e-12 && stage < max_stages + stage += 1 + β_new = any(isfinite, negL) ? tempered_next_phi(β_old, negL, r_star, n_particles) : 1.0 + @inbounds for j in 1:n_particles + logw[j] = (β_new - β_old) * Lvec[j] + end + inc = reweight_log_weights!(W, logw) + isfinite(inc) || return on_failure_loglikelihood + ll_t += inc + + if effective_sample_size(W) < resampling_threshold * n_particles + particle_resample_indices!(idx, bins, rng, W, resampling) + gather_cloud!(X_scratch, X, idx) + copy_cloud!(X, X_scratch) + gather_cloud!(anc_scratch, anc, idx) + copy_cloud!(anc, anc_scratch) + @inbounds for j in 1:n_particles + a = idx[j] + copy_col!(Eprop, j, E, a) + copy_col!(mu_scratch, j, Mu, a) + dprop[j] = dv[a] + end + copyto!(E, Eprop) + copyto!(Mu, mu_scratch) + copyto!(dv, dprop) + fill!(W, 1.0 / n_particles) + end - if effective_sample_size(W) < resampling_threshold * n_particles - particle_resample_indices!(idx, bins, rng, W, resampling) + for _ in 1:n_mh + acc = guided_anneal_mutate!(Val(algo), tr, scr, gp, X, X_scratch, anc, Fbuf, + E, Eprop, Z, R, Mu, dv, dprop, accept, c, β_new, + data_col, observables_index, inv_me_var, rows, rng) + c = adapt_mh_scale(c, acc) + end + guided_bridge_gap!(Lvec, E, Mu, dv, gp) @inbounds for j in 1:n_particles - copy_col!(X2, j, X, idx[j]) + negL[j] = -Lvec[j] end - X, X2 = X2, X - fill!(W, 1.0 / n_particles) + β_old = β_new + end + @debug "guided period" t stages = stage adaptation_ess = effective_sample_size(W) / n_particles + + if t > presample_periods + loglik += ll_t end end - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) + return isfinite(loglik) ? loglik : on_failure_loglikelihood end -function run_particle_filter(::Val{:first_order}, - ::Val{:auxiliary}, - observables_index::Vector{Int}, - 𝐒, - data_in_deviations::AbstractMatrix, - constants::constants, - state, - 𝓂::ℳ, - measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, - obs_idx_per_t::Vector{Vector{Int}}, - has_missing::Bool; - n_particles::Int = DEFAULT_N_PARTICLES, - particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, - particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, - particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, - particle_rng::Random.AbstractRNG = Random.default_rng(), - presample_periods::Int = 0, - initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, - on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, - opts::CalculationOptions = merge_calculation_options())::Float64 - T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) +# ── Tempered particle filter (Herbst & Schorfheide, 2019) ──────────────────── +# Within each period the measurement information is introduced gradually through +# a bridging sequence 0 = φ₀ < φ₁ < … < φ_N = 1 (the measurement covariance is +# inflated to H/φ). Each stage reweights by the tempered density increment, +# resamples, and mutates the particles' shocks with a random-walk Metropolis step +# targeting the stage-φ posterior. This dramatically lowers the variance of both +# the likelihood estimate and the filtered moments relative to the bootstrap +# filter at equal particle count. - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - inv_me_var = me_inverse_diagonal(me_var) +# Inefficiency ratio N·Σ(wᵖ)² / (Σwᵖ)² for the incremental weights +# wᵖ = exp(-(φ-φ_old)/2 · dᵖ). Increasing in φ, equal to 1 at φ = φ_old. +# +# The largest weight sits at the smallest dᵖ whatever the level, so the +# log-sum-exp shift is known before the loop and the whole ratio takes a single +# vectorised pass. Particles with dᵖ = Inf (an impossible prediction) fall out on +# their own: exp(-Δ·Inf) = 0 for any Δ > 0. This is called ~20 times per +# tempering stage by the bisection below, so the pass is worth having tight. +function tempered_inefficiency(Δ::Float64, dmin::Float64, d::Vector{Float64}) + n = length(d) + S1 = 0.0 + S2 = 0.0 + @turbo for p in 1:n + e = exp(-Δ * (d[p] - dmin)) + S1 += e + S2 += e * e + end + return S1 > 0 && isfinite(S2) ? n * S2 / (S1 * S1) : Inf +end - tr = build_linear_particle_transition(𝐒, T) +# Next tempering level in (φ_old, 1] targeting inefficiency `r_star` by bisection. +# The bracket is only ever used to pick a step size, so a 1e-6 tolerance is well +# beyond what the schedule can notice and saves a third of the iterations. +function tempered_next_phi(φ_old::Float64, d::Vector{Float64}, r_star::Float64, n_particles::Int) + dmin = Inf + @inbounds for p in 1:n_particles + dp = d[p] + dmin = (dp < dmin && isfinite(dp)) ? dp : dmin + end + isfinite(dmin) || return 1.0 - # Predictive variance of each observable (shock spread + measurement error). - me_diag = me_diagonal(me_var) - pred_var = Vector{Float64}(undef, length(observables_index)) - @inbounds for i in eachindex(observables_index) - pred_var[i] = sum(abs2, @view tr.B[observables_index[i], :]) + me_diag[i] + if tempered_inefficiency((1.0 - φ_old) / 2, dmin, d) <= r_star + return 1.0 + end + lo = φ_old + hi = 1.0 + for _ in 1:60 + mid = 0.5 * (lo + hi) + if tempered_inefficiency((mid - φ_old) / 2, dmin, d) < r_star + lo = mid + else + hi = mid + end + hi - lo < 1e-6 && break end - inv_pred_var = 1.0 ./ pred_var + return 0.5 * (lo + hi) +end - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) +# ── Mutation: preconditioned, adaptive random-walk Metropolis ──────────────── +# +# The stage-φ target on the period's shocks is +# π_φ(ε) ∝ N(ε; 0, I) · exp(-φ/2 · e(ε)ᵀ H⁻¹ e(ε)), +# with e(ε) the measurement residual the shock produces. An isotropic random walk +# with a fixed step is a poor way to explore it: the observation constrains the +# shocks very unevenly (a monetary shock and a price-markup shock move the +# observables by wildly different amounts), and the whole target contracts as φ +# rises. A step small enough to be accepted at φ = 1 then barely moves the +# particle at φ ≈ 0, and the cloud is not rejuvenated at all — which is exactly +# what leaves the filtered estimates at the mercy of the seed. +# +# Two fixes, both cheap: +# +# * Preconditioning. Linearising e(ε) ≈ e(0) - Bₒ ε with Bₒ the first-order +# impact of the shocks on the observables makes π_φ Gaussian with covariance +# (I + φ G)⁻¹, G = Bₒᵀ H⁻¹ Bₒ. Proposing ε' = ε + c·L_φ z with L_φ L_φᵀ = +# (I + φ G)⁻¹ therefore steps along exactly the directions and by the +# magnitudes the target allows, at nExo × nExo cost per stage. On a linear +# model this makes the proposal shape exact; on a nonlinear one it is a good +# preconditioner because the curvature that matters here is the measurement +# equation's, not the model's. +# * Adaptation. `c` is scaled after every mutation step towards a 25 % +# acceptance rate (the standard random-walk target), so it finds the right +# magnitude within the first few periods instead of being guessed. The +# schedule `φ` is already chosen adaptively from the particle system, so this +# adds no new kind of dependence; each individual Metropolis kernel is still +# exactly π_φ-invariant. +# +# How this differs from the guided filter's mutation (`guided_anneal_mutate!`). +# The machinery is deliberately the same — bridge in stages, reweight, resample, +# mutate, with the step size chosen by the same inefficiency target. What differs +# is the two endpoints, and everything else follows from that: +# +# * Where the bridge starts. Here at the prior N(ε;0,I), which knows nothing +# about yₜ, so the schedule usually needs several stages to arrive (~9 per +# period on the euro-area problem). The guided filter starts at its Laplace +# proposal, which already accounts for yₜ, and typically reaches β = 1 in one +# step — its bridge exists only for the periods the proposal fits badly. +# * What the mutation is preconditioned by. The preconditioner here has to +# follow the target as it contracts, hence the φ-dependent (I + φ G)⁻¹ and a +# fresh Cholesky per stage. The guided kernel reuses M⁻¹, the proposal's own +# covariance, at every β: it is the right shape at both ends because the +# bridge only interpolates between two distributions that already share it. +# * What β multiplies. Here φ scales the measurement term against a fixed +# prior. There the exponent moves weight from q to π̃, so the Metropolis ratio +# carries the proposal's Mahalanobis form as well as ‖ε‖². +# +# Both cost one batched transition per mutation step, so the difference in price +# is entirely the difference in stage count. - pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) - X, X2, AncX = pws.X, pws.X2, pws.Anc - E = pws.E - W, logg̃, logw = pws.W, pws.logdens, pws.logw - lam, idx, bins = pws.lam, pws.idx, pws.bins - fill!(W, 1.0 / n_particles) +# `DEFAULT_PARTICLE_LOW_ESS_FRACTION` is the average effective sample size, as a +# fraction of `n_particles`, below which the reported estimates are flagged. At +# that point the weighted moments are the average of a handful of distinct +# particles, so they move materially from seed to seed however many particles are +# nominally in the cloud — the fix is a better proposal, not a longer run. - mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) - init_linear_particles!(X, rng, mean0, L, AncX) +# Why every tempering stage resamples, following Herbst & Schorfheide. +# +# Each resampling discards the ancestors that lost, and with ~9 stages in a period +# the compounding is severe: on a Smets-Wouters-sized problem only about 2 % of +# the cloud survives a period as *distinct* ancestors (a few hundred out of ten +# thousand). Every filtered moment is an average over those, which is what sets +# the seed-to-seed spread of the reported estimates — not `n_particles` directly. +# +# Deferring the resampling until the weights degenerate (standard adaptive SMC, +# resample at ESS < N/4) was measured on that problem and is *not* worth it: the +# surviving ancestors rise only from 2.24 % to 2.63 % while the stage count rises +# from 9.0 to 11.5, because the schedule then has to take smaller steps. Per unit +# of work it is slightly worse, and it makes the step criterion inconsistent with +# the carried weights. Resampling every stage is both simpler and better here. +# +# What does move the number is the particle count, roughly linearly — which is +# why `n_particles` is the lever to reach for when the estimates need to be +# steadier, and why making the swarm cheap to propagate was worth doing. + +# G = Bₒᵀ H⁻¹ Bₒ over the observed rows, from the first-order shock loading. +function shock_information_matrix(S₁::AbstractMatrix, observables_index::Vector{Int}, + nPast::Int, me_var::AbstractVector, rows) + nExo = size(S₁, 2) - nPast + G = zeros(Float64, nExo, nExo) + @inbounds for r in rows + w = 1.0 / me_var[r] + row = @view S₁[observables_index[r], nPast+1:end] + for i in 1:nExo, j in 1:nExo + G[i, j] += w * row[i] * row[j] + end + end + return G +end - loglik = 0.0 - for t in 1:nT - rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) - data_col = @view data_in_deviations[:, t] - logZ = particle_measurement_logZ(me_var, rows, log2pi) - logZp = particle_measurement_logZ(pred_var, rows, log2pi) +function shock_information_matrix(S₁::AbstractMatrix, observables_index::Vector{Int}, + nPast::Int, me::DenseMeasurementError, rows) + me_sync!(me, rows) + Bo = Matrix{Float64}(undef, length(rows), size(S₁, 2) - nPast) + @inbounds for (k, r) in enumerate(rows) + Bo[k, :] .= @view S₁[observables_index[r], nPast+1:end] + end + Y = ℒ.LowerTriangular(me.last_L) \ Bo # Y = L⁻¹Bₒ, so YᵀY = BₒᵀH⁻¹Bₒ + return Y' * Y +end - # First stage: predictive density at the transition mean μ = A·X (one gemm). - base_batch!(X2, tr, X) - @inbounds for p in 1:n_particles - logg̃[p] = logZp - 0.5 * linear_quadform_col(X2, p, data_col, observables_index, inv_pred_var, rows) - end +# L_φ with L_φ L_φᵀ = (I + φ G)⁻¹. `cholesky(M).U` is the upper factor R with +# M = RᵀR, so R⁻¹ (R⁻¹)ᵀ = M⁻¹ and R⁻¹ can serve as L_φ directly. +function tempering_proposal_factor(G::Matrix{Float64}, φ::Float64) + n = size(G, 1) + M = Matrix{Float64}(ℒ.I, n, n) + @inbounds for i in 1:n, j in 1:n + M[i, j] += φ * G[i, j] + end + F = ℒ.cholesky(ℒ.Symmetric(M), check = false) + ℒ.issuccess(F) || return Matrix{Float64}(ℒ.I, n, n) + return Matrix{Float64}(inv(F.U)) +end - mλ = -Inf - @inbounds for p in 1:n_particles - lλ = log(W[p]) + logg̃[p] - mλ = lλ > mλ ? lλ : mλ - end - if !isfinite(mλ) - return Float64(on_failure_loglikelihood) - end - sλ = 0.0 - @inbounds for p in 1:n_particles - sλ += exp(log(W[p]) + logg̃[p] - mλ) - end - logκ = mλ + log(sλ) - @inbounds for p in 1:n_particles - lam[p] = exp(log(W[p]) + logg̃[p] - logκ) - end +# One Metropolis sweep over the whole swarm at level `φ`. Proposals are formed +# for every particle at once (one `gemm` for the preconditioned step, one batched +# transition), scored, and then accepted or rejected. Returns the acceptance rate. +# +# The accept/reject decision has to consume the RNG in particle order, so it is +# taken in one cheap serial pass over the proposal scores and only the resulting +# column copies — which move far more bytes than the decision costs — are chunked +# across threads. The RNG is drawn exactly once per particle, in order, either +# way, so the outcome does not depend on the thread count. +function tempered_mutate!(::Val{algo}, tr, scr, St::NTuple{K,Matrix{Float64}}, + parts_proposed::NTuple{K,Matrix{Float64}}, anc::NTuple{K,Matrix{Float64}}, + E, Eprop, Z, Fbuf, dv, dprop, accept, Lφ, c::Float64, φ::Float64, + data_col, observables_index, inv_me_var, rows, rng) where {algo, K} + n_particles = size(E, 2) + nExo = size(E, 1) - # Resample ancestors ∝ λ, gather them, and propagate with fresh shocks. - particle_resample_indices!(idx, bins, rng, lam, resampling) - @inbounds for j in 1:n_particles - copy_col!(AncX, j, X, idx[j]) - end - Random.randn!(rng, E) - propagate_batch!(X2, tr, AncX, E) - @inbounds for j in 1:n_particles - logw[j] = (logZ - 0.5 * linear_quadform_col(X2, j, data_col, observables_index, inv_me_var, rows)) - logg̃[idx[j]] - end - X, X2 = X2, X + Random.randn!(rng, Z) + ℒ.mul!(Eprop, Lφ, Z, c, 0.0) # ε' - ε = c·L_φ·z + @inbounds for i in eachindex(Eprop) + Eprop[i] += E[i] + end - mw = maximum(logw) - if !isfinite(mw) - return Float64(on_failure_loglikelihood) - end - sw = 0.0 - @inbounds for j in 1:n_particles - sw += exp(logw[j] - mw) - end - if sw <= 0 || !isfinite(sw) - return Float64(on_failure_loglikelihood) - end + propagate_cloud!(Val(algo), tr, scr, parts_proposed, anc, Eprop) + F = full_states!(Fbuf, parts_proposed) + quadform_cloud!(dprop, F, data_col, observables_index, inv_me_var, rows) - ll_t = logκ + (mw + log(sw) - log(n_particles)) - if t > presample_periods - loglik += ll_t + accepted = 0 + @inbounds for p in 1:n_particles + # ‖ε‖² at the current and the proposed shock: the prior part of the + # Metropolis ratio, the measurement part being `dv`/`dprop`. + shock_norm²_current = 0.0 + shock_norm²_proposed = 0.0 + for e in 1:nExo + current = E[e, p] + proposed = Eprop[e, p] + shock_norm²_current += current * current + shock_norm²_proposed += proposed * proposed + end + logα = -0.5 * ((shock_norm²_proposed - shock_norm²_current) + φ * (dprop[p] - dv[p])) + acc = log(rand(rng)) < logα + accept[p] = acc + if acc + dv[p] = dprop[p] + accepted += 1 end + end - logsw = mw + log(sw) - @inbounds for j in 1:n_particles - W[j] = exp(logw[j] - logsw) + foreach_column_chunk(n_particles) do cols + @inbounds for p in cols + if accept[p] + copy_col!(E, p, Eprop, p) + copy_cloud_col!(St, p, parts_proposed, p) + end end end - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) + return accepted / n_particles end +# Multiplicative step towards the target acceptance rate, bounded so a pathological +# period cannot drive the scale to zero or blow it up. +@inline function adapt_mh_scale(c::Float64, acceptance::Float64) + lo, hi = DEFAULT_PARTICLE_MH_SCALE_BOUNDS + return clamp(c * exp(DEFAULT_PARTICLE_MH_ADAPTATION_GAIN * (acceptance - DEFAULT_PARTICLE_MH_TARGET_ACCEPTANCE)), lo, hi) +end -function run_particle_filter(::Val{:first_order}, +function run_particle_filter(::Val{algo}, ::Val{:tempered}, observables_index::Vector{Int}, 𝐒, @@ -1637,74 +2230,76 @@ function run_particle_filter(::Val{:first_order}, presample_periods::Int = 0, initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, on_failure_loglikelihood::Real = -Inf, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, - opts::CalculationOptions = merge_calculation_options())::Float64 + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, + opts::CalculationOptions = merge_calculation_options())::Float64 where {algo} T = constants.post_model_macro - rng = particle_rng - resampling = particle_resampling - resampling_threshold = particle_resampling_threshold - initial_state_prior_scaling_factor = particle_initial_state_scaling - nVars = T.nVars - nExo = T.nExo - nT = size(data_in_deviations, 2) - presample_periods = normalize_presample_periods(presample_periods, nT) - log2pi = log(2π) - - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - inv_me_var = me_inverse_diagonal(me_var) - - r_star = Float64(tempering_target_ratio) - c = Float64(tempering_mh_scale) - n_mh = tempering_mh_steps - max_stages = tempering_max_stages - - tr = build_linear_particle_transition(𝐒, T) - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(initial_state_prior_scaling_factor)) - - # Double-buffered particle pools (columns are particles), taken from the - # model's workspace. The locals below are swapped in place of copying, which - # leaves the workspace fields pointing at whichever buffer ends up where — - # harmless, since every buffer is written before it is read. - pws = ensure_particle_workspace!(𝓂.workspaces, nVars, nExo, n_particles) - Anc, Anc2 = pws.Anc, pws.Anc2 - Sh, Sh2 = pws.E, pws.E2 - St, St2 = pws.St, pws.St2 - dv, dv2 = pws.dv, pws.dv2 - - Base = pws.X - Sprop = pws.X2 - Eprop = pws.Eprop - logw = pws.logw - Wn = pws.Wn - idx = pws.idx - bins = pws.bins - - mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) - init_linear_particles!(St, rng, mean0, L, Anc2) + nT = size(data_in_deviations, 2) + + me_var, inv_me_var, tr, scr, L, pws = + particle_filter_setup(Val(algo), 𝐒, T, 𝓂, measurement_error, n_particles, + particle_initial_state_scaling, initial_covariance, opts) + + K = pruned_components(Val(algo)) + nK = n_components(K) + pools = ensure_particle_pools!(pws, 5 * nK + 1) + anc = cloud_group(pools, 1, K) + anc_scratch = cloud_group(pools, 2, K) + St = cloud_group(pools, 3, K) + St_scratch = cloud_group(pools, 4, K) + parts_proposed = cloud_group(pools, 5, K) + Fbuf = pools[5 * nK + 1] + + init_cloud!(anc, particle_rng, state, L, Fbuf) + + return tempered_loop!(Val(algo), tr, scr, anc, anc_scratch, St, St_scratch, parts_proposed, Fbuf, + pws.E, pws.E2, pws.Eprop, pws.logw, pws.Wn, pws.dv, pws.dv2, + pws.idx, pws.bins, nT, normalize_presample_periods(presample_periods, nT), + observables_index, data_in_deviations, obs_idx_per_t, has_missing, + me_var, inv_me_var, + 𝓂.caches.first_order_solution_matrix, T.nPast_not_future_and_mixed, + particle_resampling, Float64(particle_target_ratio), + Float64(particle_mh_scale), particle_mh_steps, particle_max_stages, + particle_rng, Float64(on_failure_loglikelihood), log(2π)) +end +function tempered_loop!(::Val{algo}, tr, scr, anc::NTuple{K,Matrix{Float64}}, anc_scratch::NTuple{K,Matrix{Float64}}, + St::NTuple{K,Matrix{Float64}}, St_scratch::NTuple{K,Matrix{Float64}}, + parts_proposed::NTuple{K,Matrix{Float64}}, Fbuf, E, E_scratch, Eprop, logw, Wn, dv, dv_scratch, + idx, bins, nT, presample_periods, observables_index, data_in_deviations, + obs_idx_per_t, has_missing, me_var, inv_me_var, S₁, nPast, resampling, + r_star, mh_scale, n_mh, max_stages, rng, on_failure_loglikelihood, log2pi) where {algo, K} + n_particles = size(E, 2) + Z = Matrix{Float64}(undef, size(E, 1), n_particles) + dprop = Vector{Float64}(undef, n_particles) + accept = Vector{Bool}(undef, n_particles) + c = mh_scale loglik = 0.0 + for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view data_in_deviations[:, t] d_obs = length(rows) - # Ancestors = previous filtered states; propagate the whole swarm (2 gemm). - copyto!(Anc, St) - Random.randn!(rng, Sh) - propagate_batch!(St, tr, Anc, Sh) - @inbounds for p in 1:n_particles - dv[p] = linear_quadform_col(St, p, data_col, observables_index, inv_me_var, rows) + # Bootstrap proposal: propagate every ancestor with a fresh shock. + Random.randn!(rng, E) + propagate_cloud!(Val(algo), tr, scr, St, anc, E) + F = full_states!(Fbuf, St) + quadform_cloud!(dv, F, data_col, observables_index, inv_me_var, rows) + + if isempty(rows) + anc, St = St, anc + continue end if all(!isfinite, dv) - return Float64(on_failure_loglikelihood) + return on_failure_loglikelihood end + G = shock_information_matrix(S₁, observables_index, nPast, me_var, rows) logZ = particle_measurement_logZ(me_var, rows, log2pi) + period_ll = 0.0 φ_old = 0.0 stage = 0 @@ -1712,6 +2307,10 @@ function run_particle_filter(::Val{:first_order}, stage += 1 φ_new = tempered_next_phi(φ_old, dv, r_star, n_particles) + # Incremental weights. The tempered density is + # p_φ(y|x) = (2π)^(-d/2) φ^(d/2) |H|^(-1/2) exp(-φ/2·d), + # so stage one carries the full normalisation and later stages only + # the ratio p_{φ_new}/p_{φ_old}. if φ_old == 0.0 @inbounds for p in 1:n_particles logw[p] = logZ + 0.5 * d_obs * log(φ_new) - 0.5 * φ_new * dv[p] @@ -1723,62 +2322,32 @@ function run_particle_filter(::Val{:first_order}, end end - m = maximum(logw) - if !isfinite(m) - return Float64(on_failure_loglikelihood) - end - s = 0.0 - @inbounds for p in 1:n_particles - s += exp(logw[p] - m) - end - if s <= 0 || !isfinite(s) - return Float64(on_failure_loglikelihood) - end - period_ll += m + log(s) - log(n_particles) - - logsw = m + log(s) - @inbounds for p in 1:n_particles - Wn[p] = exp(logw[p] - logsw) - end + logsw = normalise_log_weights!(Wn, logw) + isfinite(logsw) || return on_failure_loglikelihood + period_ll += logsw - log(n_particles) particle_resample_indices!(idx, bins, rng, Wn, resampling) + gather_cloud!(anc_scratch, anc, idx) + gather_cloud!(St_scratch, St, idx) @inbounds for j in 1:n_particles - a = idx[j] - copy_col!(Anc2, j, Anc, a); copy_col!(Sh2, j, Sh, a); copy_col!(St2, j, St, a); dv2[j] = dv[a] + copy_col!(E_scratch, j, E, idx[j]) + dv_scratch[j] = dv[idx[j]] end - Anc, Anc2 = Anc2, Anc - Sh, Sh2 = Sh2, Sh - St, St2 = St2, St - dv, dv2 = dv2, dv - - # Metropolis mutation on the shocks. Base = A·Anc is shock-independent - # (one gemm); each proposal only recomputes the batched shock term - # B·Eprop before per-particle accept/reject. - base_batch!(Base, tr, Anc) + anc, anc_scratch = anc_scratch, anc + St, St_scratch = St_scratch, St + E, E_scratch = E_scratch, E + dv, dv_scratch = dv_scratch, dv + + Lφ = tempering_proposal_factor(G, φ_new) + acc_sum = 0.0 for _ in 1:n_mh - Random.randn!(rng, Eprop) - @inbounds for k in eachindex(Eprop) - Eprop[k] = Sh[k] + c * Eprop[k] - end - ℒ.mul!(Sprop, tr.B, Eprop) - Sprop .+= Base - @inbounds for p in 1:n_particles - dprop = linear_quadform_col(Sprop, p, data_col, observables_index, inv_me_var, rows) - esq_old = 0.0 - esq_new = 0.0 - for e in 1:nExo - so = Sh[e, p]; sn = Eprop[e, p] - esq_old += so * so - esq_new += sn * sn - end - logα = -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) - if log(rand(rng)) < logα - copy_col!(Sh, p, Eprop, p) - copy_col!(St, p, Sprop, p) - dv[p] = dprop - end - end + acc = tempered_mutate!(Val(algo), tr, scr, St, parts_proposed, anc, E, Eprop, Z, Fbuf, + dv, dprop, accept, Lφ, c, φ_new, data_col, + observables_index, inv_me_var, rows, rng) + c = adapt_mh_scale(c, acc) + acc_sum += acc end + @debug "tempered stage" t stage φ_new acceptance = acc_sum / max(n_mh, 1) mh_scale = c φ_old = φ_new end @@ -1786,9 +2355,13 @@ function run_particle_filter(::Val{:first_order}, if t > presample_periods loglik += period_ll end + # The filtered cloud becomes next period's ancestors. A swap rather than a + # copy: next period's propagation overwrites every column of `St` before + # reading it, so whatever `anc` held is free to be scribbled over. + anc, St = St, anc end - return isfinite(loglik) ? loglik : Float64(on_failure_loglikelihood) + return isfinite(loglik) ? loglik : on_failure_loglikelihood end @@ -1806,18 +2379,26 @@ end # `smooth = true` they condition on the whole sample, E[xₜ | y₁..T], obtained by # the genealogy smoother in `smooth_particle_trajectories!` below. # -# All three particle variants target the same filtering distribution p(xₜ|y₁..ₜ) +# All four particle variants target the same filtering distribution p(xₜ|y₁..ₜ) # — they differ only in how they get there — so the recursion below is shared. # `:bootstrap_particle` and `:auxiliary_particle` use the plain # predict/weight/resample step: the auxiliary filter's look-ahead proposal changes # the *variance* of the likelihood estimate, not the cloud it leaves behind, so -# there is nothing extra to do for the moments. `:tempered_particle` does change -# the cloud: within each period it bridges from the prior to the full measurement -# density in stages, resampling and rejuvenating the shocks by random-walk -# Metropolis at each one. That leaves a cloud with far more distinct support -# points at the same `n_particles`, which is exactly what the moments (and the -# smoother, which walks the genealogy) benefit from — so the tempering controls -# act here too. +# there is nothing extra to do for the moments. +# +# The other two do change the cloud, and that is what makes them the ones to use +# when the estimates themselves, rather than a likelihood, are the output. Both +# bridge to the period's target in stages and rejuvenate the shocks by Metropolis +# at each one, which leaves far more distinct support points at the same +# `n_particles` — exactly what the weighted moments, and the smoother that walks +# the genealogy, depend on. So the bridging controls act here too. +# +# Between the two, `:guided_particle` is the better default: it bridges from a +# proposal that already accounts for the observation rather than from the prior, +# and measures both more accurate and much cheaper (see the estimates section of +# `docs/src/filters.md`). `:tempered_particle` is the fallback for the case the +# guided proposal is built on and can get wrong — an observation far from linear +# in the shock — since it assumes nothing about that. # # `decomposition` is the shock decomposition of whichever shock path was # produced — filtered when `smooth = false`, smoothed when `smooth = true`. @@ -1828,10 +2409,10 @@ end warmup_iterations::Int = 0, opts::CalculationOptions = merge_calculation_options(), smooth::Bool = true, - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_TEMPERED_MH_STEPS, + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, measurement_error::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, n_particles::Int = DEFAULT_N_PARTICLES, particle_resampling::Symbol = DEFAULT_PARTICLE_RESAMPLING, @@ -1850,7 +2431,6 @@ end T = constants.post_model_macro nVars = T.nVars nExo = T.nExo - past_idx = T.past_not_future_and_mixed_idx ss_names = constants.post_complete_parameters.SS_and_pars_names observables_index = convert(Vector{Int}, indexin(observables, ss_names)) @@ -1859,107 +2439,204 @@ end obs_idx_per_t, has_missing = build_obs_index(dat) nT = size(dat, 2) - # `measurement_error` arrives already resolved (a variance vector or a - # covariance matrix) — `:auto` is handled by the user-facing entry points. - me_var = build_particle_measurement_error(measurement_error) - assert_positive_measurement_error(me_var) - # solution matrices and the initial state, exactly as the likelihood path builds them _, _, 𝐒, state, solved = get_relevant_steady_state_and_state_update(Val(algo), 𝓂.parameter_values, 𝓂, opts = opts) @assert solved "Could not solve the model for `algorithm = $(algo)`; cannot run the particle filter." - Σ, _ = particle_initial_state_covariance(𝓂, T, opts, initial_covariance) - L = particle_initial_cloud_factor(Σ, Float64(particle_initial_state_scaling)) - - log2pi = log(2π) - rng = particle_rng + me_var, inv_me_var, tr, scr, L, pws = + particle_filter_setup(Val(algo), 𝐒, T, 𝓂, measurement_error, n_particles, + particle_initial_state_scaling, initial_covariance, opts) # storage for the filtered moments variables = zeros(nVars, nT) stds = zeros(nVars, nT) shocks_out = zeros(nExo, nT) - W = fill(1.0 / n_particles, n_particles) - logdens = Vector{Float64}(undef, n_particles) - idx = Vector{Int}(undef, n_particles) - bins = Vector{Float64}(undef, n_particles) - shocks = [Vector{Float64}(undef, nExo) for _ in 1:n_particles] - full_buf = Vector{Float64}(undef, nVars) + K = pruned_components(Val(algo)) + nK = n_components(K) + pools = ensure_particle_pools!(pws, 6 * nK + 1) + parts = cloud_group(pools, 1, K) + parts2 = cloud_group(pools, 2, K) + anc = cloud_group(pools, 3, K) + anc2 = cloud_group(pools, 4, K) + parts_proposed = cloud_group(pools, 5, K) + sprop2 = cloud_group(pools, 6, K) + Fbuf = pools[6 * nK + 1] + + init_cloud!(parts, particle_rng, state, L, Fbuf) + + if pf == :guided_particle + guided_estimates_loop!(Val(algo), tr, scr, parts, parts2, anc, parts_proposed, sprop2, Fbuf, pws, + variables, stds, shocks_out, nT, observables_index, dat, + obs_idx_per_t, has_missing, me_var, inv_me_var, + 𝓂.caches.first_order_solution_matrix, T.nPast_not_future_and_mixed, + particle_resampling, Float64(particle_resampling_threshold), + particle_mh_steps, Float64(particle_mh_scale), + Float64(particle_target_ratio), particle_max_stages, + particle_rng, smooth, log(2π)) + else + particle_estimates_loop!(Val(algo), Val(pf == :tempered_particle), tr, scr, + parts, parts2, anc, anc2, parts_proposed, Fbuf, pws, + variables, stds, shocks_out, nT, observables_index, dat, + obs_idx_per_t, has_missing, me_var, inv_me_var, + 𝓂.caches.first_order_solution_matrix, T.nPast_not_future_and_mixed, + particle_resampling, Float64(particle_resampling_threshold), + Float64(particle_target_ratio), Float64(particle_mh_scale), + particle_mh_steps, particle_max_stages, particle_rng, smooth, log(2π)) + end + + # ── Shock decomposition ────────────────────────────────────────────────── + # A decomposition needs a shock path; the particle filter supplies one (the + # filtered or smoothed shocks above), so the same attribution the inversion + # filter uses applies here. At first order contributions are additive and the + # split is exact. At pruned higher order they are not additive, which is + # precisely what the Aumann-Shapley (marginal contribution) attribution is + # for, so the pruned decomposition reuses the routines in `inversion.jl`. + # Non-pruned `:second_order` / `:third_order` have no decomposition at all + # (the caller already turns `shock_decomposition` off for them). + # Column layout follows the inversion filter: with the Aumann-Shapley + # attribution (and at first order) it is [contributions…, baseline, total] = + # nExo+2; the sequential pruned attribution adds an explicit interaction and + # residual column, [contributions…, interaction, residual, total] = nExo+3. + sequential_pruned = algo ∈ (:pruned_second_order, :pruned_third_order) && !marginal_contribution + decomposition = zeros(nVars, sequential_pruned ? nExo + 3 : nExo + 2, nT) + decomposition[:, end, :] .= variables + + past_idx = T.past_not_future_and_mixed_idx if algo == :first_order - tr = build_linear_particle_transition(𝐒, T) - mean0 = state isa AbstractVector{<:AbstractVector} ? Vector{Float64}(state[1]) : Vector{Float64}(state) - parts = [mean0 .+ L * randn(rng, nVars) for _ in 1:n_particles] - parts2 = [zeros(Float64, nVars) for _ in 1:n_particles] - propagate! = (out, prev, sh) -> linear_propagate_estimates!(out, tr, prev, sh) + 𝐒₁ = 𝐒 isa AbstractMatrix ? 𝐒 : 𝐒[1] + init_vec = state isa AbstractVector{<:AbstractVector} ? state[1] : state + sck = zeros(nExo) + @inbounds for i in 1:nExo + fill!(sck, 0.0) + sck[i] = shocks_out[i, 1] + decomposition[:, i, 1] .= 𝐒₁ * vcat(init_vec[past_idx], sck) + end + decomposition[:, end - 1, 1] .= decomposition[:, end, 1] - sum(decomposition[:, 1:end-2, 1], dims = 2) + for t in 2:nT + @inbounds for i in 1:nExo + fill!(sck, 0.0) + sck[i] = shocks_out[i, t] + decomposition[:, i, t] .= 𝐒₁ * vcat(decomposition[past_idx, i, t-1], sck) + end + decomposition[:, end - 1, t] .= decomposition[:, end, t] - sum(decomposition[:, 1:end-2, t], dims = 2) + end + elseif algo ∈ (:pruned_second_order, :pruned_third_order) && marginal_contribution + # The Aumann-Shapley attribution requires `variables` and `shocks` to lie + # on the *same* model trajectory — it checks that the contributions plus + # the zero-shock baseline reproduce `variables`. A smoothed mean is not a + # model path (averaging does not commute with the nonlinear transition: + # E[g(x,ε)] ≠ g(E[x],E[ε])), so feeding it in directly leaves a closure + # error that the routine tries to remove by refining its quadrature. + # Decompose the trajectory implied by the smoothed shocks instead, which + # is the same object the inversion filter decomposes and closes exactly. + traj = shock_path_trajectory(Val(algo), tr, scr, state, shocks_out, nVars, nT) + decomposition[:, end, :] .= traj + + if algo == :pruned_second_order + aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, traj, shocks_out, + state, 𝐒, T, nExo; verbose = opts.verbose) + else + aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, traj, shocks_out, + state, 𝐒, T, nExo; verbose = opts.verbose) + end + elseif sequential_pruned + # Sequential attribution: run one trajectory per shock with only that + # shock switched on, plus one with all of them. Each single-shock path is + # that shock's contribution; the all-shock path minus the sum of the + # single-shock paths is the interaction the nonlinearity creates (this is + # the term the Aumann-Shapley variant instead distributes across shocks), + # and whatever is still left over goes into the residual column. The + # nExo+1 trajectories are carried as the columns of one cloud, so each + # period costs a single batched transition. + sequential_shock_decomposition!(Val(algo), tr, scr, state, shocks_out, variables, + decomposition, nVars, nExo, nT) else - 𝐒f = [Matrix{Float64}(S) for S in 𝐒] - scr = build_higher_scratch(Val(algo), T.nPast_not_future_and_mixed, nExo) - parts = init_higher_particles(Val(algo), rng, state, L, n_particles, nVars) - parts2 = [zeros_like_particle(parts[1]) for _ in 1:n_particles] - propagate! = (out, prev, sh) -> higher_propagate!(Val(algo), out, prev, sh, past_idx, 𝐒f, scr) + @info "Shock decomposition is not available for $(algo) solutions (use a pruned solution); returning zeros." maxlog = 1 end + return variables, shocks_out, stds, decomposition +end + +# The forward pass shared by every particle variant. `Val(tempered)` selects the +# within-period tempering stages; the rest of the recursion is identical. +function particle_estimates_loop!(::Val{algo}, ::Val{tempered}, tr, scr, + parts::NTuple{K,Matrix{Float64}}, parts_scratch::NTuple{K,Matrix{Float64}}, + anc::NTuple{K,Matrix{Float64}}, anc_scratch::NTuple{K,Matrix{Float64}}, + parts_proposed::NTuple{K,Matrix{Float64}}, Fbuf, pws, + variables, stds, shocks_out, nT, observables_index, dat, + obs_idx_per_t, has_missing, me_var, inv_me_var, S₁, nPast, + resampling, resampling_threshold, r_star, mh_scale, n_mh, + max_stages, rng, smooth::Bool, log2pi) where {algo, tempered, K} + n_particles = size(pws.E, 2) + nVars = size(variables, 1) + nExo = size(shocks_out, 1) + + E, E_scratch, Eprop = pws.E, pws.E2, pws.Eprop + W, logdens, Wn = pws.W, pws.logdens, pws.Wn + dv, dv_scratch = pws.dv, pws.dv2 + idx, bins = pws.idx, pws.bins + Z = tempered ? Matrix{Float64}(undef, nExo, n_particles) : Matrix{Float64}(undef, 0, 0) + dprop = tempered ? Vector{Float64}(undef, n_particles) : Float64[] + accept = tempered ? Vector{Bool}(undef, n_particles) : Bool[] + + fill!(W, 1.0 / n_particles) + c = mh_scale + ess_sum = 0.0 # effective sample size accumulated over scored periods + n_scored = 0 + # Smoothing storage (see the backward pass below). `hist_*` keep the cloud and # the shocks of every period; `parent[t]` is the resampling map applied at the # end of period t (empty ⇒ no resampling ⇒ identity), which is the genealogy - # the backward pass walks. + # the backward pass walks. `within[t]` is the composition of the tempering + # stages' resampling maps for period t: post-stage slot ↦ pre-stage slot. hist_states = smooth ? [Matrix{Float64}(undef, nVars, n_particles) for _ in 1:nT] : Matrix{Float64}[] hist_shocks = smooth ? [Matrix{Float64}(undef, nExo, n_particles) for _ in 1:nT] : Matrix{Float64}[] parent = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] - # `within[t]` is the composition of the tempering stages' resampling maps for - # period t: post-stage slot ↦ pre-stage slot. Empty for the non-tempered - # variants, which do no within-period resampling. within = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] terminal_weights = smooth ? fill(1.0 / n_particles, n_particles) : Float64[] - - tempered = pf == :tempered_particle - r_star = Float64(tempering_target_ratio) - mh_scale = Float64(tempering_mh_scale) - n_mh = tempering_mh_steps - max_stages = tempering_max_stages - # scratch used only by the tempering stages - anc_pool = tempered ? [zeros_like_particle(parts[1]) for _ in 1:n_particles] : typeof(parts)() - anc_pool2 = tempered ? [zeros_like_particle(parts[1]) for _ in 1:n_particles] : typeof(parts)() - shocks2 = tempered ? [Vector{Float64}(undef, nExo) for _ in 1:n_particles] : Vector{Float64}[] - sprop = tempered ? zeros_like_particle(parts[1]) : parts[1] - eprop = tempered ? Vector{Float64}(undef, nExo) : Float64[] - dv = tempered ? Vector{Float64}(undef, n_particles) : Float64[] - dv2 = tempered ? Vector{Float64}(undef, n_particles) : Float64[] - comp = tempered ? collect(1:n_particles) : Int[] - comp2 = tempered ? collect(1:n_particles) : Int[] + comp = tempered ? collect(1:n_particles) : Int[] + comp_scratch = tempered ? collect(1:n_particles) : Int[] for t in 1:nT rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) data_col = @view dat[:, t] + Random.randn!(rng, E) if tempered - @inbounds for p in 1:n_particles - copy_particle!(anc_pool[p], parts[p]) - end - end - - @inbounds for p in 1:n_particles - Random.randn!(rng, shocks[p]) - propagate!(parts2[p], parts[p], shocks[p]) + # Why this branch keeps last period's cloud when the others discard + # it. Bootstrap and auxiliary propagate once per period and are then + # done with xₜ₋₁. Tempering mutates the *shock*: each Metropolis + # proposal is a new εₜ that has to be pushed through the transition + # from the same ancestor to see what state it implies, so xₜ₋₁ stays + # live for the whole period. (The guided filter needs it for the same + # reason, which is why `guided_estimates_loop!` carries `anc` too.) + # + # Hence: park the ancestors in `anc` and propagate into the buffer + # `anc` was using. A swap, not a copy — the target is fully + # overwritten before it is read. + anc, parts = parts, anc + propagate_cloud!(Val(algo), tr, scr, parts, anc, E) + else + propagate_cloud!(Val(algo), tr, scr, parts_scratch, parts, E) + parts, parts_scratch = parts_scratch, parts end - parts, parts2 = parts2, parts + F = full_states!(Fbuf, parts) if isempty(rows) # nothing observed: the weights are unchanged, the cloud just predicts - fill!(logdens, 0.0) elseif tempered # Bridge from the prior to the full measurement density in stages, # resampling and rejuvenating at each one. The likelihood contribution # is not needed here (that is `run_particle_filter`'s job) — only the # cloud that comes out, which ends up equally weighted. - @inbounds for p in 1:n_particles - dv[p] = particle_quadratic_form(measurement_full(parts[p], full_buf), data_col, observables_index, me_var, rows) - end + quadform_cloud!(dv, F, data_col, observables_index, inv_me_var, rows) if any(isfinite, dv) @inbounds for p in 1:n_particles comp[p] = p end + G = shock_information_matrix(S₁, observables_index, nPast, me_var, rows) d_obs = length(rows) φ_old = 0.0 stage = 0 @@ -1971,79 +2648,57 @@ end @inbounds for p in 1:n_particles logdens[p] = lr - 0.5 * (φ_new - φ_old) * dv[p] end - m = maximum(logdens) - isfinite(m) || break - sw = 0.0 - @inbounds for p in 1:n_particles - sw += exp(logdens[p] - m) - end - (sw > 0 && isfinite(sw)) || break - logsw = m + log(sw) - @inbounds for p in 1:n_particles - W[p] = exp(logdens[p] - logsw) - end + isfinite(normalise_log_weights!(Wn, logdens)) || break - particle_resample_indices!(idx, bins, rng, W, particle_resampling) + particle_resample_indices!(idx, bins, rng, Wn, resampling) + gather_cloud!(anc_scratch, anc, idx) + gather_cloud!(parts_scratch, parts, idx) @inbounds for j in 1:n_particles a = idx[j] - copy_particle!(anc_pool2[j], anc_pool[a]) - copyto!(shocks2[j], shocks[a]) - copy_particle!(parts2[j], parts[a]) - dv2[j] = dv[a] - comp2[j] = comp[a] + copy_col!(E_scratch, j, E, a) + dv_scratch[j] = dv[a] + comp_scratch[j] = comp[a] end - anc_pool, anc_pool2 = anc_pool2, anc_pool - shocks, shocks2 = shocks2, shocks - parts, parts2 = parts2, parts - dv, dv2 = dv2, dv - comp, comp2 = comp2, comp - - # Rejuvenate: random-walk Metropolis on the shocks, targeting - # π(ε) ∝ N(ε;0,I)·exp(-φ/2·e(ε)ᵀH⁻¹e(ε)). - @inbounds for p in 1:n_particles - shp = shocks[p] - for _ in 1:n_mh - Random.randn!(rng, eprop) - esq_old = 0.0; esq_new = 0.0 - for e in 1:nExo - ep = shp[e] + mh_scale * eprop[e] - eprop[e] = ep - esq_new += ep * ep - esq_old += shp[e] * shp[e] - end - propagate!(sprop, anc_pool[p], eprop) - dprop = particle_quadratic_form(measurement_full(sprop, full_buf), data_col, observables_index, me_var, rows) - if log(rand(rng)) < -0.5 * ((esq_new - esq_old) + φ_new * (dprop - dv[p])) - copyto!(shp, eprop) - copy_particle!(parts[p], sprop) - dv[p] = dprop - end - end + # Every per-particle quantity carries a `_scratch` double. The + # gather above wrote the resampled values into the doubles, so + # swapping the two names makes them current — no copy, and the + # old buffers become next stage's scratch. + anc, anc_scratch = anc_scratch, anc + parts, parts_scratch = parts_scratch, parts + E, E_scratch = E_scratch, E + dv, dv_scratch = dv_scratch, dv + comp, comp_scratch = comp_scratch, comp + + Lφ = tempering_proposal_factor(G, φ_new) + acc_sum = 0.0 + for _ in 1:n_mh + acc = tempered_mutate!(Val(algo), tr, scr, parts, parts_proposed, anc, E, Eprop, Z, + Fbuf, dv, dprop, accept, Lφ, c, φ_new, data_col, + observables_index, inv_me_var, rows, rng) + c = adapt_mh_scale(c, acc) + acc_sum += acc end + # `comp` maps a post-stage slot back to the slot it occupied + # before the period's tempering began, so the number of + # distinct entries is exactly how many of the incoming + # ancestors are still represented. That is the quantity the + # accuracy of the filtered moments ultimately rests on. + @debug "tempered stage" t stage φ_new acceptance = acc_sum / max(n_mh, 1) mh_scale = c distinct_ancestors = length(unique(comp)) φ_old = φ_new end # the tempering stages leave an equally weighted cloud fill!(W, 1.0 / n_particles) - if smooth; within[t] = copy(comp); end - end - else - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - logdens[p] = particle_log_measurement_density(full, data_col, observables_index, me_var, rows, log2pi) - end - m = maximum(logdens) - if isfinite(m) - s = 0.0 - @inbounds for p in 1:n_particles - s += W[p] * exp(logdens[p] - m) - end - if s > 0 && isfinite(s) - @inbounds for p in 1:n_particles - W[p] = W[p] * exp(logdens[p] - m) / s - end + if smooth + within[t] = copy(comp) end + F = full_states!(Fbuf, parts) end + else + score_cloud!(logdens, F, data_col, observables_index, me_var, inv_me_var, rows, log2pi) + reweight_log_weights!(W, logdens) + ess_sum += effective_sample_size(W) + n_scored += 1 end if smooth @@ -2051,155 +2706,265 @@ end # any resampling at the end of this period, so it needs the weights in # that same indexing. Resampling would overwrite `W` with uniform ones. copyto!(terminal_weights, W) - # keep the whole cloud so the backward pass can walk the genealogy - Hs = hist_states[t]; Hh = hist_shocks[t] - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - for i in 1:nVars; Hs[i, p] = full[i]; end - sp = shocks[p] - for e in 1:nExo; Hh[e, p] = sp[e]; end - end + copyto!(hist_states[t], F) + copyto!(hist_shocks[t], E) else - # filtered moments of the (weighted) cloud - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - w = W[p] - for i in 1:nVars - variables[i, t] += w * full[i] - end - for e in 1:nExo - shocks_out[e, t] += w * shocks[p][e] - end - end - @inbounds for p in 1:n_particles - full = measurement_full(parts[p], full_buf) - w = W[p] - for i in 1:nVars - stds[i, t] += w * (full[i] - variables[i, t])^2 - end - end - @inbounds for i in 1:nVars - stds[i, t] = sqrt(max(stds[i, t], 0.0)) - end + accumulate_filtered_moments!(variables, stds, shocks_out, t, F, E, W) end - if effective_sample_size(W) < particle_resampling_threshold * n_particles - particle_resample_indices!(idx, bins, rng, W, particle_resampling) - @inbounds for j in 1:n_particles - copy_particle!(parts2[j], parts[idx[j]]) - end - parts, parts2 = parts2, parts + if effective_sample_size(W) < resampling_threshold * n_particles + particle_resample_indices!(idx, bins, rng, W, resampling) + gather_cloud!(parts_scratch, parts, idx) + parts, parts_scratch = parts_scratch, parts fill!(W, 1.0 / n_particles) - if smooth; parent[t] = copy(idx); end + if smooth + parent[t] = copy(idx) + end end end if smooth - smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, parent, within, terminal_weights) + smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, + parent, within, terminal_weights) end - # ── Shock decomposition ────────────────────────────────────────────────── - # A decomposition needs a shock path; the particle filter supplies one (the - # smoothed shocks above), so the same attribution the inversion filter uses - # applies here. At first order contributions are additive and the split is - # exact. At pruned higher order they are not additive, which is precisely - # what the Aumann-Shapley (marginal contribution) attribution is for, so the - # pruned decomposition reuses the routines in `inversion.jl`. Non-pruned - # `:second_order` / `:third_order` have no decomposition at all (the caller - # already turns `shock_decomposition` off for them). - # Column layout follows the inversion filter: with the Aumann-Shapley - # attribution (and at first order) it is [contributions…, baseline, total] = - # nExo+2; the sequential pruned attribution adds an explicit interaction and - # residual column, [contributions…, interaction, residual, total] = nExo+3. - sequential_pruned = algo ∈ (:pruned_second_order, :pruned_third_order) && !marginal_contribution - decomposition = zeros(nVars, sequential_pruned ? nExo + 3 : nExo + 2, nT) - decomposition[:, end, :] .= variables - - if algo == :first_order - 𝐒₁ = 𝐒 isa AbstractMatrix ? 𝐒 : 𝐒[1] - init_vec = state isa AbstractVector{<:AbstractVector} ? state[1] : state - sck = zeros(nExo) - @inbounds for i in 1:nExo - fill!(sck, 0.0); sck[i] = shocks_out[i, 1] - decomposition[:, i, 1] .= 𝐒₁ * vcat(init_vec[past_idx], sck) + # Fail visibly: a cloud this degenerate does not produce estimates worth + # reading, and the symptom (numbers that change every seed) is easy to + # mistake for a modelling problem. + if n_scored > 0 + ess_fraction = ess_sum / (n_scored * n_particles) + if ess_fraction < DEFAULT_PARTICLE_LOW_ESS_FRACTION + @warn "The particle cloud carried an effective sample size of only $(round(100 * ess_fraction, digits = 2))% of `n_particles` on average, so these estimates rest on a handful of distinct particles and will change materially from one `particle_rng` seed to the next. Use `filter = :tempered_particle`, which mutates the particles towards the data instead of only reweighting them, or raise `n_particles` / `measurement_error`." maxlog = 1 end - decomposition[:, end - 1, 1] .= decomposition[:, end, 1] - sum(decomposition[:, 1:end-2, 1], dims = 2) - for t in 2:nT - @inbounds for i in 1:nExo - fill!(sck, 0.0); sck[i] = shocks_out[i, t] - decomposition[:, i, t] .= 𝐒₁ * vcat(decomposition[past_idx, i, t-1], sck) + end + + return nothing +end + + +# The reported shock is the *conditional mean* μ(xₜ₋₁), not the shock that was +# drawn. Both are consistent for E[εₜ | y₁..ₜ], because +# E[εₜ | y₁..ₜ] = E[ E[εₜ | xₜ₋₁, yₜ] | y₁..ₜ ] = E[ μ(xₜ₋₁) | y₁..ₜ ], +# but the conditional mean has already integrated out the draw, so it carries none +# of that draw's variance — a Rao-Blackwellisation. It is exact whenever the +# correction weights are constant, which is precisely when the linearisation +# behind μ is exact, so the residual bias is of the same (second) order as the +# proposal's own approximation error. Switched by `DEFAULT_GUIDED_RAO_BLACKWELL`. + +function guided_estimates_loop!(::Val{algo}, tr, scr, parts::NTuple{K,Matrix{Float64}}, + parts_scratch::NTuple{K,Matrix{Float64}}, anc::NTuple{K,Matrix{Float64}}, + parts_proposed::NTuple{K,Matrix{Float64}}, sprop2::NTuple{K,Matrix{Float64}}, + Fbuf, pws, variables, stds, shocks_out, nT, observables_index, dat, + obs_idx_per_t, has_missing, me_var, inv_me_var, S₁, nPast, + resampling, resampling_threshold, n_mh, mh_scale, + r_star, max_stages, rng, smooth::Bool, log2pi) where {algo, K} + n_particles = size(pws.E, 2) + nVars = size(variables, 1) + nExo = size(shocks_out, 1) + nObs = length(observables_index) + + E, Mu, Tmp = pws.E, pws.E2, pws.Eprop + W, logλ, logw = pws.W, pws.logdens, pws.logw + idx, bins = pws.idx, pws.bins + R = Matrix{Float64}(undef, nObs, n_particles) + Z = Matrix{Float64}(undef, nExo, n_particles) + Eprop = Matrix{Float64}(undef, nExo, n_particles) + Tmp2 = Matrix{Float64}(undef, nExo, n_particles) + dv = Vector{Float64}(undef, n_particles) + dprop = Vector{Float64}(undef, n_particles) + Lvec = Vector{Float64}(undef, n_particles) + negL = Vector{Float64}(undef, n_particles) + comp = Vector{Int}(undef, n_particles) + accept = Vector{Bool}(undef, n_particles) + c = mh_scale + + fill!(W, 1.0 / n_particles) + + hist_states = smooth ? [Matrix{Float64}(undef, nVars, n_particles) for _ in 1:nT] : Matrix{Float64}[] + hist_shocks = smooth ? [Matrix{Float64}(undef, nExo, n_particles) for _ in 1:nT] : Matrix{Float64}[] + parent = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] + within = smooth ? [Int[] for _ in 1:nT] : Vector{Int}[] + terminal_weights = smooth ? fill(1.0 / n_particles, n_particles) : Float64[] + + gp_rows = Int[] + gp = GuidedProposal(length(observables_index), nExo) + rebuild_guided_proposal!(gp, S₁, observables_index, nPast, me_var, eachindex(observables_index), log2pi) + ess_sum = 0.0 + n_scored = 0 + + for t in 1:nT + rows = has_missing ? obs_idx_per_t[t] : eachindex(observables_index) + data_col = @view dat[:, t] + + if isempty(rows) + Random.randn!(rng, E) + propagate_cloud!(Val(algo), tr, scr, parts_scratch, parts, E) + parts, parts_scratch = parts_scratch, parts + F = full_states!(Fbuf, parts) + if smooth + copyto!(terminal_weights, W) + copyto!(hist_states[t], F) + copyto!(hist_shocks[t], E) + else + accumulate_filtered_moments!(variables, stds, shocks_out, t, F, E, W) end - decomposition[:, end - 1, t] .= decomposition[:, end, t] - sum(decomposition[:, 1:end-2, t], dims = 2) + continue end - elseif algo ∈ (:pruned_second_order, :pruned_third_order) && marginal_contribution - # The Aumann-Shapley attribution requires `variables` and `shocks` to lie - # on the *same* model trajectory — it checks that the contributions plus - # the zero-shock baseline reproduce `variables`. A smoothed mean is not a - # model path (averaging does not commute with the nonlinear transition: - # E[g(x,ε)] ≠ g(E[x],E[ε])), so feeding it in directly leaves a closure - # error that the routine tries to remove by refining its quadrature. - # Decompose the trajectory implied by the smoothed shocks instead, which - # is the same object the inversion filter decomposes and closes exactly. - traj = zeros(nVars, nT) - cur = deepcopy(state) - nxt = zeros_like_particle(cur) - buf = Vector{Float64}(undef, nVars) - shk = Vector{Float64}(undef, nExo) - for t in 1:nT - @inbounds for e in 1:nExo; shk[e] = shocks_out[e, t]; end - higher_propagate!(Val(algo), nxt, cur, shk, past_idx, 𝐒f, scr) - full = measurement_full(nxt, buf) - @inbounds for i in 1:nVars; traj[i, t] = full[i]; end - cur, nxt = nxt, cur + if !same_rows(gp_rows, rows) + rebuild_guided_proposal!(gp, S₁, observables_index, nPast, me_var, rows, log2pi) + gp_rows = collect(Int, rows) end - decomposition[:, end, :] .= traj - if algo == :pruned_second_order - aumann_shapley_shock_decomposition_pruned_2nd_order!(decomposition, traj, shocks_out, - state, 𝐒, T, nExo; verbose = opts.verbose) - else - aumann_shapley_shock_decomposition_pruned_3rd_order!(decomposition, traj, shocks_out, - state, 𝐒, T, nExo; verbose = opts.verbose) + # Solve for the shock that best explains yₜ from each ancestor. + fill!(E, 0.0) + propagate_cloud!(Val(algo), tr, scr, parts_scratch, parts, E) + residual_cloud!(R, full_states!(Fbuf, parts_scratch), data_col, observables_index, rows) + ℒ.mul!(Mu, proposal_K(gp), view(R, 1:length(rows), :)) + for _ in 1:DEFAULT_GUIDED_NEWTON_STEPS + propagate_cloud!(Val(algo), tr, scr, parts_scratch, parts, Mu) + residual_cloud!(R, full_states!(Fbuf, parts_scratch), data_col, observables_index, rows) + guided_newton_step!(Mu, R, Tmp, gp) end - elseif sequential_pruned - # Sequential attribution: run one trajectory per shock with only that - # shock switched on, plus one with all of them. Each single-shock path is - # that shock's contribution; the all-shock path minus the sum of the - # single-shock paths is the interaction the nonlinearity creates (this is - # the term the Aumann-Shapley variant instead distributes across shocks), - # and whatever is still left over goes into the residual column. - states_dec = [deepcopy(state) for _ in 1:nExo + 1] - nxt = zeros_like_particle(state) - dbuf = Vector{Float64}(undef, nVars) - single = zeros(nExo) - allsh = Vector{Float64}(undef, nExo) - - for t in 1:nT - @inbounds for ii in 1:nExo - fill!(single, 0.0); single[ii] = shocks_out[ii, t] - higher_propagate!(Val(algo), nxt, states_dec[ii], single, past_idx, 𝐒f, scr) - copy_particle!(states_dec[ii], nxt) - full = measurement_full(states_dec[ii], dbuf) - for v in 1:nVars; decomposition[v, ii, t] = full[v]; end + + # The ancestors are the cloud as it stands; keep a copy so the Metropolis + # rejuvenation below can re-propagate proposals from them. + copy_cloud!(anc, parts) + + Random.randn!(rng, Z) + ℒ.mul!(E, gp.Uinv, Z, DEFAULT_GUIDED_PROPOSAL_SCALE, 0.0) + @inbounds for i in eachindex(E) + E[i] += Mu[i] + end + propagate_cloud!(Val(algo), tr, scr, parts_scratch, parts, E) + parts, parts_scratch = parts_scratch, parts + F = full_states!(Fbuf, parts) + residual_cloud!(R, F, data_col, observables_index, rows) + + @inbounds for j in 1:n_particles + dv[j] = residual_quadform(R, j, inv_me_var, rows) + end + guided_bridge_gap!(Lvec, E, Mu, dv, gp) + + # Walk from the proposal to the conditional. Where the proposal is good the + # schedule reaches β = 1 in one step and this is the plain guided filter. + β_old = 0.0 + stage = 0 + @inbounds for j in 1:n_particles + negL[j] = -Lvec[j] + end + while β_old < 1.0 - 1e-12 && stage < max_stages + stage += 1 + β_new = any(isfinite, negL) ? tempered_next_phi(β_old, negL, r_star, n_particles) : 1.0 + @inbounds for j in 1:n_particles + logw[j] = (β_new - β_old) * Lvec[j] + end + isfinite(reweight_log_weights!(W, logw)) || (fill!(W, 1.0 / n_particles); break) + + if effective_sample_size(W) < resampling_threshold * n_particles + particle_resample_indices!(idx, bins, rng, W, resampling) + gather_cloud!(parts_scratch, parts, idx) + copy_cloud!(parts, parts_scratch) + gather_cloud!(parts_proposed, anc, idx) + copy_cloud!(anc, parts_proposed) + @inbounds for j in 1:n_particles + a = idx[j] + copy_col!(Eprop, j, E, a) + copy_col!(Tmp2, j, Mu, a) + dprop[j] = dv[a] + end + copyto!(E, Eprop) + copyto!(Mu, Tmp2) + copyto!(dv, dprop) + fill!(W, 1.0 / n_particles) + if smooth + if isempty(parent[t]) + parent[t] = copy(idx) + else + prev = parent[t] + @inbounds for j in 1:n_particles + comp[j] = prev[idx[j]] + end + parent[t] = copy(comp) + end + end + end + + for _ in 1:n_mh + acc = guided_anneal_mutate!(Val(algo), tr, scr, gp, parts, sprop2, anc, Fbuf, + E, Eprop, Z, R, Mu, dv, dprop, accept, c, β_new, + data_col, observables_index, inv_me_var, rows, rng) + c = adapt_mh_scale(c, acc) + end + guided_bridge_gap!(Lvec, E, Mu, dv, gp) + @inbounds for j in 1:n_particles + negL[j] = -Lvec[j] end + β_old = β_new + end + F = full_states!(Fbuf, parts) + ess_sum += effective_sample_size(W) + n_scored += 1 + @debug "guided period" t stages = stage adaptation_ess = effective_sample_size(W) / n_particles + + # Which shock to report. Without rejuvenation the conditional mean μ is the + # Rao-Blackwellised choice: E[εₜ|y₁..ₜ] = E[μ(xₜ₋₁)|y₁..ₜ], so averaging μ + # instead of the draw removes the draw's variance entirely. Once the + # particles have been mutated they are draws from the exact conditional + # rather than from the Gaussian μ centres, so μ is no longer their mean and + # the drawn shock is the consistent estimator. + reported = (DEFAULT_GUIDED_RAO_BLACKWELL && n_mh == 0 && max_stages <= 1) ? Mu : E + if smooth + copyto!(terminal_weights, W) + copyto!(hist_states[t], F) + copyto!(hist_shocks[t], reported) + else + accumulate_filtered_moments!(variables, stds, shocks_out, t, F, reported, W) + end - @inbounds for e in 1:nExo; allsh[e] = shocks_out[e, t]; end - higher_propagate!(Val(algo), nxt, states_dec[end], allsh, past_idx, 𝐒f, scr) - copy_particle!(states_dec[end], nxt) - full = measurement_full(states_dec[end], dbuf) + end + + if smooth + smooth_particle_trajectories!(variables, stds, shocks_out, hist_states, hist_shocks, + parent, within, terminal_weights) + end - # interaction = all-shock path − Σ single-shock paths - @inbounds for v in 1:nVars; decomposition[v, end - 2, t] = full[v]; end - decomposition[:, end - 2, t] .-= sum(decomposition[:, 1:end-3, t], dims = 2) - # residual = reported estimate − everything attributed so far - decomposition[:, end - 1, t] .= variables[:, t] - decomposition[:, end - 1, t] .-= sum(decomposition[:, 1:end-2, t], dims = 2) + if n_scored > 0 + ess_fraction = ess_sum / (n_scored * n_particles) + if ess_fraction < DEFAULT_PARTICLE_LOW_ESS_FRACTION + @warn "The guided proposal's importance weights carried an effective sample size of only $(round(100 * ess_fraction, digits = 2))% of `n_particles` on average, which means the observation is far from linear in the shock over this cloud and the closed-form proposal is a poor fit. Use `filter = :tempered_particle`, which makes no such assumption." maxlog = 1 end - else - @info "Shock decomposition is not available for $(algo) solutions (use a pruned solution); returning zeros." maxlog = 1 end - return variables, shocks_out, stds, decomposition + return nothing +end + +# Weighted mean and spread of the cloud in period `t`. Two passes over the +# already-summed full-state matrix `F` — the component sum that produces `F` is +# done once, before this is called, rather than once per pass. +function accumulate_filtered_moments!(variables, stds, shocks_out, t::Int, + F::Matrix{Float64}, E::Matrix{Float64}, W::Vector{Float64}) + nVars = size(variables, 1) + nExo = size(shocks_out, 1) + @inbounds for p in eachindex(W) + w = W[p] + for i in 1:nVars + variables[i, t] += w * F[i, p] + end + for e in 1:nExo + shocks_out[e, t] += w * E[e, p] + end + end + @inbounds for p in eachindex(W) + w = W[p] + for i in 1:nVars + d = F[i, p] - variables[i, t] + stds[i, t] += w * d * d + end + end + @inbounds for i in 1:nVars + stds[i, t] = sqrt(max(stds[i, t], 0.0)) + end + return nothing end # Backward pass of the particle smoother (fixed-interval smoothing by genealogy, @@ -2240,10 +3005,12 @@ function smooth_particle_trajectories!(variables::Matrix{Float64}, lineage = collect(1:n_particles) for t in nT:-1:1 - Hs = hist_states[t]; Hh = hist_shocks[t] + Hs = hist_states[t] + Hh = hist_shocks[t] @inbounds for p in 1:n_particles - a = lineage[p]; w = W[p] + a = lineage[p] + w = W[p] for i in 1:nVars variables[i, t] += w * Hs[i, a] end @@ -2252,7 +3019,8 @@ function smooth_particle_trajectories!(variables::Matrix{Float64}, end end @inbounds for p in 1:n_particles - a = lineage[p]; w = W[p] + a = lineage[p] + w = W[p] for i in 1:nVars stds[i, t] += w * (Hs[i, a] - variables[i, t])^2 end @@ -2285,13 +3053,75 @@ function smooth_particle_trajectories!(variables::Matrix{Float64}, return nothing end -# out = A·prev[past] + B·shock for a single particle (estimates path; the -# likelihood path propagates the whole swarm with one gemm instead). -@inline function linear_propagate_estimates!(out::Vector{Float64}, tr::LinearParticleTransition, - prev::Vector{Float64}, shock::Vector{Float64}) - ℒ.mul!(out, tr.A, prev) - ℒ.mul!(out, tr.B, shock, 1.0, 1.0) - return out + +# ── Deterministic trajectories for the shock decomposition ─────────────────── +# Both helpers reuse the batched transition with a handful of "particles": one +# per trajectory. The cloud machinery is exact for N = 1, so there is no separate +# single-state code path to keep in sync. + +# The model path implied by a given shock sequence. +function shock_path_trajectory(::Val{algo}, tr::ParticleTransition, scr, state, + shocks_out::Matrix{Float64}, nVars::Int, nT::Int) where {algo} + K = pruned_components(Val(algo)) + cur = alloc_cloud(K, nVars, 1) + nxt = alloc_cloud(K, nVars, 1) + fill_cloud_from_state!(cur, state) + E = Matrix{Float64}(undef, size(shocks_out, 1), 1) + F = Matrix{Float64}(undef, nVars, 1) + traj = zeros(nVars, nT) + + for t in 1:nT + @inbounds for e in axes(shocks_out, 1) + E[e, 1] = shocks_out[e, t] + end + propagate_cloud!(Val(algo), tr, scr, nxt, cur, E) + full = full_states!(F, nxt) + @inbounds for i in 1:nVars + traj[i, t] = full[i, 1] + end + cur, nxt = nxt, cur + end + + return traj +end + +# One trajectory per shock with only that shock switched on, plus one with all of +# them, all carried as the columns of a single cloud. +function sequential_shock_decomposition!(::Val{algo}, tr::ParticleTransition, scr, state, + shocks_out::Matrix{Float64}, variables::Matrix{Float64}, + decomposition::Array{Float64,3}, nVars::Int, nExo::Int, nT::Int) where {algo} + K = pruned_components(Val(algo)) + N = nExo + 1 + cur = alloc_cloud(K, nVars, N) + nxt = alloc_cloud(K, nVars, N) + fill_cloud_from_state!(cur, state) + E = zeros(Float64, nExo, N) + F = Matrix{Float64}(undef, nVars, N) + + for t in 1:nT + fill!(E, 0.0) + @inbounds for i in 1:nExo + E[i, i] = shocks_out[i, t] # column i: only shock i active + E[i, end] = shocks_out[i, t] # last column: every shock active + end + propagate_cloud!(Val(algo), tr, scr, nxt, cur, E) + full = full_states!(F, nxt) + cur, nxt = nxt, cur + + @inbounds for i in 1:nExo, v in 1:nVars + decomposition[v, i, t] = full[v, i] + end + # interaction = all-shock path − Σ single-shock paths + @inbounds for v in 1:nVars + decomposition[v, end - 2, t] = full[v, N] + end + decomposition[:, end - 2, t] .-= sum(decomposition[:, 1:end-3, t], dims = 2) + # residual = reported estimate − everything attributed so far + decomposition[:, end - 1, t] .= variables[:, t] + decomposition[:, end - 1, t] .-= sum(decomposition[:, 1:end-2, t], dims = 2) + end + + return nothing end end # @stable diff --git a/src/get_functions.jl b/src/get_functions.jl index 6d4d3ee4e..c3dd12f03 100644 --- a/src/get_functions.jl +++ b/src/get_functions.jl @@ -305,10 +305,10 @@ And data, 4×2×40 Array{Float64, 3}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -355,8 +355,8 @@ And data, 4×2×40 Array{Float64, 3}: if filter ∈ PARTICLE_FILTERS extra_kw = merge(extra_kw, (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, - particle_rng, tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale)) + particle_rng, particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale)) end if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG @@ -477,10 +477,10 @@ And data, 1×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_SELECTOR(filter), @@ -529,8 +529,8 @@ And data, 1×40 Matrix{Float64}: particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng, - tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale) : NamedTuple() + particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale) : NamedTuple() if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG @@ -634,10 +634,10 @@ And data, 4×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, @@ -687,8 +687,8 @@ And data, 4×40 Matrix{Float64}: particle_kw = filter ∈ PARTICLE_FILTERS ? (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng, - tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale) : NamedTuple() + particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale) : NamedTuple() if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG @@ -795,10 +795,10 @@ And data, 5×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, warmup_iterations::Int = DEFAULT_WARMUP_ITERATIONS, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, levels::Bool = DEFAULT_LEVELS, @@ -931,10 +931,10 @@ And data, 4×40 Matrix{Float64}: particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, data_in_levels::Bool = DEFAULT_DATA_IN_LEVELS, smooth::Bool = DEFAULT_SMOOTH_FLAG, verbose::Bool = DEFAULT_VERBOSE, @@ -989,8 +989,8 @@ And data, 4×40 Matrix{Float64}: (; measurement_error = resolve_measurement_error(filter, measurement_error, data_in_deviations), n_particles, particle_resampling, particle_resampling_threshold, particle_initial_state_scaling, particle_rng, - tempering_target_ratio, tempering_mh_steps, - tempering_max_stages, tempering_mh_scale) : NamedTuple() + particle_target_ratio, particle_mh_steps, + particle_max_stages, particle_mh_scale) : NamedTuple() if filter == :inversion && initial_covariance !== :theoretical @info "`initial_covariance` is not used by the inversion filter, which fixes the initial state and carries no state covariance. Ignoring input." maxlog = DEFAULT_MAXLOG @@ -1241,12 +1241,12 @@ The same can be achieved with the other input formats: S₂ = nothing if size(𝓂.caches.second_order_solution, 2) > 0 - S₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + S₂ = 𝓂.caches.second_order_solution end S₃ = nothing if algorithm ∈ [:third_order, :pruned_third_order] && size(𝓂.caches.third_order_solution, 2) > 0 - S₃ = 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ + S₃ = 𝓂.caches.third_order_solution end ensure_conditional_forecast_constants!(𝓂.constants; third_order = !isnothing(S₃)) @@ -1560,7 +1560,7 @@ function irf_forward_simulate!(::Val{:second_order}, shocks_store[si, t] = shock_hist[:, t] prev = states_store[si, t] aug = [prev[past_idx]; one(S); shocks_store[si, t]] - y_t = 𝐒₁ * aug + 𝐒₂ * ℒ.kron(aug, aug) / 2 + y_t = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / 2 states_store[si, t+1] = y_t Y_all[:, t, si] = y_t end @@ -1584,8 +1584,7 @@ function irf_forward_simulate!(::Val{:third_order}, shocks_store[si, t] = shock_hist[:, t] prev = states_store[si, t] aug = [prev[past_idx]; one(S); shocks_store[si, t]] - kaug = ℒ.kron(aug, aug) - y_t = 𝐒₁ * aug + 𝐒₂ * kaug / 2 + 𝐒₃ * ℒ.kron(kaug, aug) / 6 + y_t = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / 2 + 𝐒₃ * compressed_kron³_power(aug) / 6 states_store[si, t+1] = y_t Y_all[:, t, si] = y_t end @@ -4617,10 +4616,10 @@ function get_loglikelihood(𝓂::ℳ, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, @@ -4646,10 +4645,10 @@ function get_loglikelihood(𝓂::ℳ, particle_resampling_threshold = particle_resampling_threshold, particle_initial_state_scaling = particle_initial_state_scaling, particle_rng = particle_rng, - tempering_target_ratio = tempering_target_ratio, - tempering_mh_steps = tempering_mh_steps, - tempering_max_stages = tempering_max_stages, - tempering_mh_scale = tempering_mh_scale, + particle_target_ratio = particle_target_ratio, + particle_mh_steps = particle_mh_steps, + particle_max_stages = particle_max_stages, + particle_mh_scale = particle_mh_scale, tol = tol, quadratic_matrix_equation_algorithm = quadratic_matrix_equation_algorithm, lyapunov_algorithm = lyapunov_algorithm, @@ -4677,10 +4676,10 @@ function get_loglikelihood(𝓂::ℳ, particle_resampling_threshold::Real = DEFAULT_PARTICLE_RESAMPLING_THRESHOLD, particle_initial_state_scaling::Real = DEFAULT_PARTICLE_INITIAL_STATE_SCALING, particle_rng::Random.AbstractRNG = Random.default_rng(), - tempering_target_ratio::Real = DEFAULT_TEMPERING_TARGET_RATIO, - tempering_mh_steps::Int = DEFAULT_TEMPERING_MH_STEPS, - tempering_max_stages::Int = DEFAULT_TEMPERING_MAX_STAGES, - tempering_mh_scale::Real = DEFAULT_TEMPERING_MH_SCALE, + particle_target_ratio::Real = DEFAULT_PARTICLE_TARGET_RATIO, + particle_mh_steps::Int = DEFAULT_PARTICLE_MH_STEPS_SELECTOR(filter), + particle_max_stages::Int = DEFAULT_PARTICLE_MAX_STAGES, + particle_mh_scale::Real = DEFAULT_PARTICLE_MH_SCALE, tol::Tolerances = Tolerances(), quadratic_matrix_equation_algorithm::Symbol = DEFAULT_QME_SELECTOR(𝓂), lyapunov_algorithm::Symbol = DEFAULT_LYAPUNOV_ALGORITHM, @@ -4849,7 +4848,12 @@ function get_loglikelihood(𝓂::ℳ, constants_obj, state, 𝓂, - measurement_error_H, + # Narrowed here rather than into a local above: the + # `nothing` case is already rejected in the guard + # block, but that guard is a separate `if` and JET + # cannot correlate the two, so a local binding reads + # as possibly undefined. + measurement_error_H::Union{AbstractVector{<:Real},AbstractMatrix{<:Real}}, obs_idx_per_t, has_missing; n_particles = n_particles, @@ -4860,10 +4864,10 @@ function get_loglikelihood(𝓂::ℳ, presample_periods = presample_periods, initial_covariance = initial_covariance, on_failure_loglikelihood = on_failure_loglikelihood, - tempering_target_ratio = tempering_target_ratio, - tempering_mh_steps = tempering_mh_steps, - tempering_max_stages = tempering_max_stages, - tempering_mh_scale = tempering_mh_scale, + particle_target_ratio = particle_target_ratio, + particle_mh_steps = particle_mh_steps, + particle_max_stages = particle_max_stages, + particle_mh_scale = particle_mh_scale, opts = opts) elseif filter == :kalman if has_missing @@ -5338,13 +5342,13 @@ function filter_free_loglikelihood_loop(::Val{:second_order}, for t in 1:n_warm ϵ = view(shocks, :, t) aug = vcat(cur_state[past_idx], one(R), ϵ) - cur_state = 𝐒₁ * aug + 𝐒₂ * ℒ.kron(aug, aug) / R(2) + cur_state = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / R(2) end for t in 1:nT ϵ = view(shocks, :, n_warm + t) aug = vcat(cur_state[past_idx], one(R), ϵ) - new_state = 𝐒₁ * aug + 𝐒₂ * ℒ.kron(aug, aug) / R(2) + new_state = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / R(2) idx = obs_idx_per_t[t] if !isempty(idx) obs_dev = new_state[obs_indices[idx]] @@ -5380,15 +5384,13 @@ function filter_free_loglikelihood_loop(::Val{:third_order}, for t in 1:n_warm ϵ = view(shocks, :, t) aug = vcat(cur_state[past_idx], one(R), ϵ) - kaug = ℒ.kron(aug, aug) - cur_state = 𝐒₁ * aug + 𝐒₂ * kaug / R(2) + 𝐒₃ * ℒ.kron(kaug, aug) / R(6) + cur_state = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / R(2) + 𝐒₃ * compressed_kron³_power(aug) / R(6) end for t in 1:nT ϵ = view(shocks, :, n_warm + t) aug = vcat(cur_state[past_idx], one(R), ϵ) - kaug = ℒ.kron(aug, aug) - new_state = 𝐒₁ * aug + 𝐒₂ * kaug / R(2) + 𝐒₃ * ℒ.kron(kaug, aug) / R(6) + new_state = 𝐒₁ * aug + 𝐒₂ * compressed_kron²_power(aug) / R(2) + 𝐒₃ * compressed_kron³_power(aug) / R(6) idx = obs_idx_per_t[t] if !isempty(idx) obs_dev = new_state[obs_indices[idx]] diff --git a/src/occasionally_binding_constraints.jl b/src/occasionally_binding_constraints.jl index 8c8197574..0cf52c281 100644 --- a/src/occasionally_binding_constraints.jl +++ b/src/occasionally_binding_constraints.jl @@ -614,29 +614,34 @@ function obc_dYdx_nonpruned_higher!(Y, dYdx, state, shock_vals, zero_shock, S = eltype(Y) nv = size(Y, 1) n_x = size(dYdx, 2) - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution Ŝ₁̂ = [Ŝ₁[:, 1:n_past] zeros(S, nv) Ŝ₁[:, n_past+1:end]] n_aug = n_past + 1 + n_shocks has_third = algorithm == :third_order - 𝐒₃ = has_third ? 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ : nothing + 𝐒₃ = has_third ? 𝓂.caches.third_order_solution : nothing # ── t = 0 ── aug = [state[past_idx]; one(S); shock_vals] - kron_aug = ℒ.kron(aug, aug) - Y[:, 1] = Ŝ₁̂ * aug + 𝐒₂ * kron_aug / 2 - if has_third; Y[:, 1] += 𝐒₃ * ℒ.kron(kron_aug, aug) / 6; end + Y[:, 1] = Ŝ₁̂ * aug + 𝐒₂ * compressed_kron²_power(aug) / 2 + if has_third + Y[:, 1] += 𝐒₃ * compressed_kron³_power(aug) / 6 + end + # Directional derivatives. The forward weights ½ and ⅙ do not carry over: + # d/dε compressed_kron²_power(a + εd) = 2·compressed_kron²(a, d) and + # d/dε compressed_kron³_power(a + εd) = 3·compressed_kron³(a, a, d), so ½·2 + # is 1 (no factor below) and ⅙·3 is ½. The uncompressed form summed the two + # (resp. three) permutations itself and so divided by 2 (resp. 6) instead; + # the compressed kernels already include them, which is where the apparent + # factor-of-3 change comes from. `test/test_compressed_kron.jl` pins both. d_aug = zeros(S, n_aug) for j in 1:n_x fill!(d_aug, zero(S)) d_aug[n_past + 1 + obc_idx[j]] = one(S) - dYdx[:, j, 1] = Ŝ₁̂ * d_aug + - 𝐒₂ * (ℒ.kron(d_aug, aug) + ℒ.kron(aug, d_aug)) / 2 + dYdx[:, j, 1] = Ŝ₁̂ * d_aug + 𝐒₂ * compressed_kron²(d_aug, aug) if has_third - dYdx[:, j, 1] += 𝐒₃ * (ℒ.kron(ℒ.kron(d_aug, aug), aug) + - ℒ.kron(ℒ.kron(aug, d_aug), aug) + - ℒ.kron(kron_aug, d_aug)) / 6 + dYdx[:, j, 1] += 𝐒₃ * compressed_kron³(d_aug, aug, aug) / 2 end end @@ -644,19 +649,17 @@ function obc_dYdx_nonpruned_higher!(Y, dYdx, state, shock_vals, zero_shock, d_aug_t = zeros(S, n_aug) for t in 1:periods aug_t = [Y[past_idx, t]; one(S); zeros(S, n_shocks)] - kron_aug_t = ℒ.kron(aug_t, aug_t) - Y[:, t+1] = Ŝ₁̂ * aug_t + 𝐒₂ * kron_aug_t / 2 - if has_third; Y[:, t+1] += 𝐒₃ * ℒ.kron(kron_aug_t, aug_t) / 6; end + Y[:, t+1] = Ŝ₁̂ * aug_t + 𝐒₂ * compressed_kron²_power(aug_t) / 2 + if has_third + Y[:, t+1] += 𝐒₃ * compressed_kron³_power(aug_t) / 6 + end for j in 1:n_x fill!(d_aug_t, zero(S)) d_aug_t[1:n_past] .= @view dYdx[past_idx, j, t] - dYdx[:, j, t+1] = Ŝ₁̂ * d_aug_t + - 𝐒₂ * (ℒ.kron(d_aug_t, aug_t) + ℒ.kron(aug_t, d_aug_t)) / 2 + dYdx[:, j, t+1] = Ŝ₁̂ * d_aug_t + 𝐒₂ * compressed_kron²(d_aug_t, aug_t) if has_third - dYdx[:, j, t+1] += 𝐒₃ * (ℒ.kron(ℒ.kron(d_aug_t, aug_t), aug_t) + - ℒ.kron(ℒ.kron(aug_t, d_aug_t), aug_t) + - ℒ.kron(kron_aug_t, d_aug_t)) / 6 + dYdx[:, j, t+1] += 𝐒₃ * compressed_kron³(d_aug_t, aug_t, aug_t) / 2 end end end @@ -670,12 +673,12 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, S = eltype(Y) nv = size(Y, 1) n_x = size(dYdx, 2) - 𝐒₂ = 𝓂.caches.second_order_solution * 𝓂.constants.second_order.𝐔₂ + 𝐒₂ = 𝓂.caches.second_order_solution Ŝ₁̂ = [Ŝ₁[:, 1:n_past] zeros(S, nv) Ŝ₁[:, n_past+1:end]] n_aug = n_past + 1 + n_shocks has_third = algorithm == :pruned_third_order - 𝐒₃ = has_third ? 𝓂.caches.third_order_solution * 𝓂.constants.third_order.𝐔₃ : nothing + 𝐒₃ = has_third ? 𝓂.caches.third_order_solution : nothing # Component vectors y₁ = state isa AbstractVector{<:AbstractVector} ? state[1] : state @@ -694,7 +697,7 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, y₁_new = Ŝ₁̂ * aug₁ aug₂ = [y₂[past_idx]; zero(S); zeros(S, n_shocks)] - kron_aug₁ = ℒ.kron(aug₁, aug₁) + kron_aug₁ = compressed_kron²_power(aug₁) y₂_new = Ŝ₁̂ * aug₂ + 𝐒₂ * kron_aug₁ / 2 for j in 1:n_x @@ -702,22 +705,20 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, d_aug[n_past + 1 + obc_idx[j]] = one(S) dy₁dx[:, j] = Ŝ₁̂ * d_aug # dy₂ only depends on aug₁ perturbation (aug₂ initial is independent of x) - dy₂dx[:, j] = 𝐒₂ * (ℒ.kron(d_aug, aug₁) + ℒ.kron(aug₁, d_aug)) / 2 + dy₂dx[:, j] = 𝐒₂ * compressed_kron²(d_aug, aug₁) end if has_third aug₁̂ = [y₁[past_idx]; zero(S); shock_vals] aug₃ = [y₃[past_idx]; zero(S); zeros(S, n_shocks)] - y₃_new = Ŝ₁̂ * aug₃ + 𝐒₂ * ℒ.kron(aug₁̂, aug₂) + 𝐒₃ * ℒ.kron(kron_aug₁, aug₁) / 6 + y₃_new = Ŝ₁̂ * aug₃ + 𝐒₂ * compressed_kron²(aug₁̂, aug₂) + 𝐒₃ * compressed_kron³_power(aug₁) / 6 for j in 1:n_x fill!(d_aug, zero(S)) d_aug[n_past + 1 + obc_idx[j]] = one(S) d_aug₁̂ = copy(d_aug); d_aug₁̂[n_past + 1] = zero(S) # hat: zero for the "1" slot - dy₃dx[:, j] = 𝐒₂ * (ℒ.kron(d_aug₁̂, aug₂) + ℒ.kron(aug₁̂, zeros(S, n_aug))) + - 𝐒₃ * (ℒ.kron(ℒ.kron(d_aug, aug₁), aug₁) + - ℒ.kron(ℒ.kron(aug₁, d_aug), aug₁) + - ℒ.kron(kron_aug₁, d_aug)) / 6 + dy₃dx[:, j] = 𝐒₂ * compressed_kron²(d_aug₁̂, aug₂) + + 𝐒₃ * compressed_kron³(d_aug, aug₁, aug₁) / 2 end y₃ = y₃_new end @@ -732,7 +733,7 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, d_aug_t = zeros(S, n_aug) for t in 1:periods aug₁_t = [y₁[past_idx]; one(S); zeros(S, n_shocks)] - kron_aug₁_t = ℒ.kron(aug₁_t, aug₁_t) + kron_aug₁_t = compressed_kron²_power(aug₁_t) y₁_new = Ŝ₁̂ * aug₁_t aug₂_t = [y₂[past_idx]; zero(S); zeros(S, n_shocks)] @@ -749,13 +750,13 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, d_aug₂_t = zeros(S, n_aug) d_aug₂_t[1:n_past] .= @view dy₂dx[past_idx, j] dy₂dx_new[:, j] = Ŝ₁̂ * d_aug₂_t + - 𝐒₂ * (ℒ.kron(d_aug_t, aug₁_t) + ℒ.kron(aug₁_t, d_aug_t)) / 2 + 𝐒₂ * compressed_kron²(d_aug_t, aug₁_t) end if has_third aug₁̂_t = [y₁[past_idx]; zero(S); zeros(S, n_shocks)] aug₃_t = [y₃[past_idx]; zero(S); zeros(S, n_shocks)] - y₃_new = Ŝ₁̂ * aug₃_t + 𝐒₂ * ℒ.kron(aug₁̂_t, aug₂_t) + 𝐒₃ * ℒ.kron(kron_aug₁_t, aug₁_t) / 6 + y₃_new = Ŝ₁̂ * aug₃_t + 𝐒₂ * compressed_kron²(aug₁̂_t, aug₂_t) + 𝐒₃ * compressed_kron³_power(aug₁_t) / 6 dy₃dx_new = zeros(S, nv, n_x) for j in 1:n_x @@ -770,10 +771,8 @@ function obc_dYdx_pruned!(Y, dYdx, state, shock_vals, zero_shock, d_aug₃_t[1:n_past] .= @view dy₃dx[past_idx, j] dy₃dx_new[:, j] = Ŝ₁̂ * d_aug₃_t + - 𝐒₂ * (ℒ.kron(d_aug₁̂_t, aug₂_t) + ℒ.kron(aug₁̂_t, d_aug₂_t)) + - 𝐒₃ * (ℒ.kron(ℒ.kron(d_aug_t, aug₁_t), aug₁_t) + - ℒ.kron(ℒ.kron(aug₁_t, d_aug_t), aug₁_t) + - ℒ.kron(kron_aug₁_t, d_aug_t)) / 6 + 𝐒₂ * (compressed_kron²(d_aug₁̂_t, aug₂_t) + compressed_kron²(aug₁̂_t, d_aug₂_t)) + + 𝐒₃ * compressed_kron³(d_aug_t, aug₁_t, aug₁_t) / 2 end y₃ = y₃_new dy₃dx .= dy₃dx_new diff --git a/src/options_and_caches.jl b/src/options_and_caches.jl index e8f7111b8..1ec1a1c9d 100644 --- a/src/options_and_caches.jl +++ b/src/options_and_caches.jl @@ -87,6 +87,7 @@ function Second_order_indices() empty_sparse_int = SparseMatrixCSC{Int, Int64}(ℒ.I, 0, 0) empty_sparse_float = spzeros(Float64, 0, 0) empty_matrix_float = Matrix{Float64}(undef, 0, 0) + empty_matrix_int = Matrix{Int}(undef, 0, 0) return second_order_indices( # Auxiliary matrices (𝛔, 𝛔_sym, 𝛔c₂, 𝛔𝐂₂, 𝐂₂, 𝐔₂, 𝐔∇₂, 𝐈ₙ₊, 𝐈ₙ₋) empty_sparse_int, @@ -128,10 +129,17 @@ function Second_order_indices() empty_matrix_float, # I_exo empty_matrix_float, # I_state_vol empty_matrix_float, # I_aug + empty_matrix_int, # compressed_pair_index_map + Int[], # shockvar_cols + Int[], # shock²_cols + Int[], # var_vol²_cols # Conditional forecast indices Int[], # var²_idxs Int[], # shockvar²_idxs Int[], # shockvar_no_vol_idxs + Int[], # var²_cols + Int[], # shockvar²_cols + Int[], # shockvar_no_vol_cols # Moment computation caches BitVector(), # kron_states empty_sparse_float, # I_plus_s_s @@ -194,7 +202,14 @@ function Third_order_indices() Int[], # shockvar3_idxs Int[], # shockvar³2_idxs Int[], # shockvar³_idxs - empty_sparse_float, # I_exo2 + Int[], # shock_state_state_idxs + Int[], # shock_state_state_rows + Int[], # shock_shock_state_idxs + Int[], # shock_shock_state_rows + Int[], # var_vol³_cols + Int[], # shock³_cols + Int[], # shockvar³2_cols + Int[], # shockvar³_cols # Moment computation caches Float64[], # e6 BitVector(), # kron_e_v @@ -279,11 +294,11 @@ All buffers are initialized to 0-dimensional objects and resized on-demand via e function Find_shocks_workspace(::Type{TT} = Float64) where {TT <: Real} find_shocks_workspace{TT}( 0, # n_exo dimension - zeros(TT,0), # kron_buffer (n_exo^2) - zeros(TT,0,0), # kron_buffer2 (n_exo × n_exo) - zeros(TT,0), # kron_buffer² (n_exo^3) - zeros(TT,0,0), # kron_buffer3 (n_exo × n_exo^2) - zeros(TT,0,0), # kron_buffer4 (n_exo^2 × n_exo) + zeros(TT,0), # kron_buffer (compressed n_exo pair) + zeros(TT,0,0), # kron_buffer2 (compressed n_exo pair × n_exo) + zeros(TT,0), # kron_buffer² (compressed n_exo triple) + zeros(TT,0,0), # kron_buffer3 (compressed n_exo triple × n_exo) + zeros(TT,0,0), # kron_buffer4 (compressed n_exo triple × compressed n_exo pair) 0, # n_past dimension zeros(TT,0), # kron_state_vol zeros(TT,0), # kron_state_vol3 @@ -393,18 +408,20 @@ end """ ensure_sss_kron_buffers!(ws, nPast; third_order=false) -Lazily (re)allocate kron! buffers used by the stochastic-steady-state Newton iter + Lazily (re)allocate compressed-kron buffers used by the stochastic-steady-state Newton iter on `ws` (a `higher_order_workspace`). `nPast` is `T.nPast_not_future_and_mixed`. The 3rd-order-only buffers are only sized when `third_order=true`. """ function ensure_sss_kron_buffers!(ws::higher_order_workspace{S,G,H}, nPast::Int; third_order::Bool=false) where {S <: Real, G <: AbstractFloat, H <: Real} n_aug = nPast + 1 length(ws.x_aug_buf) == n_aug || (ws.x_aug_buf = zeros(S, n_aug)) - length(ws.kron_x_aug_xx) == n_aug^2 || (ws.kron_x_aug_xx = zeros(S, n_aug^2)) - size(ws.kron_x_aug_I) == (n_aug * nPast, nPast) || (ws.kron_x_aug_I = zeros(S, n_aug * nPast, nPast)) + n_aug2 = n_aug * (n_aug + 1) ÷ 2 + length(ws.kron_x_aug_xx) == n_aug2 || (ws.kron_x_aug_xx = zeros(S, n_aug2)) + size(ws.kron_x_aug_I) == (n_aug2, nPast) || (ws.kron_x_aug_I = zeros(S, n_aug2, nPast)) if third_order - length(ws.kron_x_aug_x_kron) == n_aug^3 || (ws.kron_x_aug_x_kron = zeros(S, n_aug^3)) - size(ws.kron_x_kron_I) == (n_aug^2 * nPast, nPast) || (ws.kron_x_kron_I = zeros(S, n_aug^2 * nPast, nPast)) + n_aug3 = n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6 + length(ws.kron_x_aug_x_kron) == n_aug3 || (ws.kron_x_aug_x_kron = zeros(S, n_aug3)) + size(ws.kron_x_kron_I) == (n_aug3, nPast) || (ws.kron_x_kron_I = zeros(S, n_aug3, nPast)) end return ws end @@ -834,14 +851,14 @@ end Ensure the find_shocks workspaces are allocated for the given number of shocks. Only allocates 3rd order buffers if third_order=true. -Buffer sizes: kron_buffer (n_exo^2), kron_buffer2 (n_exo^2 × n_exo), - kron_buffer² (n_exo^3), kron_buffer3 (n_exo^3 × n_exo), kron_buffer4 (n_exo^3 × n_exo^2) +Buffer sizes: compressed pair/triple shock coordinates, with matrix buffers using +the corresponding compressed row and column counts. """ function ensure_find_shocks_buffers!(ws::find_shocks_workspace{T}, n_exo::Int; third_order::Bool = false) where T ws.n_exo = n_exo - n_exo² = n_exo^2 - n_exo³ = n_exo^3 + n_exo² = n_exo * (n_exo + 1) ÷ 2 + n_exo³ = n_exo * (n_exo + 1) * (n_exo + 2) ÷ 6 # 2nd order buffers (always needed) if length(ws.kron_buffer) != n_exo² @@ -881,23 +898,26 @@ function ensure_find_shocks_state_buffers!(ws::find_shocks_workspace{T}, n_exo:: ws.n_past = n_past n_aug = n_past + 1 - if length(ws.kron_state_vol) != n_aug^2 - ws.kron_state_vol = zeros(T, n_aug^2) + n_aug² = n_aug * (n_aug + 1) ÷ 2 + if length(ws.kron_state_vol) != n_aug² + ws.kron_state_vol = zeros(T, n_aug²) end if size(ws.kron_I_state) != (n_exo * n_aug, n_exo) ws.kron_I_state = zeros(T, n_exo * n_aug, n_exo) end if third_order - if length(ws.kron_state_vol3) != n_aug^3 - ws.kron_state_vol3 = zeros(T, n_aug^3) + n_aug³ = n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6 + if length(ws.kron_state_vol3) != n_aug³ + ws.kron_state_vol3 = zeros(T, n_aug³) end - if size(ws.kron_I_state_state) != (n_exo * n_aug^2, n_exo) - ws.kron_I_state_state = zeros(T, n_exo * n_aug^2, n_exo) + if size(ws.kron_I_state_state) != (n_exo * n_aug², n_exo) + ws.kron_I_state_state = zeros(T, n_exo * n_aug², n_exo) end if third_order_pruning - if length(ws.kron_state₁₂) != n_past^2 - ws.kron_state₁₂ = zeros(T, n_past^2) + n_past² = n_past * (n_past + 1) ÷ 2 + if length(ws.kron_state₁₂) != n_past² + ws.kron_state₁₂ = zeros(T, n_past²) end if size(ws.kron_I_state₂) != (n_exo * n_past, n_exo) ws.kron_I_state₂ = zeros(T, n_exo * n_past, n_exo) @@ -918,16 +938,16 @@ All buffers are initialized to 0-dimensional objects and resized on-demand via e function Inversion_workspace(::Type{TT} = Float64) where {TT <: Real} inversion_workspace{TT}( 0, 0, # n_exo, n_past dimensions - zeros(TT, 0), # kron_buffer (n_exo^2) - zeros(TT, 0, 0), # kron_buffer2 (n_exo^2 × n_exo) - zeros(TT, 0), # kron_buffer² (n_exo^3) - zeros(TT, 0, 0), # kron_buffer3 (n_exo^3 × n_exo) - zeros(TT, 0, 0), # kron_buffer4 (n_exo^3 × n_exo^2) + zeros(TT, 0), # kron_buffer (compressed n_exo pair) + zeros(TT, 0, 0), # kron_buffer2 (compressed n_exo pair × n_exo) + zeros(TT, 0), # kron_buffer² (compressed n_exo triple) + zeros(TT, 0, 0), # kron_buffer3 (compressed n_exo triple × n_exo) + zeros(TT, 0, 0), # kron_buffer4 (compressed n_exo triple × compressed n_exo pair) zeros(TT, 0, 0), # kron_buffer_state (n_exo × n_past+1) zeros(TT, 0), # kron_shock_state (n_exo * (n_past+1)) - zeros(TT, 0), # kronstate_vol ((n_past+1)^2) - zeros(TT, 0), # kronaug_state ((n_past+1+n_exo)^2) - zeros(TT, 0), # kron_kron_aug_state ((n_past+1+n_exo)^3) + zeros(TT, 0), # kronstate_vol (compressed past-state pair) + zeros(TT, 0), # kronaug_state (compressed augmented-state pair) + zeros(TT, 0), # kron_kron_aug_state (compressed augmented-state triple) zeros(TT, 0), # state_vol (n_past+1) zeros(TT, 0), # aug_state₁ (n_past+1+n_exo) zeros(TT, 0), # aug_state₂ (n_past+1+n_exo) @@ -937,7 +957,7 @@ function Inversion_workspace(::Type{TT} = Float64) where {TT <: Real} zeros(TT, 0), # init_guess (n_exo) zeros(TT, 0, 0), # Si_buffer (n_cond_var × n_exo) zeros(TT, 0, 0), # jacc_buffer (n_cond_var × n_exo) - zeros(TT, 0, 0), # Si2e_buffer (n_cond_var × n_exo^2) + zeros(TT, 0, 0), # Si2e_buffer (n_cond_var × compressed n_exo pair) zeros(TT, 0), # y_obs (n_cond_var) zeros(TT, 0), # x_shocks (n_exo) zeros(TT, 0), # state_concat (n_past + n_exo) @@ -947,13 +967,13 @@ function Inversion_workspace(::Type{TT} = Float64) where {TT <: Real} zeros(TT, 0), # aug_state₃ (n_past+1+n_exo) zeros(TT, 0), # aug_state₁̂ (n_past+1+n_exo) zeros(TT, 0), # state²⁻_vol (n_past+1) - zeros(TT, 0), # kronstate_vol³ ((n_past+1)^3) - zeros(TT, 0), # kron_buffer2ss (n_past^2) - zeros(TT, 0, 0), # kron_buffer3sv (n_exo*(n_past+1)^2 × n_exo) - zeros(TT, 0, 0), # kron_buffer4sv (n_exo^2*(n_past+1) × n_exo^2) - zeros(TT, 0), # kron_shock_state2 (n_exo * (n_past+1)^2) - zeros(TT, 0), # kron_shock2_state (n_exo^2 * (n_past+1)) - zeros(TT, 0), # kronaug_state_aux ((n_past+1+n_exo)^2) + zeros(TT, 0), # kronstate_vol³ (compressed state triple) + zeros(TT, 0), # kron_buffer2ss (compressed past-state pair) + zeros(TT, 0, 0), # kron_buffer3sv (shock × compressed state pair × shock) + zeros(TT, 0, 0), # kron_buffer4sv (compressed shock pair × state) + zeros(TT, 0), # kron_shock_state2 (shock × compressed state pair) + zeros(TT, 0), # kron_shock2_state (compressed shock pair × state) + zeros(TT, 0), # kronaug_state_aux (compressed augmented-state pair) # Pullback buffers (for reverse-mode AD) zeros(TT, 0, 0), # ∂_tmp1 (n_exo × n_past+n_exo) zeros(TT, 0, 0), # ∂_tmp2 (n_past × n_past+n_exo) @@ -962,7 +982,7 @@ function Inversion_workspace(::Type{TT} = Float64) where {TT <: Real} zeros(TT, 0, 0), # ∂data (n_past × n_periods) # Pullback buffers for pruned second order zeros(TT, 0, 0), # ∂𝐒ⁱ²ᵉtmp (n_exo × n_exo*n_obs) - zeros(TT, 0, 0), # ∂𝐒ⁱ²ᵉtmp2 (n_obs × n_exo^2) + zeros(TT, 0, 0), # ∂𝐒ⁱ²ᵉtmp2 (n_obs × compressed n_exo pair) zeros(TT, 0), # kronSλ (n_obs * n_exo) zeros(TT, 0), # kronxS (n_exo * n_obs) # Per-period sequence buffers captured by rrule pullbacks (grown lazily @@ -994,10 +1014,14 @@ function ensure_inversion_buffers!(ws::inversion_workspace{T}, n_exo::Int, n_pas ws.n_exo = n_exo ws.n_past = n_past - n_exo² = n_exo^2 - n_exo³ = n_exo^3 + n_exo² = n_exo * (n_exo + 1) ÷ 2 + n_exo³ = n_exo * (n_exo + 1) * (n_exo + 2) ÷ 6 n_state_vol = n_past + 1 n_aug = n_past + 1 + n_exo + n_state_vol² = n_state_vol * (n_state_vol + 1) ÷ 2 + n_state_vol³ = n_state_vol * (n_state_vol + 1) * (n_state_vol + 2) ÷ 6 + n_aug² = n_aug * (n_aug + 1) ÷ 2 + n_aug³ = n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6 # Shock-related kron buffers (2nd order) if length(ws.kron_buffer) != n_exo² @@ -1028,15 +1052,15 @@ function ensure_inversion_buffers!(ws::inversion_workspace{T}, n_exo::Int, n_pas if length(ws.kron_shock_state) != n_exo * n_state_vol ws.kron_shock_state = zeros(T, n_exo * n_state_vol) end - if length(ws.kronstate_vol) != n_state_vol^2 - ws.kronstate_vol = zeros(T, n_state_vol^2) + if length(ws.kronstate_vol) != n_state_vol² + ws.kronstate_vol = zeros(T, n_state_vol²) end - if length(ws.kronaug_state) != n_aug^2 - ws.kronaug_state = zeros(T, n_aug^2) + if length(ws.kronaug_state) != n_aug² + ws.kronaug_state = zeros(T, n_aug²) end if third_order - if length(ws.kron_kron_aug_state) != n_aug^3 - ws.kron_kron_aug_state = zeros(T, n_aug^3) + if length(ws.kron_kron_aug_state) != n_aug³ + ws.kron_kron_aug_state = zeros(T, n_aug³) end end @@ -1073,26 +1097,28 @@ function ensure_inversion_buffers!(ws::inversion_workspace{T}, n_exo::Int, n_pas if length(ws.state²⁻_vol) != n_state_vol ws.state²⁻_vol = zeros(T, n_state_vol) end - if length(ws.kronstate_vol³) != n_state_vol^3 - ws.kronstate_vol³ = zeros(T, n_state_vol^3) + if length(ws.kronstate_vol³) != n_state_vol³ + ws.kronstate_vol³ = zeros(T, n_state_vol³) end - if length(ws.kron_buffer2ss) != n_past^2 - ws.kron_buffer2ss = zeros(T, n_past^2) + n_past² = n_past * (n_past + 1) ÷ 2 + if length(ws.kron_buffer2ss) != n_past² + ws.kron_buffer2ss = zeros(T, n_past²) end - if size(ws.kron_buffer3sv, 1) != n_exo * n_state_vol^2 || size(ws.kron_buffer3sv, 2) != n_exo - ws.kron_buffer3sv = zeros(T, n_exo * n_state_vol^2, n_exo) + n_state_vol² = n_state_vol * (n_state_vol + 1) ÷ 2 + if size(ws.kron_buffer3sv, 1) != n_exo * n_state_vol² || size(ws.kron_buffer3sv, 2) != n_exo + ws.kron_buffer3sv = zeros(T, n_exo * n_state_vol², n_exo) end if size(ws.kron_buffer4sv, 1) != n_exo² * n_state_vol || size(ws.kron_buffer4sv, 2) != n_exo² ws.kron_buffer4sv = zeros(T, n_exo² * n_state_vol, n_exo²) end - if length(ws.kron_shock_state2) != n_exo * n_state_vol^2 - ws.kron_shock_state2 = zeros(T, n_exo * n_state_vol^2) + if length(ws.kron_shock_state2) != n_exo * n_state_vol² + ws.kron_shock_state2 = zeros(T, n_exo * n_state_vol²) end if length(ws.kron_shock2_state) != n_exo² * n_state_vol ws.kron_shock2_state = zeros(T, n_exo² * n_state_vol) end - if length(ws.kronaug_state_aux) != n_aug^2 - ws.kronaug_state_aux = zeros(T, n_aug^2) + if length(ws.kronaug_state_aux) != n_aug² + ws.kronaug_state_aux = zeros(T, n_aug²) end end @@ -1107,7 +1133,7 @@ Ensure observation-dimension-dependent estimation buffers are allocated. Call after ensure_inversion_buffers! when the number of conditioning variables (observables) is known. """ function ensure_inversion_estimation_buffers!(ws::inversion_workspace{T}, n_exo::Int, n_cond_var::Int; third_order::Bool = false) where T - n_exo² = n_exo^2 + n_exo² = n_exo * (n_exo + 1) ÷ 2 if ws.n_cond_var == n_cond_var && length(ws.shock_independent) == n_cond_var && size(ws.Si_buffer) == (n_cond_var, n_exo) && size(ws.JJt_buf) == (n_cond_var, n_cond_var) && @@ -1171,7 +1197,7 @@ function ensure_inversion_rrule_buffers!(ws::inversion_workspace{T}, order::Symbol = :first_order) where T n_aug = n_past + 1 + n_exo n_vol = n_past + 1 - n_exo² = n_exo^2 + n_exo² = n_exo * (n_exo + 1) ÷ 2 grow_vec_seq!(ws.x_seq_rrule, Tt, n_exo, T) @@ -1272,8 +1298,7 @@ buffers start empty and are sized on demand by `ensure_particle_workspace!`. function Particle_workspace(::Type{TT} = Float64) where {TT <: Real} particle_workspace{TT}( 0, 0, 0, # nVars, nExo, n_particles - zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # X, X2, Anc - zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # Anc2, St, St2 + Matrix{TT}[], # pools zeros(TT, 0, 0), zeros(TT, 0, 0), zeros(TT, 0, 0), # E, E2, Eprop zeros(TT, 0), zeros(TT, 0), zeros(TT, 0), # W, Wn, logdens zeros(TT, 0), zeros(TT, 0), zeros(TT, 0), # logw, lam, dv @@ -1292,12 +1317,7 @@ function ensure_particle_workspace!(workspaces::workspaces, nVars::Int, nExo::In ws = workspaces.particle if ws.nVars != nVars || ws.n_particles != n_particles - ws.X = Matrix{Float64}(undef, nVars, n_particles) - ws.X2 = Matrix{Float64}(undef, nVars, n_particles) - ws.Anc = Matrix{Float64}(undef, nVars, n_particles) - ws.Anc2 = Matrix{Float64}(undef, nVars, n_particles) - ws.St = Matrix{Float64}(undef, nVars, n_particles) - ws.St2 = Matrix{Float64}(undef, nVars, n_particles) + empty!(ws.pools) # a dimension change invalidates every state buffer ws.nVars = nVars end @@ -1324,6 +1344,20 @@ function ensure_particle_workspace!(workspaces::workspaces, nVars::Int, nExo::In return ws end +""" + ensure_particle_pools!(ws, n) + +Guarantee that the workspace holds at least `n` `nVars × n_particles` state +buffers and return the pool vector. The particle filters take these in groups of +one per pruned state component, so `n` is `n_groups * n_components`. +""" +function ensure_particle_pools!(ws::particle_workspace{Float64}, n::Int) + while length(ws.pools) < n + push!(ws.pools, Matrix{Float64}(undef, ws.nVars, ws.n_particles)) + end + return ws.pools +end + """ Kalman_workspace(::Type{TT} = Float64) @@ -1806,6 +1840,16 @@ function ensure_computational_constants!(constants::constants) I_exo = Matrix{Float64}(ℒ.I, nᵉ, nᵉ) I_state_vol = Matrix{Float64}(ℒ.I, nˢ + 1, nˢ + 1) I_aug = Matrix{Float64}(ℒ.I, nˢ + 1 + nᵉ, nˢ + 1 + nᵉ) + n_aug = nˢ + 1 + nᵉ + compressed_pair_index_map = Matrix{Int}(undef, n_aug, n_aug) + @inbounds for i in 1:n_aug + base = (i - 1) * i ÷ 2 + for j in 1:i + index = base + j + compressed_pair_index_map[i, j] = index + compressed_pair_index_map[j, i] = index + end + end so.s_in_s⁺ = s_in_s⁺ so.s_in_s = s_in_s @@ -1827,6 +1871,7 @@ function ensure_computational_constants!(constants::constants) so.I_exo = I_exo so.I_state_vol = I_state_vol so.I_aug = I_aug + so.compressed_pair_index_map = compressed_pair_index_map end return constants.second_order @@ -1834,6 +1879,9 @@ end function ensure_conditional_forecast_constants!(constants::constants; third_order::Bool = false) so = ensure_computational_constants!(constants) + T = constants.post_model_macro + nᵉ = T.nExo + nˢ = T.nPast_not_future_and_mixed if isempty(so.var²_idxs) s_in_s⁺ = so.s_in_s @@ -1851,6 +1899,16 @@ function ensure_conditional_forecast_constants!(constants::constants; third_orde so.var_vol²_idxs = var_vol²_idxs end + if isempty(so.var²_cols) + n_global = nˢ + 1 + nᵉ + so.shockvar_cols = compressed_pair_indices(so.shockvar_idxs, n_global) + so.shock²_cols = compressed_pair_indices(so.shock²_idxs, n_global) + so.var_vol²_cols = compressed_pair_indices(so.var_vol²_idxs, n_global) + so.var²_cols = compressed_pair_indices(so.var²_idxs, n_global) + so.shockvar²_cols = compressed_pair_indices(so.shockvar²_idxs, n_global) + so.shockvar_no_vol_cols = compressed_pair_indices(so.shockvar_no_vol_idxs, n_global) + end + if third_order to = constants.third_order if isempty(to.var_vol³_idxs) @@ -1877,10 +1935,23 @@ function ensure_conditional_forecast_constants!(constants::constants; third_orde to.shockvar3_idxs = shockvar3_idxs to.shockvar³2_idxs = shockvar³2_idxs to.shockvar³_idxs = shockvar³_idxs + + end + + if isempty(to.var_vol³_cols) + n_global = nˢ + 1 + nᵉ + to.var_vol³_cols = compressed_triple_indices(to.var_vol³_idxs, n_global) + to.shock³_cols = compressed_triple_indices(to.shock³_idxs, n_global) + to.shockvar³2_cols = compressed_triple_indices(to.shockvar³2_idxs, n_global) + to.shockvar³_cols = compressed_triple_indices(to.shockvar³_idxs, n_global) end - if size(to.I_exo2, 1) != constants.post_model_macro.nExo^2 - to.I_exo2 = sparse(ℒ.I(constants.post_model_macro.nExo^2)) + if isempty(to.shock_state_state_idxs) + state_length = nˢ + 1 + to.shock_state_state_idxs, to.shock_state_state_rows = + compressed_shock_state_state_index_map(state_length, nᵉ) + to.shock_shock_state_idxs, to.shock_shock_state_rows = + compressed_shock_shock_state_index_map(state_length, nᵉ) end end diff --git a/src/perturbation/solution.jl b/src/perturbation/solution.jl index dd5504636..372aa9bf6 100644 --- a/src/perturbation/solution.jl +++ b/src/perturbation/solution.jl @@ -2411,6 +2411,1084 @@ function compressed_kron²(a::AbstractMatrix{T}; return out end +"""Fill `out` with the compressed square of `a`. + +The output is ordered like `𝐔₂ * kron(a, a)`, with diagonal entries `a[i]^2` +and off-diagonal entries `2a[i]a[j]`. The dedicated name makes repeated-input +contractions explicit at their call sites and avoids the generic permutation +logic used for two different inputs. +""" +# +# `@simd` was tried on these inner loops and made things worse, so it is +# deliberately absent. Measured on the pair kernel, microseconds per call, best +# of 20 x 2000: n = 34, 0.13 plain against 0.21 with `@simd`; n = 64, 0.30 +# against 0.80. The body carries a data-dependent branch (`i == j`, taken +# exactly once, on the last iteration, so the predictor gets it right every +# time) and `@simd` trades that perfectly predicted branch for a vectorised +# select and masked store. Peeling the diagonal out into its own statement so +# the inner loop is branch-free is a wash for the pair kernel and worth a few +# percent at best for the cube — not enough to justify two more loop bodies. +function compressed_kron²_power!(out::AbstractVector, a::AbstractVector) + n = length(a) + expected_length = n * (n + 1) ÷ 2 + length(out) == expected_length || throw(DimensionMismatch("compressed pair output must have length $expected_length")) + + p = 0 + @inbounds for i in 1:n + ai = a[i] + for j in 1:i + p += 1 + out[p] = i == j ? ai * ai : 2 * ai * a[j] + end + end + return out +end + +function compressed_kron²_power(a::AbstractVector) + T = eltype(a) + out = Vector{T}(undef, length(a) * (length(a) + 1) ÷ 2) + return compressed_kron²_power!(out, a) +end + +compressed_kron²_same!(out::AbstractVector, a::AbstractVector) = compressed_kron²_power!(out, a) + +function compressed_kron²!(out::AbstractVector, a::AbstractVector, b::AbstractVector) + a === b && return compressed_kron²_power!(out, a) + n = length(a) + length(b) == n || throw(DimensionMismatch("compressed pair inputs must have equal length")) + expected_length = n * (n + 1) ÷ 2 + length(out) == expected_length || throw(DimensionMismatch("compressed pair output must have length $expected_length")) + + p = 0 + @inbounds for i in 1:n + for j in 1:i + p += 1 + if i == j + out[p] = a[i] * b[j] + else + out[p] = a[i] * b[j] + a[j] * b[i] + end + end + end + return out +end + +function compressed_kron²(a::AbstractVector, b::AbstractVector) + T = promote_type(eltype(a), eltype(b)) + out = Vector{T}(undef, length(a) * (length(a) + 1) ÷ 2) + return compressed_kron²!(out, a, b) +end + +# One vector against every column of a matrix — the shape the Jacobian of a +# compressed pair term takes when `b` is an identity block. +# +# Argument order does not matter: the underlying pair product is symmetric +# (`out[i,j] = a[i]b[j] + a[j]b[i]` off the diagonal, `a[i]b[i]` on it), so +# `compressed_kron²(x, J)` and `compressed_kron²(J, x)` agree entry for entry — +# `test/test_compressed_kron.jl` pins that, as it does for the fully symmetric +# triple. Only this argument order has a method, so call sites are written +# vector-first. +function compressed_kron²!(out::AbstractMatrix, a::AbstractVector, b::AbstractMatrix) + n = length(a) + size(b, 1) == n || throw(DimensionMismatch("compressed pair inputs must have equal row count")) + expected_rows = n * (n + 1) ÷ 2 + size(out, 1) == expected_rows || throw(DimensionMismatch("compressed pair output has the wrong row count")) + size(out, 2) == size(b, 2) || throw(DimensionMismatch("compressed pair output has the wrong column count")) + @inbounds for column in axes(b, 2) + compressed_kron²!(view(out, :, column), a, view(b, :, column)) + end + return out +end + +function compressed_kron²(a::AbstractVector, b::AbstractMatrix) + T = promote_type(eltype(a), eltype(b)) + n = length(a) + out = Matrix{T}(undef, n * (n + 1) ÷ 2, size(b, 2)) + return compressed_kron²!(out, a, b) +end + +"""Fill `out` with the compressed cube of `a`. + +The output is ordered like `𝐔₃ * kron(kron(a, a), a)`. Repeated-index terms +use their exact multiplicities: `a[i]^3`, `3a[i]^2a[k]`, `3a[i]a[j]^2`, and +`6a[i]a[j]a[k]`. +""" +function compressed_kron³_power!(out::AbstractVector, a::AbstractVector) + n = length(a) + expected_length = n * (n + 1) * (n + 2) ÷ 6 + length(out) == expected_length || throw(DimensionMismatch("compressed triple output must have length $expected_length")) + + p = 0 + @inbounds for i in 1:n + ai = a[i] + for j in 1:i + aj = a[j] + for k in 1:j + p += 1 + if i == j + out[p] = j == k ? ai * ai * ai : 3 * ai * ai * a[k] + elseif j == k + out[p] = 3 * ai * aj * aj + else + out[p] = 6 * ai * aj * a[k] + end + end + end + end + return out +end + +function compressed_kron³_power(a::AbstractVector) + T = eltype(a) + out = Vector{T}(undef, length(a) * (length(a) + 1) * (length(a) + 2) ÷ 6) + return compressed_kron³_power!(out, a) +end + +compressed_kron³_same!(out::AbstractVector, a::AbstractVector) = compressed_kron³_power!(out, a) + +# Column-wise variants: column `j` of `out` is the compressed power (or product) +# of column `j` of the input, rather than one vector against every column the way +# `compressed_kron²!(::AbstractMatrix, ::AbstractVector, ::AbstractMatrix)` works. +# The particle filters carry their swarm as one column per particle and need this +# shape; the ordering is the vector kernels' own, which is what makes the swarm +# contractible against the same 𝐒₂/𝐒₃ the scalar paths use. +function compressed_kron²_power_columns!(out::AbstractMatrix, a::AbstractMatrix) + size(out, 2) == size(a, 2) || throw(DimensionMismatch("compressed pair output has the wrong column count")) + @inbounds for column in axes(a, 2) + compressed_kron²_power!(view(out, :, column), view(a, :, column)) + end + return out +end + +function compressed_kron²_columns!(out::AbstractMatrix, a::AbstractMatrix, b::AbstractMatrix) + size(out, 2) == size(a, 2) || throw(DimensionMismatch("compressed pair output has the wrong column count")) + size(b, 2) == size(a, 2) || throw(DimensionMismatch("compressed pair inputs must have equal column count")) + @inbounds for column in axes(a, 2) + compressed_kron²!(view(out, :, column), view(a, :, column), view(b, :, column)) + end + return out +end + +function compressed_kron³_power_columns!(out::AbstractMatrix, a::AbstractMatrix) + size(out, 2) == size(a, 2) || throw(DimensionMismatch("compressed triple output has the wrong column count")) + @inbounds for column in axes(a, 2) + compressed_kron³_power!(view(out, :, column), view(a, :, column)) + end + return out +end + +"""Fill `out` with the unique triple terms of `kron(kron(a, b), c)`. + +The output uses the same ordering as `𝐔₃ * kron(kron(a, b), c)`: sorted index +triples `(i, j, k)` with `i ≥ j ≥ k`. Repeated indices are handled by adding +each distinct permutation exactly once. +""" +function compressed_kron³!(out::AbstractVector, a::AbstractVector, + b::AbstractVector, c::AbstractVector) + n = length(a) + length(b) == n || throw(DimensionMismatch("compressed triple inputs must have equal length")) + length(c) == n || throw(DimensionMismatch("compressed triple inputs must have equal length")) + expected_length = n * (n + 1) * (n + 2) ÷ 6 + length(out) == expected_length || throw(DimensionMismatch("compressed triple output must have length $expected_length")) + + if a === b && b === c + return compressed_kron³_power!(out, a) + end + + p = 0 + @inbounds for i in 1:n + for j in 1:i + for k in 1:j + p += 1 + if i == j + if j == k + out[p] = a[i] * b[i] * c[i] + else + out[p] = a[i] * b[i] * c[k] + + a[i] * b[k] * c[i] + + a[k] * b[i] * c[i] + end + elseif j == k + out[p] = a[i] * b[j] * c[j] + + a[j] * b[i] * c[j] + + a[j] * b[j] * c[i] + else + out[p] = a[i] * b[j] * c[k] + + a[i] * b[k] * c[j] + + a[j] * b[i] * c[k] + + a[j] * b[k] * c[i] + + a[k] * b[i] * c[j] + + a[k] * b[j] * c[i] + end + end + end + end + return out +end + +function compressed_kron²_vjp!(da::AbstractVector, db::AbstractVector, + cotangent::AbstractVector, + a::AbstractVector, b::AbstractVector, + scale = one(eltype(da))) + n = length(a) + length(b) == n == length(da) == length(db) || + throw(DimensionMismatch("compressed pair VJP inputs have incompatible lengths")) + length(cotangent) == n * (n + 1) ÷ 2 || + throw(DimensionMismatch("compressed pair VJP cotangent has the wrong length")) + fill!(da, zero(eltype(da))) + fill!(db, zero(eltype(db))) + p = 0 + @inbounds for i in 1:n + for j in 1:i + p += 1 + value = cotangent[p] * scale + if i == j + da[i] += value * b[i] + db[i] += value * a[i] + else + da[i] += value * b[j] + da[j] += value * b[i] + db[i] += value * a[j] + db[j] += value * a[i] + end + end + end + return da, db +end + +function compressed_kron²_power_vjp!(da::AbstractVector, cotangent::AbstractVector, + a::AbstractVector, scale = one(eltype(da))) + n = length(a) + length(da) == n || throw(DimensionMismatch("compressed pair VJP output has the wrong length")) + length(cotangent) == n * (n + 1) ÷ 2 || + throw(DimensionMismatch("compressed pair VJP cotangent has the wrong length")) + fill!(da, zero(eltype(da))) + p = 0 + @inbounds for i in 1:n + for j in 1:i + p += 1 + value = scale * cotangent[p] + if i == j + da[i] += 2 * value * a[i] + else + da[i] += 2 * value * a[j] + da[j] += 2 * value * a[i] + end + end + end + return da +end + +compressed_kron²_same_vjp!(da::AbstractVector, cotangent::AbstractVector, + a::AbstractVector, scale = one(eltype(da))) = + compressed_kron²_power_vjp!(da, cotangent, a, scale) + +function compressed_kron³_vjp!(da::AbstractVector, db::AbstractVector, + dc::AbstractVector, cotangent::AbstractVector, + a::AbstractVector, b::AbstractVector, c::AbstractVector, + scale = one(eltype(da))) + n = length(a) + length(b) == n == length(c) == length(da) == length(db) == length(dc) || + throw(DimensionMismatch("compressed triple VJP inputs have incompatible lengths")) + length(cotangent) == n * (n + 1) * (n + 2) ÷ 6 || + throw(DimensionMismatch("compressed triple VJP cotangent has the wrong length")) + fill!(da, zero(eltype(da))) + fill!(db, zero(eltype(db))) + fill!(dc, zero(eltype(dc))) + p = 0 + @inbounds for i in 1:n + for j in 1:i + for k in 1:j + p += 1 + value = cotangent[p] * scale + if i == j + if j == k + da[i] += value * b[i] * c[i] + db[i] += value * a[i] * c[i] + dc[i] += value * a[i] * b[i] + else + da[i] += value * (b[i] * c[k] + b[k] * c[i]) + da[k] += value * b[i] * c[i] + db[i] += value * (a[i] * c[k] + a[k] * c[i]) + db[k] += value * a[i] * c[i] + dc[i] += value * (a[i] * b[k] + a[k] * b[i]) + dc[k] += value * a[i] * b[i] + end + elseif j == k + da[i] += value * b[j] * c[j] + da[j] += value * (b[i] * c[j] + b[j] * c[i]) + db[i] += value * a[j] * c[j] + db[j] += value * (a[i] * c[j] + a[j] * c[i]) + dc[i] += value * a[j] * b[j] + dc[j] += value * (a[i] * b[j] + a[j] * b[i]) + else + da[i] += value * (b[j] * c[k] + b[k] * c[j]) + da[j] += value * (b[i] * c[k] + b[k] * c[i]) + da[k] += value * (b[i] * c[j] + b[j] * c[i]) + db[i] += value * (a[j] * c[k] + a[k] * c[j]) + db[j] += value * (a[i] * c[k] + a[k] * c[i]) + db[k] += value * (a[i] * c[j] + a[j] * c[i]) + dc[i] += value * (a[j] * b[k] + a[k] * b[j]) + dc[j] += value * (a[i] * b[k] + a[k] * b[i]) + dc[k] += value * (a[i] * b[j] + a[j] * b[i]) + end + end + end + end + return da, db, dc +end + +function compressed_kron³_power_vjp!(da::AbstractVector, cotangent::AbstractVector, + a::AbstractVector, scale = one(eltype(da))) + n = length(a) + length(da) == n || throw(DimensionMismatch("compressed triple VJP output has the wrong length")) + length(cotangent) == n * (n + 1) * (n + 2) ÷ 6 || + throw(DimensionMismatch("compressed triple VJP cotangent has the wrong length")) + fill!(da, zero(eltype(da))) + p = 0 + @inbounds for i in 1:n + for j in 1:i + for k in 1:j + p += 1 + value = scale * cotangent[p] + if i == j + if j == k + da[i] += 3 * value * a[i] * a[i] + else + da[i] += 6 * value * a[i] * a[k] + da[k] += 3 * value * a[i] * a[i] + end + elseif j == k + da[i] += 3 * value * a[j] * a[j] + da[j] += 6 * value * a[i] * a[j] + else + da[i] += 6 * value * a[j] * a[k] + da[j] += 6 * value * a[i] * a[k] + da[k] += 6 * value * a[i] * a[j] + end + end + end + end + return da +end + +compressed_kron³_same_vjp!(da::AbstractVector, cotangent::AbstractVector, + a::AbstractVector, scale = one(eltype(da))) = + compressed_kron³_power_vjp!(da, cotangent, a, scale) + +"""Accumulate the VJP of `compressed_kron³(a, a, I)` with respect to `a`.""" +function compressed_kron³_identity_vjp!(da::AbstractVector, + cotangent::AbstractMatrix, + a::AbstractVector, + scale = one(eltype(da))) + n = length(a) + n_triple = n * (n + 1) * (n + 2) ÷ 6 + size(cotangent) == (n_triple, n) || + throw(DimensionMismatch("compressed triple identity VJP has the wrong size")) + basis = zeros(promote_type(eltype(a), eltype(cotangent)), n) + ∂a = similar(da) + ∂b = similar(da) + ∂c = similar(da) + @inbounds for column in 1:n + fill!(basis, zero(eltype(basis))) + basis[column] = one(eltype(basis)) + compressed_kron³_vjp!(∂a, ∂b, ∂c, view(cotangent, :, column), a, a, basis, scale) + da .+= ∂a + da .+= ∂b + end + return da +end + +"""Accumulate the analytical VJP of `compressed_kron²(a, I)` into `da`.""" +function compressed_kron²_identity_vjp!(da::AbstractVector, + cotangent::AbstractMatrix, + a::AbstractVector, + scale = one(eltype(da))) + n = length(a) + n_pair = n * (n + 1) ÷ 2 + size(cotangent) == (n_pair, n) || + throw(DimensionMismatch("compressed pair identity VJP has the wrong size")) + p = 0 + @inbounds for i in 1:n + for j in 1:i + p += 1 + if i == j + da[i] += scale * cotangent[p, j] + else + da[i] += scale * cotangent[p, j] + da[j] += scale * cotangent[p, i] + end + end + end + return da +end + +function compressed_kron³(a::AbstractVector, b::AbstractVector, c::AbstractVector) + T = promote_type(eltype(a), eltype(b), eltype(c)) + n = length(a) + out = Vector{T}(undef, n * (n + 1) * (n + 2) ÷ 6) + return compressed_kron³!(out, a, b, c) +end + +function compressed_kron³!(out::AbstractMatrix, a::AbstractVector, + b::AbstractVector, c::AbstractMatrix) + n = length(a) + length(b) == n || throw(DimensionMismatch("compressed triple inputs must have equal length")) + size(c, 1) == n || throw(DimensionMismatch("compressed triple inputs must have equal row count")) + expected_rows = n * (n + 1) * (n + 2) ÷ 6 + size(out, 1) == expected_rows || throw(DimensionMismatch("compressed triple output has the wrong row count")) + size(out, 2) == size(c, 2) || throw(DimensionMismatch("compressed triple output has the wrong column count")) + @inbounds for column in axes(c, 2) + compressed_kron³!(view(out, :, column), a, b, view(c, :, column)) + end + return out +end + +function compressed_kron³(a::AbstractVector, b::AbstractVector, c::AbstractMatrix) + T = promote_type(eltype(a), eltype(b), eltype(c)) + n = length(a) + out = Matrix{T}(undef, n * (n + 1) * (n + 2) ÷ 6, size(c, 2)) + return compressed_kron³!(out, a, b, c) +end + +"""Fill a symmetric pair Hessian from compressed quadratic coefficients.""" +function compressed_pair_hessian!(out::AbstractMatrix, coefficients::AbstractVector) + n = round(Int, (sqrt(8 * length(coefficients) + 1) - 1) / 2) + n * (n + 1) ÷ 2 == length(coefficients) || + throw(DimensionMismatch("compressed pair coefficients have an invalid length")) + size(out) == (n, n) || throw(DimensionMismatch("pair Hessian has the wrong size")) + fill!(out, zero(eltype(out))) + p = 0 + @inbounds for i in 1:n + for j in 1:i + p += 1 + out[i, j] = coefficients[p] + out[j, i] = coefficients[p] + end + end + return out +end + +"""Add the symmetric Hessian of a compressed cubic polynomial to `out`.""" +function compressed_triple_hessian!(out::AbstractMatrix, + coefficients::AbstractVector, + x::AbstractVector) + n = length(x) + expected_length = n * (n + 1) * (n + 2) ÷ 6 + length(coefficients) == expected_length || + throw(DimensionMismatch("compressed triple coefficients have the wrong length")) + size(out) == (n, n) || throw(DimensionMismatch("triple Hessian has the wrong size")) + + p = 0 + @inbounds for i in 1:n + for j in 1:i + for k in 1:j + p += 1 + coefficient = coefficients[p] + if i == j + if j == k + out[i, i] += coefficient * x[i] + else + out[i, i] += coefficient * x[k] + out[i, k] += coefficient * x[i] + out[k, i] += coefficient * x[i] + end + elseif j == k + out[j, j] += coefficient * x[i] + out[i, j] += coefficient * x[j] + out[j, i] += coefficient * x[j] + else + out[i, j] += coefficient * x[k] + out[j, i] += coefficient * x[k] + out[i, k] += coefficient * x[j] + out[k, i] += coefficient * x[j] + out[j, k] += coefficient * x[i] + out[k, j] += coefficient * x[i] + end + end + end + end + return out +end + +function compressed_shock_state_state_rows(selected_indices, + shock_offset::Int, + n_state::Int, + n_exo::Int) + n_state_pair = n_state * (n_state + 1) ÷ 2 + rows = Vector{Int}(undef, n_exo * n_state_pair) + p = 0 + @inbounds for q in 1:n_exo + for i in 1:n_state + for j in 1:i + p += 1 + triple_index = compressed_triple_index(shock_offset + q, i, j) + row = searchsortedfirst(selected_indices, triple_index) + row <= length(selected_indices) && selected_indices[row] == triple_index || + throw(ArgumentError("selected cubic indices do not contain a shock-state-state term")) + rows[p] = row + end + end + end + return rows +end + +function compressed_shock_shock_state_rows(selected_indices, + shock_offset::Int, + n_state::Int, + n_exo::Int) + n_shock_pair = n_exo * (n_exo + 1) ÷ 2 + rows = Vector{Int}(undef, n_shock_pair * n_state) + p = 0 + @inbounds for i in 1:n_exo + for j in 1:i + for state_index in 1:n_state + p += 1 + triple_index = compressed_triple_index(shock_offset + i, + shock_offset + j, + state_index) + row = searchsortedfirst(selected_indices, triple_index) + row <= length(selected_indices) && selected_indices[row] == triple_index || + throw(ArgumentError("selected cubic indices do not contain a shock-shock-state term")) + rows[p] = row + end + end + end + return rows +end + +# The cubic index sets that mix shocks with states, and the row maps into them. +# +# Both depend only on the model's dimensions, so they live with the model's other +# index constants: `third_order_indices` holds them as `shock_state_state_idxs` / +# `shock_state_state_rows` and `shock_shock_state_idxs` / `shock_shock_state_rows`, +# filled once by `ensure_conditional_forecast_constants!`. Everything downstream — +# the inversion filter loops, the joint-warmup solvers and their pullbacks — takes +# them from there rather than rebuilding a comprehension over O(n_exo·n_state²) +# triples per call. +# +# These two builders are the single construction shared by that cache and by the +# handful of pullback entry points that can be called without the model's +# constants in scope, so the two can never disagree. The shock offset is always +# the state length (states, then the volatility slot, then the shocks), so it is +# not a separate argument. Sorting the loop-ordered indices and inverting the +# permutation yields both the sorted set and the loop-position-to-row map in one +# pass, which is what the callers index with. +function compressed_shock_state_state_index_map(n_state::Int, n_exo::Int) + shock_offset = n_state + loop_order = [compressed_triple_index(shock_offset + q, i, j) + for q in 1:n_exo for i in 1:n_state for j in 1:i] + permutation = sortperm(loop_order) + return loop_order[permutation], invperm(permutation) +end + +function compressed_shock_shock_state_index_map(n_state::Int, n_exo::Int) + shock_offset = n_state + loop_order = [compressed_triple_index(shock_offset + i, shock_offset + j, k) + for i in 1:n_exo for j in 1:i for k in 1:n_state] + permutation = sortperm(loop_order) + return loop_order[permutation], invperm(permutation) +end + +"""Fill a compressed matrix for fixing one state coordinate in a cubic term.""" +function compressed_triple_state_to_pair!(output::AbstractMatrix, + state::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + n_pair = n_exo * (n_exo + 1) ÷ 2 + size(output) == (length(selected_indices), n_pair) || + throw(DimensionMismatch("compressed state-to-pair output has the wrong size")) + fill!(output, zero(eltype(output))) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), n_exo) : index_rows + pair_index = 0 + row_index = 0 + @inbounds for i in 1:n_exo + for j in 1:i + pair_index += 1 + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + output[row, pair_index] = state[state_index] / 2 + end + end + end + return output +end + +function compressed_triple_state_to_pair(state::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + n_pair = n_exo * (n_exo + 1) ÷ 2 + output = zeros(promote_type(eltype(state), Float64), length(selected_indices), n_pair) + return compressed_triple_state_to_pair!(output, state, n_global, shock_offset, n_exo, + selected_indices; index_rows = index_rows) +end + +"""Accumulate the VJP of `compressed_triple_state_to_pair` into a state cotangent.""" +function compressed_triple_state_to_pair_vjp!(dstate::AbstractVector, + cotangent::AbstractMatrix, + state::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + size(cotangent, 1) == length(selected_indices) || + throw(DimensionMismatch("compressed triple state-to-pair cotangent has the wrong row count")) + size(cotangent, 2) == n_exo * (n_exo + 1) ÷ 2 || + throw(DimensionMismatch("compressed triple state-to-pair cotangent has the wrong column count")) + pair_index = 0 + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), n_exo) : index_rows + row_index = 0 + @inbounds for i in 1:n_exo + for j in 1:i + pair_index += 1 + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + dstate[state_index] += cotangent[row, pair_index] / 2 + end + end + end + return dstate +end + +"""Build compressed shock-state-state cubic entries for selected global indices.""" +function compressed_triple_shock_state_state(shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(shock), eltype(state), Float64), length(selected_indices)) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for shock_index in eachindex(shock) + for i in eachindex(state) + for j in 1:i + row_index += 1 + row = rows[row_index] + output[row] = shock[shock_index] * (i == j ? state[i] * state[i] : 2 * state[i] * state[j]) + end + end + end + return output +end + +"""Accumulate the VJP of compressed shock-state-state entries.""" +function compressed_triple_shock_state_state_vjp!(dshock::AbstractVector, + dstate::AbstractVector, + cotangent::AbstractVector, + shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices, + scale = 1; + index_rows = nothing) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for q in eachindex(shock) + for i in eachindex(state) + for j in 1:i + row_index += 1 + row = rows[row_index] + value = cotangent[row] * scale + dshock[q] += value * (i == j ? state[i] * state[i] : 2 * state[i] * state[j]) + if i == j + dstate[i] += value * 2 * shock[q] * state[i] + else + dstate[i] += value * 2 * shock[q] * state[j] + dstate[j] += value * 2 * shock[q] * state[i] + end + end + end + end + return nothing +end + +"""Build compressed shock-shock-state cubic entries for selected global indices.""" +function compressed_triple_shock_shock_state(shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(shock), eltype(state), Float64), length(selected_indices)) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + pair_value = i == j ? shock[i] * shock[i] : 2 * shock[i] * shock[j] + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + output[row] = pair_value * state[state_index] + end + end + end + return output +end + +"""Accumulate the VJP of compressed shock-shock-state entries.""" +function compressed_triple_shock_shock_state_vjp!(dshock::AbstractVector, + dstate::AbstractVector, + cotangent::AbstractVector, + shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices, + scale = 1; + index_rows = nothing) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + pair_value = i == j ? shock[i] * shock[i] : 2 * shock[i] * shock[j] + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + value = cotangent[row] * scale + dstate[state_index] += value * pair_value + if i == j + dshock[i] += value * 2 * shock[i] * state[state_index] + else + dshock[i] += value * 2 * shock[j] * state[state_index] + dshock[j] += value * 2 * shock[i] * state[state_index] + end + end + end + end + return nothing +end + +"""Build the state Jacobian of compressed shock-shock-state entries.""" +function compressed_triple_shock_shock_state_to_state(shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(shock), eltype(state), Float64), + length(selected_indices), length(state)) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + pair_value = i == j ? shock[i] * shock[i] : 2 * shock[i] * shock[j] + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + output[row, state_index] = pair_value + end + end + end + return output +end + +"""Accumulate the VJP of the state Jacobian of shock-shock-state entries.""" +function compressed_triple_shock_shock_state_to_state_vjp!(dshock::AbstractVector, + cotangent::AbstractMatrix, + shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices, + scale = 1; + index_rows = nothing) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + value = scale * cotangent[row, state_index] + if i == j + dshock[i] += value * 2 * shock[i] + else + dshock[i] += value * 2 * shock[j] + dshock[j] += value * 2 * shock[i] + end + end + end + end + return nothing +end + +"""Fill a compressed matrix for fixing two state coordinates in a cubic term.""" +function compressed_triple_state_pair_to_shock!(output::AbstractMatrix, + state_pair::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + n_state = round(Int, (sqrt(8 * length(state_pair) + 1) - 1) / 2) + return compressed_triple_state_pair_to_shock!(output, state_pair, n_global, shock_offset, + n_exo, selected_indices, n_state; + index_rows = index_rows) +end + +function compressed_triple_state_pair_to_shock!(output::AbstractMatrix, + state_pair::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices, + n_state::Int; + index_rows = nothing) + n_state * (n_state + 1) ÷ 2 == length(state_pair) || + throw(DimensionMismatch("compressed state-pair vector has an invalid length")) + size(output) == (length(selected_indices), n_exo) || + throw(DimensionMismatch("compressed state-pair-to-shock output has the wrong size")) + fill!(output, zero(eltype(output))) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, n_state, n_exo) : index_rows + @inbounds for shock_index in 1:n_exo + row_index = (shock_index - 1) * (n_state * (n_state + 1) ÷ 2) + pair_index = 0 + for i in 1:n_state + for j in 1:i + row_index += 1 + pair_index += 1 + row = rows[row_index] + output[row, shock_index] = state_pair[pair_index] / 2 + end + end + end + return output +end + +function compressed_triple_state_pair_to_shock(state_pair::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(state_pair), Float64), length(selected_indices), n_exo) + n_state = round(Int, (sqrt(8 * length(state_pair) + 1) - 1) / 2) + return compressed_triple_state_pair_to_shock!(output, state_pair, n_global, shock_offset, + n_exo, selected_indices, n_state; + index_rows = index_rows) +end + +"""Accumulate the VJP of `compressed_triple_state_pair_to_shock` into a state cotangent.""" +function compressed_triple_state_pair_to_shock_vjp!(dstate::AbstractVector, + cotangent::AbstractMatrix, + state::AbstractVector, + n_global::Int, + shock_offset::Int, + n_exo::Int, + selected_indices; + index_rows = nothing) + n_state_pair = length(state) * (length(state) + 1) ÷ 2 + size(cotangent) == (length(selected_indices), n_exo) || + throw(DimensionMismatch("compressed triple state-pair-to-shock cotangent has the wrong size")) + dstate_pair = zeros(promote_type(eltype(state), eltype(cotangent)), n_state_pair) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, length(state), n_exo) : index_rows + @inbounds for shock_index in 1:n_exo + row_index = (shock_index - 1) * n_state_pair + pair_index = 0 + for i in eachindex(state) + for j in 1:i + row_index += 1 + pair_index += 1 + row = rows[row_index] + dstate_pair[pair_index] += cotangent[row, shock_index] / 2 + end + end + end + # `compressed_kron²_power_vjp!` fills its output. This helper has + # accumulating semantics, so add the state-pair contribution explicitly + # instead of overwriting cotangents already supplied by other terms. + pair_index = 0 + @inbounds for i in eachindex(state) + for j in 1:i + pair_index += 1 + value = dstate_pair[pair_index] + if i == j + dstate[i] += 2 * value * state[i] + else + dstate[i] += 2 * value * state[j] + dstate[j] += 2 * value * state[i] + end + end + end + return dstate +end + +"""Build cubic derivatives for one fixed shock and two state coordinates.""" +function compressed_triple_shock_state_to_state(shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(shock), eltype(state), Float64), + length(selected_indices), length(state)) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for shock_index in eachindex(shock) + for i in eachindex(state) + for j in 1:i + row_index += 1 + row = rows[row_index] + if i == j + output[row, i] = shock[shock_index] * state[i] + else + output[row, i] = shock[shock_index] * state[j] + output[row, j] = shock[shock_index] * state[i] + end + end + end + end + return output +end + +"""Accumulate the VJP of compressed shock-state-state derivatives.""" +function compressed_triple_shock_state_to_state_vjp!(dshock::AbstractVector, + dstate::AbstractVector, + cotangent::AbstractMatrix, + shock::AbstractVector, + state::AbstractVector, + shock_offset::Int, + selected_indices, + scale = 1; + index_rows = nothing) + rows = isnothing(index_rows) ? compressed_shock_state_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for q in eachindex(shock) + for i in eachindex(state) + for j in 1:i + row_index += 1 + row = rows[row_index] + if i == j + dshock[q] += scale * cotangent[row, i] * state[i] + dstate[i] += scale * cotangent[row, i] * shock[q] + else + dshock[q] += scale * (cotangent[row, i] * state[j] + cotangent[row, j] * state[i]) + dstate[i] += scale * cotangent[row, j] * shock[q] + dstate[j] += scale * cotangent[row, i] * shock[q] + end + end + end + end + return nothing +end + +"""Build cubic derivatives for one fixed state and two shock coordinates.""" +function compressed_triple_state_shock_to_shock(state::AbstractVector, + shock::AbstractVector, + shock_offset::Int, + selected_indices; + index_rows = nothing) + output = zeros(promote_type(eltype(shock), eltype(state), Float64), + length(selected_indices), length(shock)) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + if i == j + output[row, i] = state[state_index] * shock[i] + else + output[row, i] = state[state_index] * shock[j] + output[row, j] = state[state_index] * shock[i] + end + end + end + end + return output +end + +"""Accumulate the VJP of compressed state-shock-shock derivatives.""" +function compressed_triple_state_shock_to_shock_vjp!(dstate::AbstractVector, + dshock::AbstractVector, + cotangent::AbstractMatrix, + state::AbstractVector, + shock::AbstractVector, + shock_offset::Int, + selected_indices, + scale = 1; + index_rows = nothing) + rows = isnothing(index_rows) ? compressed_shock_shock_state_rows( + selected_indices, shock_offset, length(state), length(shock)) : index_rows + row_index = 0 + @inbounds for i in eachindex(shock) + for j in 1:i + for state_index in eachindex(state) + row_index += 1 + row = rows[row_index] + value = cotangent[row, :] + if i == j + dstate[state_index] += scale * value[i] * shock[i] + dshock[i] += scale * value[i] * state[state_index] + else + dstate[state_index] += scale * (value[i] * shock[j] + value[j] * shock[i]) + dshock[i] += scale * value[j] * state[state_index] + dshock[j] += scale * value[i] * state[state_index] + end + end + end + end + return nothing +end + +@inline function compressed_pair_index(i::Int, j::Int) + hi = max(i, j) + lo = min(i, j) + return (hi - 1) * hi ÷ 2 + lo +end + +@inline function compressed_triple_index(i::Int, j::Int, k::Int) + hi = max(i, j, k) + lo = min(i, j, k) + mid = i + j + k - hi - lo + return (hi - 1) * hi * (hi + 1) ÷ 6 + (mid - 1) * mid ÷ 2 + lo +end + +function compressed_pair_indices(full_indices, n::Int, offset::Int = 0) + indices = Vector{Int}(undef, length(full_indices)) + @inbounds for p in eachindex(full_indices) + full_index = full_indices[p] - 1 + i, j = full_index ÷ n + 1, full_index % n + 1 + indices[p] = compressed_pair_index(i - offset, j - offset) + end + sort!(indices) + unique!(indices) + return indices +end + +function compressed_triple_indices(full_indices, n::Int, offset::Int = 0) + indices = Vector{Int}(undef, length(full_indices)) + @inbounds for p in eachindex(full_indices) + full_index = full_indices[p] - 1 + k = full_index % n + 1 + full_index ÷= n + j = full_index % n + 1 + i = full_index ÷ n + 1 + indices[p] = compressed_triple_index(i - offset, j - offset, k - offset) + end + sort!(indices) + unique!(indices) + return indices +end + # Detect unit roots from QME solution without computing eigenvalues. # If sol has an eigenvalue near 1, (I - sol) is nearly singular. # Uses LU factorization: exactly singular (info > 0) or smallest absolute pivot < tol. diff --git a/src/rrules.jl b/src/rrules.jl index 7366edfa3..2fee008c6 100644 --- a/src/rrules.jl +++ b/src/rrules.jl @@ -145,19 +145,17 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), # Get cached computational constants constants = initialise_constants!(𝓂) - so = constants.second_order + so = ensure_computational_constants!(constants) T = constants.post_model_macro s_in_s⁺ = so.s_in_s⁺ s_in_s = so.s_in_s I_nPast = T.I_nPast - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - A = 𝐒₁[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed] - B = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] + n_state_aug = T.nPast_not_future_and_mixed + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + B = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx, 1:n_state_pair] + B̂ = B # end # timeit_debug @@ -170,15 +168,16 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), ℂ = 𝓂.workspaces.second_order nPast = length(x) + state_identity = @view so.I_state_vol[:, 1:nPast] ensure_sss_kron_buffers!(ℂ, nPast; third_order=false) kron_x_aug_buf = ℂ.kron_x_aug_xx kron_x_aug_I = ℂ.kron_x_aug_I for i in 1:max_iters copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug_buf, x_aug, x_aug) + compressed_kron²_power!(kron_x_aug_buf, x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I - I_nPast) Δx = (A * x + B̂ * kron_x_aug_buf / 2 - x) @@ -199,7 +198,7 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), end copyto!(x_aug, 1, x, 1, nPast) # Local kron for closure capture (workspace buffers may be overwritten before pullback runs) - kron_x_aug = ℒ.kron(x_aug, x_aug) + kron_x_aug = compressed_kron²_power(x_aug) solved = isapprox(A * x + B̂ * kron_x_aug / 2, x, rtol = tol) ∂𝐒₁ = zero(𝐒₁) @@ -211,11 +210,12 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), function second_order_stochastic_steady_state_pullback(∂x) # @timeit_debug timer "Calculate SSS - pullback" begin ∂x₁ = unthunk(∂x[1]) - S = -∂x₁' / (A + B * ℒ.kron(x_aug, I_nPast) - I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) + S = -∂x₁' / (A + B * kron_x_aug_I - I_nPast) ∂𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] = S' * x' - ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] = S' * kron_x_aug' / 2 + ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:n_state_pair] = S' * kron_x_aug' / 2 # end # timeit_debug return NoTangent(), NoTangent(), ∂𝐒₁, ∂𝐒₂, NoTangent(), NoTangent(), NoTangent() @@ -228,8 +228,8 @@ end function rrule(::typeof(solve_stochastic_steady_state_newton), ::Val{:third_order}, 𝐒₁::Matrix{Float64}, - 𝐒₂::AbstractSparseMatrix{Float64}, - 𝐒₃::AbstractSparseMatrix{Float64}, + 𝐒₂::AbstractMatrix{Float64}, + 𝐒₃::AbstractMatrix{Float64}, x::Vector{Float64}, 𝓂::ℳ; tol::AbstractFloat = 1e-14) @@ -240,19 +240,14 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), s_in_s = so.s_in_s I_nPast = T.I_nPast - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - - kron_s⁺_s⁺_s⁺ = so.kron_s⁺_s⁺_s⁺ - - kron_s_s⁺_s⁺ = so.kron_s_s⁺_s⁺ - A = 𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - B = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] - C = 𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s_s⁺_s⁺] - Ĉ = 𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺_s⁺] + n_state_aug = T.nPast_not_future_and_mixed + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + n_state_triple = n_state_aug * (n_state_aug + 1) * (n_state_aug + 2) ÷ 6 + B = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx, 1:n_state_pair] + B̂ = B + C = 𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx, 1:n_state_triple] + Ĉ = C max_iters = 100 # SSS .= 𝐒₁ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + 𝐒₃ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 @@ -261,6 +256,7 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), ℂ = 𝓂.workspaces.third_order nPast = length(x) + state_identity = @view so.I_state_vol[:, 1:nPast] ensure_sss_kron_buffers!(ℂ, nPast; third_order=true) kron_x_aug_buf = ℂ.kron_x_aug_xx kron_x_kron_buf = ℂ.kron_x_aug_x_kron @@ -269,11 +265,11 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), for i in 1:max_iters copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug_buf, x_aug, x_aug) - ℒ.kron!(kron_x_kron_buf, x_aug, kron_x_aug_buf) + compressed_kron²_power!(kron_x_aug_buf, x_aug) + compressed_kron³_power!(kron_x_kron_buf, x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) - ℒ.kron!(kron_x_kron_I, kron_x_aug_buf, I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) + compressed_kron³!(kron_x_kron_I, x_aug, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I + C * kron_x_kron_I / 2 - I_nPast) Δx = (A * x + B̂ * kron_x_aug_buf / 2 + Ĉ * kron_x_kron_buf / 6 - x) @@ -295,8 +291,8 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), copyto!(x_aug, 1, x, 1, nPast) # Local kron for closure capture (workspace buffers may be overwritten before pullback runs) - kron_x_aug = ℒ.kron(x_aug, x_aug) - kron_x_kron = ℒ.kron(x_aug, kron_x_aug) + kron_x_aug = compressed_kron²_power(x_aug) + kron_x_kron = compressed_kron³_power(x_aug) solved = isapprox(A * x + B̂ * kron_x_aug / 2 + Ĉ * kron_x_kron / 6, x, rtol = tol) ∂𝐒₁ = zero(𝐒₁) @@ -305,13 +301,15 @@ function rrule(::typeof(solve_stochastic_steady_state_newton), function third_order_stochastic_steady_state_pullback(∂x) ∂x₁ = unthunk(∂x[1]) - S = -∂x₁' / (A + B * ℒ.kron(x_aug, I_nPast) + C * ℒ.kron(kron_x_aug, I_nPast) / 2 - I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) + compressed_kron³!(kron_x_kron_I, x_aug, x_aug, state_identity) + S = -∂x₁' / (A + B * kron_x_aug_I + C * kron_x_kron_I / 2 - I_nPast) ∂𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] = S' * x' - ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] = S' * kron_x_aug' / 2 + ∂𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:n_state_pair] = S' * kron_x_aug' / 2 - ∂𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺_s⁺] = S' * kron_x_kron' / 6 + ∂𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:n_state_triple] = S' * kron_x_kron' / 6 return NoTangent(), NoTangent(), ∂𝐒₁, ∂𝐒₂, ∂𝐒₃, NoTangent(), NoTangent(), NoTangent() end @@ -812,12 +810,11 @@ function rrule(::typeof(prepare_stochastic_steady_state_base_terms), return common, pullback end - 𝐔₂ = 𝓂.constants.second_order.𝐔₂ - 𝐒₂ = (sparse(𝐒₂_raw) * 𝐔₂)::SparseMatrixCSC{Float64, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝐒₂_raw)::SparseMatrixCSC{Float64, Int} 𝐒₁ = [𝐒₁_raw[:, 1:nPast] zeros(nVars) 𝐒₁_raw[:, nPast+1:end]] aug_state₁ = sparse([zeros(nPast); 1; zeros(nExo)]) - kron_aug1 = ℒ.kron(aug_state₁, aug_state₁) + kron_aug1 = compressed_kron²_power(aug_state₁) tmp = collect(T.I_nPast - 𝐒₁[past_idx, 1:nPast]) rhs = collect((𝐒₂ * kron_aug1 / 2)[past_idx]) @@ -902,7 +899,7 @@ function rrule(::typeof(prepare_stochastic_steady_state_base_terms), ∂𝐒₁_aug[past_idx, 1:nPast] .-= ∂tmp ∂𝐒₂_from_rhs = spzeros(Float64, size(𝐒₂)...) ∂𝐒₂_from_rhs[past_idx, :] += ∂rhs_buffer * kron_aug1' / 2 - ∂𝐒₂_raw_total += ∂𝐒₂_from_rhs * 𝐔₂' + ∂𝐒₂_raw_total += ∂𝐒₂_from_rhs end X = ms.steady_state_expand_matrix @@ -973,15 +970,13 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - # Expand compressed 𝐒₂_raw to full for stochastic SS computation - 𝐔₂ = 𝓂.constants.second_order.𝐔₂ - 𝐒₂ = (sparse(𝐒₂_raw) * 𝐔₂)::SparseMatrixCSC{Float64, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝐒₂_raw)::SparseMatrixCSC{Float64, Int} so = 𝓂.constants.second_order nPast = 𝓂.constants.post_model_macro.nPast_not_future_and_mixed - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ A = 𝐒₁[:,1:nPast] - B̂ = 𝐒₂[:,kron_s⁺_s⁺] + n_state_pair = (nPast + 1) * (nPast + 2) ÷ 2 + B̂ = 𝐒₂[:,1:n_state_pair] newton_result, newton_pullback = rrule(solve_stochastic_steady_state_newton, Val(:second_order), 𝐒₁, 𝐒₂, collect(SSSstates), 𝓂) @@ -1006,7 +1001,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - state = A * SSSstates_final + B̂ * ℒ.kron(vcat(SSSstates_final,1), vcat(SSSstates_final,1)) / 2 + state = A * SSSstates_final + B̂ * compressed_kron²(vcat(SSSstates_final,1), vcat(SSSstates_final,1)) / 2 sss = all_SS + vec(state) result = (sss, converged, SS_and_pars, solution_error, ∇₁, ∇₂, 𝐒₁, 𝐒₂) @@ -1035,27 +1030,26 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂state_vec = Δsss aug_sss = vcat(SSSstates_final, 1) - kron_aug = ℒ.kron(aug_sss, aug_sss) + kron_aug = compressed_kron²_power(aug_sss) ∂𝐒₁_from_state = zeros(Float64, size(𝐒₁)) ∂𝐒₁_from_state[:, 1:nPast] += ∂state_vec * SSSstates_final' ∂𝐒₂_from_state = spzeros(Float64, size(𝐒₂)...) - ∂𝐒₂_from_state[:, kron_s⁺_s⁺] += ∂state_vec * kron_aug' / 2 + ∂𝐒₂_from_state[:, 1:n_state_pair] += ∂state_vec * kron_aug' / 2 + # d/dx of compressed_kron²_power(aug(x)) is 2 * compressed_kron²(aug, ∂aug/∂x), + # so the 1/2 in front of the 𝐒₂ term cancels. Same convention as the Newton + # Jacobian in solve_stochastic_steady_state_newton. ∂SSSstates_from_state = A' * ∂state_vec - n_aug = length(aug_sss) - I_aug = Matrix{Float64}(ℒ.I, n_aug, n_aug) - pad = vcat(Matrix{Float64}(ℒ.I, nPast, nPast), zeros(1, nPast)) - dkron_dx = ℒ.kron(I_aug, aug_sss) * pad + ℒ.kron(aug_sss, I_aug) * pad - ∂SSSstates_from_state += (B̂' * ∂state_vec)' * dkron_dx / 2 |> vec + dkron_dx = compressed_kron²(aug_sss, vcat(Matrix{Float64}(ℒ.I, nPast, nPast), zeros(1, nPast))) + ∂SSSstates_from_state += (B̂' * ∂state_vec)' * dkron_dx |> vec newton_tangents = newton_pullback((∂SSSstates_from_state, NoTangent())) ∂𝐒₁_newton = newton_tangents[3] ∂𝐒₂_newton = newton_tangents[4] - # Convert full-space ∂𝐒₂ to compressed for common_pullback - ∂𝐒₂_raw_total = (∂𝐒₂_from_state + ∂𝐒₂_newton + Δ𝐒₂) * 𝐔₂' + ∂𝐒₂_raw_total = ∂𝐒₂_from_state + ∂𝐒₂_newton + Δ𝐒₂ common_tangents = common_pullback((NoTangent(), Δsss, @@ -1106,14 +1100,12 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - # Expand compressed 𝐒₂_raw to full for stochastic SS computation - 𝐔₂ = 𝓂.constants.second_order.𝐔₂ - 𝐒₂ = (sparse(𝐒₂_raw) * 𝐔₂)::SparseMatrixCSC{Float64, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝐒₂_raw)::SparseMatrixCSC{Float64, Int} T = 𝓂.constants.post_model_macro nPast = T.nPast_not_future_and_mixed aug_state₁ = sparse([zeros(nPast); 1; zeros(T.nExo)]) - kron_aug1 = ℒ.kron(aug_state₁, aug_state₁) + kron_aug1 = compressed_kron²_power(aug_state₁) state = 𝐒₁[:,1:nPast] * SSSstates + 𝐒₂ * kron_aug1 / 2 sss = all_SS + vec(state) @@ -1149,8 +1141,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂𝐒₂_from_state += ∂state_vec * kron_aug1' / 2 ∂SSSstates = 𝐒₁[:,1:nPast]' * ∂state_vec - # Convert full-space ∂𝐒₂ to compressed for common_pullback - ∂𝐒₂_raw_total = (∂𝐒₂_from_state + Δ𝐒₂) * 𝐔₂' + ∂𝐒₂_raw_total = ∂𝐒₂_from_state + Δ𝐒₂ common_tangents = common_pullback((NoTangent(), Δsss, @@ -1201,8 +1192,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - 𝐔₂ = 𝓂.constants.second_order.𝐔₂ - 𝐒₂ = (sparse(𝐒₂_raw) * 𝐔₂)::SparseMatrixCSC{Float64, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝐒₂_raw)::SparseMatrixCSC{Float64, Int} ∇₃, third_derivatives_pullback = rrule(calculate_third_order_derivatives, parameters, SS_and_pars, 𝓂.caches, 𝓂.functions.third_order_derivatives, 𝓂.workspaces) @@ -1237,17 +1227,15 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - 𝐔₃ = 𝓂.constants.third_order.𝐔₃ - 𝐒₃̂ = sparse(𝐒₃) * 𝐔₃ # was: dense_to_sparse + 𝐒₃̂ = sparse(𝐒₃)::SparseMatrixCSC{Float64, Int} so = 𝓂.constants.second_order nPast = 𝓂.constants.post_model_macro.nPast_not_future_and_mixed - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - kron_s⁺_s⁺_s⁺ = so.kron_s⁺_s⁺_s⁺ - A = 𝐒₁[:,1:nPast] - B̂ = 𝐒₂[:,kron_s⁺_s⁺] - Ĉ = 𝐒₃̂[:,kron_s⁺_s⁺_s⁺] + n_state_pair = (nPast + 1) * (nPast + 2) ÷ 2 + n_state_triple = (nPast + 1) * (nPast + 2) * (nPast + 3) ÷ 6 + B̂ = 𝐒₂[:,1:n_state_pair] + Ĉ = 𝐒₃̂[:,1:n_state_triple] newton_result, newton_pullback = rrule(solve_stochastic_steady_state_newton, Val(:third_order), 𝐒₁, 𝐒₂, 𝐒₃̂, collect(SSSstates), 𝓂) @@ -1273,8 +1261,8 @@ function rrule(::typeof(calculate_stochastic_steady_state), end aug_sss = vcat(SSSstates_final, 1) - kron_aug = ℒ.kron(aug_sss, aug_sss) - kron_aug3 = ℒ.kron(aug_sss, kron_aug) + kron_aug = compressed_kron²_power(aug_sss) + kron_aug3 = compressed_kron³_power(aug_sss) state = A * SSSstates_final + B̂ * kron_aug / 2 + Ĉ * kron_aug3 / 6 sss = all_SS + vec(state) @@ -1315,22 +1303,23 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂𝐒₁_from_state[:, 1:nPast] += ∂state_vec * SSSstates_final' ∂𝐒₂_from_state = spzeros(Float64, size(𝐒₂)...) - ∂𝐒₂_from_state[:, kron_s⁺_s⁺] += ∂state_vec * kron_aug' / 2 + ∂𝐒₂_from_state[:, 1:n_state_pair] += ∂state_vec * kron_aug' / 2 ∂𝐒₃̂_from_state = spzeros(Float64, size(𝐒₃̂)...) - ∂𝐒₃̂_from_state[:, kron_s⁺_s⁺_s⁺] += ∂state_vec * kron_aug3' / 6 + ∂𝐒₃̂_from_state[:, 1:n_state_triple] += ∂state_vec * kron_aug3' / 6 ∂SSSstates_from_state = A' * ∂state_vec - n_aug = length(aug_sss) - I_aug = Matrix{Float64}(ℒ.I, n_aug, n_aug) pad = vcat(Matrix{Float64}(ℒ.I, nPast, nPast), zeros(1, nPast)) - dkron_dx = ℒ.kron(I_aug, aug_sss) * pad + ℒ.kron(aug_sss, I_aug) * pad - ∂SSSstates_from_state += (B̂' * ∂state_vec)' * dkron_dx / 2 |> vec + # Differentiating the compressed powers brings down their multiplicities: + # d/dx compressed_kron²_power(aug) = 2 * compressed_kron²(aug, ∂aug/∂x) and + # d/dx compressed_kron³_power(aug) = 3 * compressed_kron³(aug, aug, ∂aug/∂x), + # so the 1/2 and 1/6 weights become 1 and 1/2. Same convention as the Newton + # Jacobian in solve_stochastic_steady_state_newton. + dkron_dx = compressed_kron²(aug_sss, pad) + ∂SSSstates_from_state += (B̂' * ∂state_vec)' * dkron_dx |> vec - dkron3_dx = ℒ.kron(pad, ℒ.kron(aug_sss, aug_sss)) + - ℒ.kron(aug_sss, ℒ.kron(pad, aug_sss)) + - ℒ.kron(aug_sss, ℒ.kron(aug_sss, pad)) - ∂SSSstates_from_state += (Ĉ' * ∂state_vec)' * dkron3_dx / 6 |> vec + dkron3_dx = compressed_kron³(aug_sss, aug_sss, pad) + ∂SSSstates_from_state += (Ĉ' * ∂state_vec)' * dkron3_dx / 2 |> vec newton_tangents = newton_pullback((∂SSSstates_from_state, NoTangent())) ∂𝐒₁_newton = newton_tangents[3] @@ -1338,7 +1327,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂𝐒₃̂_newton = newton_tangents[5] ∂𝐒₃̂_total = ∂𝐒₃̂_from_state + ∂𝐒₃̂_newton + Δ𝐒₃̂ - ∂𝐒₃_raw = Matrix(∂𝐒₃̂_total) * 𝐔₃' + ∂𝐒₃_raw = ∂𝐒₃̂_total so3_tangents = third_order_solution_pullback((∂𝐒₃_raw, NoTangent())) ∂∇₁_from_so3 = so3_tangents[2] isa Union{NoTangent, AbstractZero} ? zero(∇₁) : so3_tangents[2] @@ -1356,8 +1345,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂params_from_∇₃ = third_derivatives_tangents[2] ∂SS_and_pars_from_∇₃ = third_derivatives_tangents[3] - # Convert full-space ∂𝐒₂ terms to compressed, then accumulate with compressed ∂𝐒₂_raw_from_so3 - ∂𝐒₂_raw_for_common = ∂𝐒₂_raw_from_so3 + (∂𝐒₂_from_state + ∂𝐒₂_newton + Δ𝐒₂) * 𝐔₂' + ∂𝐒₂_raw_for_common = ∂𝐒₂_raw_from_so3 + ∂𝐒₂_from_state + ∂𝐒₂_newton + Δ𝐒₂ common_tangents = common_pullback((NoTangent(), Δsss, @@ -1409,8 +1397,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - 𝐔₂ = 𝓂.constants.second_order.𝐔₂ - 𝐒₂ = (sparse(𝐒₂_raw) * 𝐔₂)::SparseMatrixCSC{Float64, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝐒₂_raw)::SparseMatrixCSC{Float64, Int} ∇₃, third_derivatives_pullback = rrule(calculate_third_order_derivatives, parameters, SS_and_pars, 𝓂.caches, 𝓂.functions.third_order_derivatives, 𝓂.workspaces) @@ -1445,13 +1432,12 @@ function rrule(::typeof(calculate_stochastic_steady_state), return result, pullback end - 𝐔₃ = 𝓂.constants.third_order.𝐔₃ - 𝐒₃̂ = sparse(𝐒₃) * 𝐔₃ # was: dense_to_sparse + 𝐒₃̂ = sparse(𝐒₃)::SparseMatrixCSC{Float64, Int} T = 𝓂.constants.post_model_macro nPast = T.nPast_not_future_and_mixed aug_state₁ = sparse([zeros(nPast); 1; zeros(T.nExo)]) - kron_aug1 = ℒ.kron(aug_state₁, aug_state₁) + kron_aug1 = compressed_kron²_power(aug_state₁) state = 𝐒₁[:,1:nPast] * SSSstates + 𝐒₂ * kron_aug1 / 2 sss = all_SS + vec(state) @@ -1493,7 +1479,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂𝐒₂_from_state += ∂state_vec * kron_aug1' / 2 ∂SSSstates = 𝐒₁[:,1:nPast]' * ∂state_vec - ∂𝐒₃_raw = Matrix(Δ𝐒₃̂) * 𝐔₃' + ∂𝐒₃_raw = Δ𝐒₃̂ so3_tangents = third_order_solution_pullback((∂𝐒₃_raw, NoTangent())) ∂∇₁_from_so3 = so3_tangents[2] isa Union{NoTangent, AbstractZero} ? zero(∇₁) : so3_tangents[2] ∂∇₂_from_so3 = so3_tangents[3] isa Union{NoTangent, AbstractZero} ? zero(∇₂) : so3_tangents[3] @@ -1510,8 +1496,7 @@ function rrule(::typeof(calculate_stochastic_steady_state), ∂params_from_∇₃ = third_derivatives_tangents[2] ∂SS_and_pars_from_∇₃ = third_derivatives_tangents[3] - # Convert full-space ∂𝐒₂ terms to compressed, then accumulate with compressed ∂𝐒₂_raw_from_so3 - ∂𝐒₂_raw_for_common = ∂𝐒₂_raw_from_so3 + (∂𝐒₂_from_state + Δ𝐒₂) * 𝐔₂' + ∂𝐒₂_raw_for_common = ∂𝐒₂_raw_from_so3 + ∂𝐒₂_from_state + Δ𝐒₂ common_tangents = common_pullback((NoTangent(), Δsss, @@ -2157,7 +2142,8 @@ function irf_bptt(::Val{:pruned_second_order}, ∂SS_from_init = zeros(S, nVar_len) n_aug = nPast + 1 + nExo # Preallocated kron buffer reused across all (si, t) iterations - kaug₁ = Vector{S}(undef, n_aug^2) + kaug₁ = Vector{S}(undef, n_aug * (n_aug + 1) ÷ 2) + vjp_aug₁ = zeros(S, n_aug) for si in 1:nShocks ∂y₁_accum = zeros(S, nVars) @@ -2173,7 +2159,7 @@ function irf_bptt(::Val{:pruned_second_order}, aug₁ = [prev_st[1][past_idx]; one(S); shock_t] aug₂ = [prev_st[2][past_idx]; zero(S); zero(shock_t)] - ℒ.kron!(kaug₁, aug₁, aug₁) + compressed_kron²_power!(kaug₁, aug₁) # y₁_new = 𝐒₁ * aug₁ ∂𝐒₁ .+= ∂y₁_t * aug₁' @@ -2184,8 +2170,8 @@ function irf_bptt(::Val{:pruned_second_order}, ∂aug₂ = 𝐒₁' * ∂δ_t ∂𝐒₂ .+= ∂δ_t * kaug₁' / 2 ∂kaug₁ = 𝐒₂' * ∂δ_t / 2 - ∂kaug₁_mat = reshape(∂kaug₁, n_aug, n_aug) - ∂aug₁ .+= ∂kaug₁_mat' * aug₁ + ∂kaug₁_mat * aug₁ + compressed_kron²_power_vjp!(vjp_aug₁, ∂kaug₁, aug₁) + ∂aug₁ .+= vjp_aug₁ ∂y₁_accum = zeros(S, nVars) ∂δ_accum = zeros(S, nVars) @@ -2215,9 +2201,12 @@ function irf_bptt(::Val{:pruned_third_order}, ∂SS_from_init = zeros(S, nVar_len) n_aug = nPast + 1 + nExo # Preallocated kron buffers reused across all (si, t) iterations - kaug₁ = Vector{S}(undef, n_aug^2) - kaug₁₁ = Vector{S}(undef, n_aug^3) - k_aug₁̂_aug₂ = Vector{S}(undef, n_aug^2) + kaug₁ = Vector{S}(undef, n_aug * (n_aug + 1) ÷ 2) + kaug₁₁ = Vector{S}(undef, n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6) + k_aug₁̂_aug₂ = Vector{S}(undef, n_aug * (n_aug + 1) ÷ 2) + vjp_aug₁ = zeros(S, n_aug) + vjp_aug₁̂ = zeros(S, n_aug) + vjp_aug₂ = zeros(S, n_aug) for si in 1:nShocks ∂y₁_accum = zeros(S, nVars) @@ -2237,8 +2226,8 @@ function irf_bptt(::Val{:pruned_third_order}, aug₁̂ = [prev_st[1][past_idx]; zero(S); shock_t] aug₂ = [prev_st[2][past_idx]; zero(S); zero(shock_t)] aug₃ = [prev_st[3][past_idx]; zero(S); zero(shock_t)] - ℒ.kron!(kaug₁, aug₁, aug₁) - ℒ.kron!(kaug₁₁, kaug₁, aug₁) + compressed_kron²_power!(kaug₁, aug₁) + compressed_kron³_power!(kaug₁₁, aug₁) # y₁_new = 𝐒₁ * aug₁ ∂𝐒₁ .+= ∂y₁_t * aug₁' @@ -2249,28 +2238,24 @@ function irf_bptt(::Val{:pruned_third_order}, ∂aug₂ = 𝐒₁' * ∂δ_t ∂𝐒₂ .+= ∂δ_t * kaug₁' / 2 ∂kaug₁_from_δ = 𝐒₂' * ∂δ_t / 2 - ∂kaug₁_mat = reshape(∂kaug₁_from_δ, n_aug, n_aug) - ∂aug₁ .+= ∂kaug₁_mat' * aug₁ + ∂kaug₁_mat * aug₁ + compressed_kron²_power_vjp!(vjp_aug₁, ∂kaug₁_from_δ, aug₁) + ∂aug₁ .+= vjp_aug₁ # ξ_new = 𝐒₁ * aug₃ + 𝐒₂ * kron(aug₁̂, aug₂) + 𝐒₃ * kron(kaug₁, aug₁) / 6 ∂𝐒₁ .+= ∂ξ_t * aug₃' ∂aug₃ = 𝐒₁' * ∂ξ_t - ℒ.kron!(k_aug₁̂_aug₂, aug₁̂, aug₂) + compressed_kron²!(k_aug₁̂_aug₂, aug₁̂, aug₂) ∂𝐒₂ .+= ∂ξ_t * k_aug₁̂_aug₂' ∂k12 = 𝐒₂' * ∂ξ_t - ∂k12_mat = reshape(∂k12, n_aug, n_aug) - ∂aug₁̂ = ∂k12_mat * aug₂ - ∂aug₂ .+= ∂k12_mat' * aug₁̂ + compressed_kron²_vjp!(vjp_aug₁̂, vjp_aug₂, ∂k12, aug₁̂, aug₂) + ∂aug₁̂ = vjp_aug₁̂ + ∂aug₂ .+= vjp_aug₂ ∂𝐒₃ .+= ∂ξ_t * kaug₁₁' / 6 ∂kaug₁₁ = 𝐒₃' * ∂ξ_t / 6 - n_aug2 = n_aug * n_aug - ∂kaug₁₁_mat = reshape(∂kaug₁₁, n_aug2, n_aug) - ∂kaug₁_from_ξ = ∂kaug₁₁_mat * aug₁ - ∂aug₁ .+= ∂kaug₁₁_mat' * kaug₁ - ∂kaug₁_mat2 = reshape(∂kaug₁_from_ξ, n_aug, n_aug) - ∂aug₁ .+= ∂kaug₁_mat2' * aug₁ + ∂kaug₁_mat2 * aug₁ + compressed_kron³_power_vjp!(vjp_aug₁, ∂kaug₁₁, aug₁) + ∂aug₁ .+= vjp_aug₁ # aug₁̂ shares past_idx and shock with aug₁ ∂aug₁[1:nPast] .+= ∂aug₁̂[1:nPast] @@ -2305,7 +2290,8 @@ function irf_bptt(::Val{:second_order}, ∂state_init = zeros(S, nVars) ∂SS_from_init = zeros(S, nVar_len) n_aug = nPast + 1 + nExo - kaug = Vector{S}(undef, n_aug^2) + kaug = Vector{S}(undef, n_aug * (n_aug + 1) ÷ 2) + vjp_aug = zeros(S, n_aug) for si in 1:nShocks ∂y_accum = zeros(S, nVars) @@ -2316,14 +2302,14 @@ function irf_bptt(::Val{:second_order}, prev_st = states_store[si, t] shock_t = shocks_store[si, t] aug = [prev_st[past_idx]; one(S); shock_t] - ℒ.kron!(kaug, aug, aug) + compressed_kron²_power!(kaug, aug) ∂𝐒₁ .+= ∂y_t * aug' ∂aug = 𝐒₁' * ∂y_t ∂𝐒₂ .+= ∂y_t * kaug' / 2 ∂kaug = 𝐒₂' * ∂y_t / 2 - ∂kaug_mat = reshape(∂kaug, n_aug, n_aug) - ∂aug .+= ∂kaug_mat' * aug + ∂kaug_mat * aug + compressed_kron²_power_vjp!(vjp_aug, ∂kaug, aug) + ∂aug .+= vjp_aug ∂y_accum = zeros(S, nVars) ∂y_accum[past_idx] .+= ∂aug[1:nPast] @@ -2349,8 +2335,9 @@ function irf_bptt(::Val{:third_order}, ∂state_init = zeros(S, nVars) ∂SS_from_init = zeros(S, nVar_len) n_aug = nPast + 1 + nExo - kaug = Vector{S}(undef, n_aug^2) - kaug3 = Vector{S}(undef, n_aug^3) + kaug = Vector{S}(undef, n_aug * (n_aug + 1) ÷ 2) + kaug3 = Vector{S}(undef, n_aug * (n_aug + 1) * (n_aug + 2) ÷ 6) + vjp_aug = zeros(S, n_aug) for si in 1:nShocks ∂y_accum = zeros(S, nVars) @@ -2361,8 +2348,8 @@ function irf_bptt(::Val{:third_order}, prev_st = states_store[si, t] shock_t = shocks_store[si, t] aug = [prev_st[past_idx]; one(S); shock_t] - ℒ.kron!(kaug, aug, aug) - ℒ.kron!(kaug3, kaug, aug) + compressed_kron²_power!(kaug, aug) + compressed_kron³_power!(kaug3, aug) ∂𝐒₁ .+= ∂y_t * aug' ∂aug = 𝐒₁' * ∂y_t @@ -2371,13 +2358,10 @@ function irf_bptt(::Val{:third_order}, ∂𝐒₃ .+= ∂y_t * kaug3' / 6 ∂kaug3 = 𝐒₃' * ∂y_t / 6 - n_aug2 = n_aug * n_aug - ∂kaug3_mat = reshape(∂kaug3, n_aug2, n_aug) - ∂kaug .+= ∂kaug3_mat * aug - ∂aug .+= ∂kaug3_mat' * kaug - - ∂kaug_mat = reshape(∂kaug, n_aug, n_aug) - ∂aug .+= ∂kaug_mat' * aug + ∂kaug_mat * aug + compressed_kron²_power_vjp!(vjp_aug, ∂kaug, aug) + ∂aug .+= vjp_aug + compressed_kron³_power_vjp!(vjp_aug, ∂kaug3, aug) + ∂aug .+= vjp_aug ∂y_accum = zeros(S, nVars) ∂y_accum[past_idx] .+= ∂aug[1:nPast] @@ -6381,6 +6365,49 @@ end function fill_kron_adjoint!(∂A::V, ∂B::V, ∂X::V, A::V, B::V) where V <: Vector{<: Real} @assert size(∂A) == size(A) @assert size(∂B) == size(B) + + # Higher-order policy coefficients use the symmetric pair basis. Keep + # this compatibility helper analytical so older pullback code can share + # the compressed kernel while it is migrated to explicit VJP calls. + if length(A) == length(B) && length(∂X) == length(A) * (length(A) + 1) ÷ 2 + n = length(A) + pair_index = 0 + if ∂A === ∂B + # Both argument cotangents share storage in the power-kernel + # pullbacks. Accumulate their sum without overwriting either + # contribution or reading values after the cotangent is updated. + @inbounds for i in 1:n + for j in 1:i + pair_index += 1 + value = ∂X[pair_index] + if i == j + ∂A[i] += value * (B[i] + A[i]) + else + ∂A[i] += value * (B[j] + A[j]) + ∂A[j] += value * (B[i] + A[i]) + end + end + end + else + @inbounds for i in 1:n + for j in 1:i + pair_index += 1 + value = ∂X[pair_index] + if i == j + ∂A[i] += value * B[i] + ∂B[i] += value * A[i] + else + ∂A[i] += value * B[j] + ∂A[j] += value * B[i] + ∂B[i] += value * A[j] + ∂B[j] += value * A[i] + end + end + end + end + return nothing + end + @assert length(∂X) == length(B) * length(A) "∂X must have the same length as kron(B,A)" re∂X = reshape(∂X, @@ -9521,20 +9548,25 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ensure_inversion_rrule_buffers!(ws, n_exo, n_past, n_cond, Tt; order = :pruned_second_order) cc = ensure_conditional_forecast_constants!(constants) + pair_index_map = cc.compressed_pair_index_map shock_idxs = cc.shock_idxs shock²_idxs = cc.shock²_idxs shockvar²_idxs = cc.shockvar²_idxs var_vol²_idxs = cc.var_vol²_idxs var²_idxs = cc.var²_idxs - + n_global = n_past + 1 + n_exo + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols 𝐒⁻¹ = 𝐒[1][Tcc.past_not_future_and_mixed_idx, :] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:n_past] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_idxs]) - 𝐒²⁻ = collect(𝐒[2][cond_var_idx, var²_idxs]) - 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_idxs]) - 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_idxs]) + 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_cols]) + 𝐒²⁻ = collect(𝐒[2][cond_var_idx, var²_cols]) + 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_cols]) + 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_cols]) 𝐒⁻² = collect(𝐒[2][Tcc.past_not_future_and_mixed_idx, :]) 𝐒ⁱ²ᵉ = 𝐒²ᵉ ./ 2 @@ -9614,7 +9646,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁_seq[1]) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂_seq[1]) - ℒ.kron!(kronaug_state₁, aug_state₁_seq[1], aug_state₁_seq[1]) + compressed_kron²_power!(kronaug_state₁, aug_state₁_seq[1]) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -9637,7 +9669,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(shock_independent, view(data_in_deviations, :, t)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) ℒ.kron!(kron_buffer3, J, state¹⁻_vol) @@ -9668,7 +9700,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if t > presample_periods jac_v = similar(𝐒ⁱ_v) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jac_v, 𝐒ⁱ²ᵉ_v, kron_buffer2) ℒ.axpby!(1, 𝐒ⁱ_v, 2, jac_v) logabsdets += m == n_exo ? ℒ.logabsdet(jac_v)[1] : ℒ.logabsdet(jac_v * jac_v')[1] / 2 @@ -9689,7 +9721,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁_seq[t]) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂_seq[t]) - ℒ.kron!(kronaug_state₁, aug_state₁_seq[t], aug_state₁_seq[t]) + compressed_kron²_power!(kronaug_state₁, aug_state₁_seq[t]) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) copyto!(state₁_seq[t+1], state₁) copyto!(state₂_seq[t+1], state₂) @@ -9719,11 +9751,14 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂state₁_next = zeros(n_past) ∂state₂_next = zeros(n_past) - kronaug_buf = zeros((n_past + 1 + n_exo)^2) - ∂kronaug = zeros((n_past + 1 + n_exo)^2) + n_state_pair = (n_past + 1) * (n_past + 2) ÷ 2 + n_aug_pair = (n_past + 1 + n_exo) * (n_past + 2 + n_exo) ÷ 2 + n_exo² = n_exo * (n_exo + 1) ÷ 2 + kronaug_buf = zeros(n_aug_pair) + ∂kronaug = zeros(n_aug_pair) ∂aug_state₁ = zeros(n_past + 1 + n_exo) ∂aug_state₂ = zeros(n_past + 1 + n_exo) - ∂kronstate = zeros((n_past + 1)^2) + ∂kronstate = zeros(n_state_pair) ∂state¹⁻_vol = zeros(n_past + 1) ∂kronIstate = zeros(n_exo * (n_past + 1)) # Hoisted per-period pullback buffers (max-size, used via views when m varies) @@ -9732,8 +9767,8 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂jac_v_buf = zeros(length(cond_var_idx), n_exo) kron_Isv_buf = zeros(n_exo * (n_past + 1), n_exo) ∂state₂_contrib = zeros(n_past) - ∂kron_sv = zeros((n_past + 1)^2) - kron_sv = zeros((n_past + 1)^2) + ∂kron_sv = zeros(n_state_pair) + kron_sv = zeros(n_state_pair) ∂kronIstate_local = zeros(n_exo * (n_past + 1), n_exo) function pruned2_missing_pullback(∂llh) @@ -9768,7 +9803,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # state₂_next = 𝐒⁻¹ * aug_state₂ + 0.5 * 𝐒⁻² * kron(aug_state₁, aug_state₁) ℒ.mul!(∂𝐒⁻¹, ∂state₂_next, aug_state₂', 1, 1) ℒ.mul!(∂aug_state₂, 𝐒⁻¹', ∂state₂_next) - ℒ.kron!(kronaug_buf, aug_state₁, aug_state₁) + compressed_kron²_power!(kronaug_buf, aug_state₁) ℒ.mul!(∂𝐒⁻², ∂state₂_next, kronaug_buf', 1/2, 1) ℒ.mul!(∂kronaug, 𝐒⁻²', ∂state₂_next) ℒ.rdiv!(∂kronaug, 2) @@ -9791,11 +9826,12 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # shocks² and logabsdet contributions (only if t > presample and m > 0) ∂jac_v = view(∂jac_v_buf, 1:m, :); fill!(∂jac_v, 0) jac_v_local = zeros(m, n_exo) - 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo^2) + 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo²) if m > 0 𝐒ⁱ_v_local = 𝐒ⁱ_full_seq[t][idx, :] 𝐒ⁱ²ᵉ_v_local = 𝐒ⁱ²ᵉ[idx, :] - jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * ℒ.kron(J, x) + compressed_kron²!(kron_buffer2, x, J) + jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * kron_buffer2 end if m > 0 && t > presample_periods # ∂shocks² = -1/2 (from llh wrt shocks²); ∂x_k += -x_k @@ -9815,7 +9851,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} @inbounds for l in 1:n_exo s = 0.0 for r in 1:n_exo - col = (r-1) * n_exo + l + col = pair_index_map[r, l] for i_local in 1:m s += ∂jac_v[i_local, r] * 𝐒ⁱ²ᵉ_v_local[i_local, col] end @@ -9841,9 +9877,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end # KKT system: G(y; θ) = [2x - jac_v(x)' λ; F(x; θ)] = 0. - # dG/dy = [2I - 2 reshape(𝐒ⁱ²ᵉ_v' λ, n, n) -jac_v'] + # dG/dy = [2I - 2 H(𝐒ⁱ²ᵉ_v' λ) -jac_v'] # [jac_v 0 ] - M = reshape(𝐒ⁱ²ᵉ_v' * λ, n_exo, n_exo) + M = zeros(n_exo, n_exo) + compressed_pair_hessian!(M, 𝐒ⁱ²ᵉ_v' * λ) topL = 2 * ℒ.I(n_exo) - 2 * M fXλp = [topL -jac_v' jac_v zeros(m, m)] @@ -9865,18 +9902,28 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # ∂𝐒ⁱ²ᵉ_v from KKT: # dG_top[r]/d𝐒ⁱ²ᵉ_v[i, (p-1)n_exo+q] = -2 δ_{rp} x_q λ[i] → contrib = +2 λ[i] Sx[p] x_q # dG_F[i']/d𝐒ⁱ²ᵉ_v[i, (p-1)n_exo+q] = δ_{ii'} x_p x_q → contrib = -Sλ[i] x_p x_q - xSx = x * Sx' # xSx[q,p] = x_q * Sx[p] - xx_outer = x * x' # symmetric - ∂𝐒ⁱ²ᵉ_v_top = 2 * λ * vec(xSx)' - ∂𝐒ⁱ²ᵉ_v_F = -Sλ * vec(xx_outer)' - ∂𝐒ⁱ²ᵉ_v_kkt = ∂𝐒ⁱ²ᵉ_v_top + ∂𝐒ⁱ²ᵉ_v_F + ∂xx = compressed_kron²_power(x) + pair_top = Vector{Float64}(undef, n_exo²) + pair_column = 0 + @inbounds for p in 1:n_exo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? Sx[p] * x[q] : Sx[p] * x[q] + Sx[q] * x[p] + end + end + # Two rank-1 updates into one matrix, rather than building the + # two terms separately and adding them. + ∂𝐒ⁱ²ᵉ_v_kkt = zeros(m, n_exo²) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, λ, pair_top', 2, 0) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, Sλ, ∂xx', -1, 1) # Add direct ∂jac_v contributions: # ∂𝐒ⁱ_v += ∂jac_v # ∂𝐒ⁱ²ᵉ_v += 2 * ∂jac_v * kron(I, x)' if t > presample_periods ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v + ∂jac_v - ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * ℒ.kron(J, x)' + compressed_kron²!(kron_buffer2, x, J) + ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * kron_buffer2' else ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt @@ -9890,7 +9937,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ_full[idx[i_local], j] = ∂𝐒ⁱ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^2 + @inbounds for j in 1:n_exo² for i_local in 1:m ∂𝐒ⁱ²ᵉ[idx[i_local], j] += ∂𝐒ⁱ²ᵉ_v_total[i_local, j] end @@ -9935,7 +9982,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂state₂_next[j] += -∂state₂_contrib[j] end # ∂𝐒²⁻ᵛ -= 0.5 * ∂shock_independent * kron(s¹⁻_vol, s¹⁻_vol)' - kron_sv = ℒ.kron(state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kron_sv, state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, kron_sv', -1/2, 1) # ∂kron_sv = -0.5 * 𝐒²⁻ᵛ' * ∂shock_independent ∂kron_sv = -(𝐒²⁻ᵛ' * ∂shock_independent) ./ 2 @@ -10020,10 +10067,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒_1[cond_var_idx, 1:n_past+1] .+= ∂𝐒¹⁻ᵛ ∂𝐒_1[cond_var_idx, 1:n_past] .+= ∂𝐒¹⁻ ∂𝐒_1[cond_var_idx, end-n_exo+1:end] .+= ∂𝐒¹ᵉ - ∂𝐒_2[cond_var_idx, var_vol²_idxs] .+= ∂𝐒²⁻ᵛ - ∂𝐒_2[cond_var_idx, shockvar²_idxs] .+= ∂𝐒²⁻ᵉ + ∂𝐒_2[cond_var_idx, var_vol²_cols] .+= ∂𝐒²⁻ᵛ + ∂𝐒_2[cond_var_idx, shockvar²_cols] .+= ∂𝐒²⁻ᵉ # 𝐒ⁱ²ᵉ = 𝐒²ᵉ / 2 → ∂𝐒²ᵉ = ∂𝐒ⁱ²ᵉ / 2 - ∂𝐒_2[cond_var_idx, shock²_idxs] .+= ∂𝐒ⁱ²ᵉ ./ 2 + ∂𝐒_2[cond_var_idx, shock²_cols] .+= ∂𝐒ⁱ²ᵉ ./ 2 ℒ.rmul!(∂𝐒_1, ∂llh) ℒ.rmul!(∂𝐒_2, ∂llh) @@ -10045,8 +10092,26 @@ function accumulate_sym_kron_jacobian_pullback!( ∂vec::AbstractVector{Float64}, ∂jac::AbstractMatrix{Float64}, vec::AbstractVector{Float64}, + scale::Real = 1, ) n = length(vec) + + # For a compressed Jacobian, J(a) = compressed_kron²(a, I), each column + # is a pair kernel with a fixed basis vector. Differentiate those + # columns directly; no full n² Jacobian is formed. + if size(∂jac, 1) == n * (n + 1) ÷ 2 + basis = zeros(Float64, n) + ∂basis = zeros(Float64, n) + ∂column = zeros(Float64, n) + @inbounds for j in 1:n + fill!(basis, 0.0) + basis[j] = 1.0 + compressed_kron²_vjp!(∂column, ∂basis, view(∂jac, :, j), vec, basis, scale) + ∂vec .+= ∂column + end + return nothing + end + basis = zeros(Float64, n) ∂dummy = zeros(Float64, n) seed = zeros(Float64, size(∂jac, 1)) @@ -10055,6 +10120,7 @@ function accumulate_sym_kron_jacobian_pullback!( fill!(basis, 0.0) basis[j] = 1.0 copyto!(seed, 1, view(∂jac, :, j), 1, length(seed)) + seed .*= scale ℒ.rdiv!(seed, 2) fill!(∂dummy, 0.0) @@ -10230,7 +10296,7 @@ function second_order_warmup_state_pullback!( @inbounds for i in 1:warmup_iterations-1 aug_state = [st; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) st = 𝐒⁻¹ * aug_state + 𝐒⁻² * kronaug_state / 2 state_hist[i + 1] = copy(st) end @@ -10239,7 +10305,7 @@ function second_order_warmup_state_pullback!( @inbounds for i in warmup_iterations-1:-1:1 aug_state = [state_hist[i]; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) ℒ.mul!(∂𝐒⁻¹, ∂state, aug_state', 1, 1) ℒ.mul!(∂𝐒⁻², ∂state, kronaug_state', 1/2, 1) @@ -10281,7 +10347,11 @@ function second_order_warmup_observation_and_jacobian_pullback!( ∂jac_seed::AbstractMatrix{Float64}, I_aug::AbstractMatrix{Float64}, I_state_vol::AbstractMatrix{Float64}, - I_exo::AbstractMatrix{Float64} + I_exo::AbstractMatrix{Float64}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing ) n_past = length(state0) n_exo = size(𝐒¹ᵉ, 2) @@ -10306,10 +10376,10 @@ function second_order_warmup_observation_and_jacobian_pullback!( ds_hist[i] = copy(ds_dz) aug_state = [st; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) state_next = 𝐒⁻¹ * aug_state + 𝐒⁻² * kronaug_state / 2 - jac_aug = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state, I_aug) Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug[:, 1:n_past] Fu = 𝐒⁻¹[:, n_past + 2:end] + 𝐒⁻² * jac_aug[:, n_past + 2:end] @@ -10322,12 +10392,16 @@ function second_order_warmup_observation_and_jacobian_pullback!( state_vol = [state_hist[end]; 1.0] final_shock = copy(view(warmup_shocks, :, n_warm)) - kronstate_vol = ℒ.kron(state_vol, state_vol) - jac_state_vol = (ℒ.kron(I_state_vol, state_vol) + ℒ.kron(state_vol, I_state_vol)) / 2 + kronstate_vol = compressed_kron²_power(state_vol) + jac_state_vol = compressed_kron²(state_vol, I_state_vol) + # 𝐒²⁻ᵉ multiplies a rectangular shock×state kron, not a symmetric pair, so + # this stays a plain kron (as in second_order_warmup_observation_and_jacobian + # and in the ∂𝐒²⁻ᵉ term below). jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * jac_state_vol + 𝐒²⁻ᵉ * ℒ.kron(final_shock, I_state_vol) ∂state = zeros(Float64, n_past) ∂state_vol = zeros(Float64, n_state_vol) + ∂state_vol_pair = zeros(Float64, n_state_vol) ∂final_shock = zeros(Float64, n_exo) ∂ds_dz = zeros(Float64, n_past, n_z) @@ -10336,7 +10410,8 @@ function second_order_warmup_observation_and_jacobian_pullback!( ℒ.mul!(∂𝐒²⁻ᵛ, ∂y_pred, kronstate_vol', 1/2, 1) ∂kronstate_vol = 𝐒²⁻ᵛ' * ∂y_pred / 2 - fill_kron_adjoint!(∂state_vol, ∂state_vol, ∂kronstate_vol, state_vol, state_vol) + compressed_kron²_power_vjp!(∂state_vol_pair, ∂kronstate_vol, state_vol) + ∂state_vol .+= ∂state_vol_pair ℒ.mul!(∂𝐒¹ᵉ, ∂y_pred, final_shock', 1, 1) ℒ.mul!(∂final_shock, 𝐒¹ᵉ', ∂y_pred, 1, 1) @@ -10346,7 +10421,7 @@ function second_order_warmup_observation_and_jacobian_pullback!( ∂kron_shock_state = 𝐒²⁻ᵉ' * ∂y_pred fill_kron_adjoint!(∂state_vol, ∂final_shock, ∂kron_shock_state, state_vol, final_shock) - kron_shock_shock = ℒ.kron(final_shock, final_shock) + kron_shock_shock = compressed_kron²_power(final_shock) ℒ.mul!(∂𝐒²ᵉ, ∂y_pred, kron_shock_shock', 1/2, 1) ∂kron_shock_shock = 𝐒²ᵉ' * ∂y_pred / 2 fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock, final_shock, final_shock) @@ -10365,7 +10440,7 @@ function second_order_warmup_observation_and_jacobian_pullback!( ∂kron_I_state = 𝐒²⁻ᵉ' * ∂jac_x fill_kron_adjoint_∂A!(∂kron_I_state, ∂state_vol, I_exo) - sym_kron_shock = (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 + sym_kron_shock = compressed_kron²(final_shock, I_exo) ℒ.mul!(∂𝐒²ᵉ, ∂jac_x, sym_kron_shock', 1, 1) ∂sym_kron_shock = 𝐒²ᵉ' * ∂jac_x accumulate_sym_kron_jacobian_pullback!(∂final_shock, ∂sym_kron_shock, final_shock) @@ -10388,9 +10463,9 @@ function second_order_warmup_observation_and_jacobian_pullback!( state_before = state_hist[i] ds_before = ds_hist[i] aug_state = [state_before; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) - jac_aug = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state, I_aug) Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug[:, 1:n_past] Fu = 𝐒⁻¹[:, n_past + 2:end] + 𝐒⁻² * jac_aug[:, n_past + 2:end] @@ -10410,7 +10485,7 @@ function second_order_warmup_observation_and_jacobian_pullback!( ℒ.mul!(∂𝐒⁻², ∂Fs, jac_aug[:, 1:n_past]', 1, 1) ℒ.mul!(∂𝐒⁻², ∂Fu, jac_aug[:, n_past + 2:end]', 1, 1) - ∂jac_aug = zeros(Float64, n_aug^2, n_aug) + ∂jac_aug = zeros(Float64, n_aug * (n_aug + 1) ÷ 2, n_aug) ℒ.mul!(view(∂jac_aug, :, 1:n_past), 𝐒⁻²', ∂Fs, 1, 0) ℒ.mul!(view(∂jac_aug, :, n_past + 2:n_aug), 𝐒⁻²', ∂Fu, 1, 0) accumulate_sym_kron_jacobian_pullback!(∂aug_state, ∂jac_aug, aug_state) @@ -10742,7 +10817,7 @@ function pruned_second_order_warmup_state_pullback!( @inbounds for i in 1:warmup_iterations-1 aug_state1 = [state1; 1.0; view(warmup_shocks, :, i)] aug_state2 = [state2; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) state1 = 𝐒⁻¹ * aug_state1 state2 = 𝐒⁻¹ * aug_state2 + 𝐒⁻² * kronaug_state1 / 2 @@ -10758,7 +10833,7 @@ function pruned_second_order_warmup_state_pullback!( block = (i - 1) * n_exo + 1:i * n_exo aug_state1 = [state1_hist[i]; 1.0; view(warmup_shocks, :, i)] aug_state2 = [state2_hist[i]; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) ℒ.mul!(∂𝐒⁻¹, ∂state1, aug_state1', 1, 1) ∂aug_state1 = 𝐒⁻¹' * ∂state1 @@ -10809,7 +10884,11 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( ∂jac_seed::AbstractMatrix{Float64}, I_aug::AbstractMatrix{Float64}, I_state_vol::AbstractMatrix{Float64}, - I_exo::AbstractMatrix{Float64} + I_exo::AbstractMatrix{Float64}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing ) n_past = length(state10) n_exo = size(𝐒¹ᵉ, 2) @@ -10841,12 +10920,12 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( aug_state1 = [state1; 1.0; view(warmup_shocks, :, i)] aug_state2 = [state2; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) state1_next = 𝐒⁻¹ * aug_state1 state2_next = 𝐒⁻¹ * aug_state2 + 𝐒⁻² * kronaug_state1 / 2 - jac_aug = (ℒ.kron(I_aug, aug_state1) + ℒ.kron(aug_state1, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state1, I_aug) A11 = 𝐒⁻¹[:, 1:n_past] B1 = 𝐒⁻¹[:, n_past + 2:end] A22 = 𝐒⁻¹[:, 1:n_past] @@ -10867,8 +10946,8 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( state1_vol = [state1_hist[end]; 1.0] final_shock = copy(view(warmup_shocks, :, n_warm)) - kronstate1_vol = ℒ.kron(state1_vol, state1_vol) - jac_state1_vol = (ℒ.kron(I_state_vol, state1_vol) + ℒ.kron(state1_vol, I_state_vol)) / 2 + kronstate1_vol = compressed_kron²_power(state1_vol) + jac_state1_vol = compressed_kron²(state1_vol, I_state_vol) kron_shock_I = ℒ.kron(final_shock, I_state_vol) jac_y_s1 = 𝐒¹⁻ᵛ[:, 1:n_past] + 𝐒²⁻ᵛ * jac_state1_vol[:, 1:n_past] + 𝐒²⁻ᵉ * kron_shock_I[:, 1:n_past] @@ -10899,7 +10978,7 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( ∂kron_shock_state = 𝐒²⁻ᵉ' * ∂y_pred fill_kron_adjoint!(∂state1_vol, ∂final_shock, ∂kron_shock_state, state1_vol, final_shock) - kron_shock_shock = ℒ.kron(final_shock, final_shock) + kron_shock_shock = compressed_kron²_power(final_shock) ℒ.mul!(∂𝐒²ᵉ, ∂y_pred, kron_shock_shock', 1/2, 1) ∂kron_shock_shock = 𝐒²ᵉ' * ∂y_pred / 2 fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock, final_shock, final_shock) @@ -10923,7 +11002,7 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( ∂kron_I_state = 𝐒²⁻ᵉ' * ∂jac_x fill_kron_adjoint_∂A!(∂kron_I_state, ∂state1_vol, I_exo) - sym_kron_shock = (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 + sym_kron_shock = compressed_kron²(final_shock, I_exo) ℒ.mul!(∂𝐒²ᵉ, ∂jac_x, sym_kron_shock', 1, 1) ∂sym_kron_shock = 𝐒²ᵉ' * ∂jac_x accumulate_sym_kron_jacobian_pullback!(∂final_shock, ∂sym_kron_shock, final_shock) @@ -10931,7 +11010,7 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( @views ℒ.axpy!(1, ∂jac_y_s1, ∂𝐒¹⁻ᵛ[:, 1:n_past]) ℒ.mul!(∂𝐒²⁻ᵛ, ∂jac_y_s1, jac_state1_vol[:, 1:n_past]', 1, 1) - ∂jac_state1_vol = zeros(Float64, n_state_vol^2, n_state_vol) + ∂jac_state1_vol = zeros(Float64, n_state_vol * (n_state_vol + 1) ÷ 2, n_state_vol) @views ℒ.mul!(view(∂jac_state1_vol, :, 1:n_past), 𝐒²⁻ᵛ', ∂jac_y_s1, 1, 0) accumulate_sym_kron_jacobian_pullback!(∂state1_vol, ∂jac_state1_vol, state1_vol) @@ -10954,9 +11033,9 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( aug_state1 = [state1_before; 1.0; view(warmup_shocks, :, i)] aug_state2 = [state2_before; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) - jac_aug = (ℒ.kron(I_aug, aug_state1) + ℒ.kron(aug_state1, I_aug)) / 2 + jac_aug = compressed_kron²(aug_state1, I_aug) A11 = 𝐒⁻¹[:, 1:n_past] B1 = 𝐒⁻¹[:, n_past + 2:end] A22 = 𝐒⁻¹[:, 1:n_past] @@ -10995,7 +11074,7 @@ function pruned_second_order_warmup_observation_and_jacobian_pullback!( ∂kronaug_state1 = 𝐒⁻²' * ∂state2 / 2 fill_kron_adjoint!(∂aug_state1, ∂aug_state1, ∂kronaug_state1, aug_state1, aug_state1) - ∂jac_aug = zeros(Float64, n_aug^2, n_aug) + ∂jac_aug = zeros(Float64, n_aug * (n_aug + 1) ÷ 2, n_aug) ℒ.mul!(view(∂jac_aug, :, 1:n_past), 𝐒⁻²', ∂A21, 1, 0) ℒ.mul!(view(∂jac_aug, :, n_past + 2:n_aug), 𝐒⁻²', ∂B2, 1, 0) accumulate_sym_kron_jacobian_pullback!(∂aug_state1, ∂jac_aug, aug_state1) @@ -11370,11 +11449,11 @@ function pruned_third_order_warmup_state_pullback!( aug_state1hat = [state1; 0.0; view(warmup_shocks, :, i)] aug_state2 = [state2; 0.0; zeros(Float64, n_exo)] aug_state3 = [state3; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) state1 = 𝐒⁻¹ * aug_state1 state2 = 𝐒⁻¹ * aug_state2 + 𝐒⁻² * kronaug_state1 / 2 - state3 = 𝐒⁻¹ * aug_state3 + 𝐒⁻² * ℒ.kron(aug_state1hat, aug_state2) + 𝐒⁻³ * ℒ.kron(kronaug_state1, aug_state1) / 6 + state3 = 𝐒⁻¹ * aug_state3 + 𝐒⁻² * compressed_kron²(aug_state1hat, aug_state2) + 𝐒⁻³ * compressed_kron³_power(aug_state1) / 6 state1_hist[i + 1] = copy(state1) state2_hist[i + 1] = copy(state2) @@ -11391,7 +11470,7 @@ function pruned_third_order_warmup_state_pullback!( aug_state1hat = [state1_hist[i]; 0.0; view(warmup_shocks, :, i)] aug_state2 = [state2_hist[i]; 0.0; zeros(Float64, n_exo)] aug_state3 = [state3_hist[i]; 0.0; zeros(Float64, n_exo)] - kronaug_state1 = ℒ.kron(aug_state1, aug_state1) + kronaug_state1 = compressed_kron²_power(aug_state1) ∂aug_state1 = zeros(Float64, n_aug) ∂aug_state1hat = zeros(Float64, n_aug) @@ -11400,26 +11479,31 @@ function pruned_third_order_warmup_state_pullback!( ℒ.mul!(∂𝐒⁻¹, ∂state3, aug_state3', 1, 1) ∂aug_state3 = 𝐒⁻¹' * ∂state3 - kron_aug_state1hat_state2 = ℒ.kron(aug_state1hat, aug_state2) + kron_aug_state1hat_state2 = compressed_kron²(aug_state1hat, aug_state2) ℒ.mul!(∂𝐒⁻², ∂state3, kron_aug_state1hat_state2', 1, 1) ∂kron_aug_state1hat_state2 = 𝐒⁻²' * ∂state3 - fill_kron_adjoint!(∂aug_state1hat, ∂aug_state2, ∂kron_aug_state1hat_state2, aug_state1hat, aug_state2) + compressed_kron²_vjp!(∂aug_state1hat, ∂aug_state2, ∂kron_aug_state1hat_state2, aug_state1hat, aug_state2) - kron_kron_aug_state1 = ℒ.kron(kronaug_state1, aug_state1) + kron_kron_aug_state1 = compressed_kron³_power(aug_state1) ℒ.mul!(∂𝐒⁻³, ∂state3, kron_kron_aug_state1', 1/6, 1) ∂kron_kron_aug_state1 = 𝐒⁻³' * ∂state3 / 6 - ∂kronaug_state1 = zeros(Float64, n_aug^2) - fill_kron_adjoint!(∂aug_state1, ∂kronaug_state1, ∂kron_kron_aug_state1, aug_state1, kronaug_state1) + ∂triple_aug_state1 = zeros(Float64, n_aug) + compressed_kron³_power_vjp!(∂triple_aug_state1, ∂kron_kron_aug_state1, aug_state1) + ∂aug_state1 .+= ∂triple_aug_state1 ℒ.mul!(∂𝐒⁻¹, ∂state2, aug_state2', 1, 1) - ℒ.mul!(∂aug_state2, 𝐒⁻¹', ∂state2, 1, 1) + ∂aug_state2_linear = 𝐒⁻¹' * ∂state2 + ∂aug_state2 .+= ∂aug_state2_linear ℒ.mul!(∂𝐒⁻², ∂state2, kronaug_state1', 1/2, 1) - ∂kronaug_state1 .+= 𝐒⁻²' * ∂state2 / 2 - fill_kron_adjoint!(∂aug_state1, ∂aug_state1, ∂kronaug_state1, aug_state1, aug_state1) + ∂kronaug_state1 = 𝐒⁻²' * ∂state2 / 2 + ∂pair_aug_state1 = zeros(Float64, n_aug) + compressed_kron²_power_vjp!(∂pair_aug_state1, ∂kronaug_state1, aug_state1) + ∂aug_state1 .+= ∂pair_aug_state1 ℒ.mul!(∂𝐒⁻¹, ∂state1, aug_state1', 1, 1) - ℒ.mul!(∂aug_state1, 𝐒⁻¹', ∂state1, 1, 1) + ∂aug_state1_linear = 𝐒⁻¹' * ∂state1 + ∂aug_state1 .+= ∂aug_state1_linear copyto!(∂state1, 1, ∂aug_state1, 1, n_past) @views ℒ.axpy!(1, ∂aug_state1hat[1:n_past], ∂state1) @@ -11479,6 +11563,11 @@ function rrule(::typeof(calculate_loglikelihood), shockvar²_idxs = cc.shockvar²_idxs var_vol²_idxs = cc.var_vol²_idxs var²_idxs = cc.var²_idxs + n_global = T.nPast_not_future_and_mixed + 1 + T.nExo + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒⁻¹ᵉ = 𝐒[1][T.past_not_future_and_mixed_idx,end-T.nExo+1:end] @@ -11486,10 +11575,10 @@ function rrule(::typeof(calculate_loglikelihood), 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -11501,11 +11590,12 @@ function rrule(::typeof(calculate_loglikelihood), state₁ = state[1][T.past_not_future_and_mixed_idx] state₂ = state[2][T.past_not_future_and_mixed_idx] - kronxx = [zeros(T.nExo^2) for _ in 1:size(data_in_deviations,2)] + n_exo² = T.nExo * (T.nExo + 1) ÷ 2 + kronxx = [zeros(n_exo²) for _ in 1:size(data_in_deviations,2)] J = ℒ.I(T.nExo) - kron_buffer2 = ℒ.kron(J, zeros(T.nExo)) + kron_buffer2 = zeros(n_exo², T.nExo) kron_buffer3 = ℒ.kron(J, zeros(T.nPast_not_future_and_mixed + 1)) @@ -11514,6 +11604,7 @@ function rrule(::typeof(calculate_loglikelihood), state¹⁻ = state₁ state¹⁻_vol = vcat(state¹⁻, 1) + kronstate¹⁻_vol = ws.kronstate_vol state²⁻ = state₂ @@ -11550,7 +11641,8 @@ function rrule(::typeof(calculate_loglikelihood), warmup_aug₁ = zeros(size(𝐒⁻¹, 2)) warmup_aug₂ = zeros(size(𝐒⁻¹, 2)) - warmup_kronaug₁ = zeros(size(𝐒⁻¹, 2)^2) + n_aug = size(𝐒⁻¹, 2) + warmup_kronaug₁ = zeros(n_aug * (n_aug + 1) ÷ 2) warmup_shocks = reshape(x_warmup, T.nExo, warmup_iterations) @inbounds for w in 1:warmup_iterations-1 copyto!(warmup_aug₁, 1, state₁, 1, T.nPast_not_future_and_mixed) @@ -11563,7 +11655,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(state₁, 𝐒⁻¹, warmup_aug₁) ℒ.mul!(state₂, 𝐒⁻¹, warmup_aug₂) - ℒ.kron!(warmup_kronaug₁, warmup_aug₁, warmup_aug₁) + compressed_kron²_power!(warmup_kronaug₁, warmup_aug₁) ℒ.mul!(state₂, 𝐒⁻², warmup_kronaug₁, 1/2, 1) end @@ -11576,7 +11668,8 @@ function rrule(::typeof(calculate_loglikelihood), aug_state₁ = [copy([state₁; 1; ones(T.nExo)]) for _ in 1:size(data_in_deviations,2)] aug_state₂ = [zeros(size(𝐒⁻¹,2)) for _ in 1:size(data_in_deviations,2)] - tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(length(x[1])), x[1]) + compressed_kron²!(kron_buffer2, x[1], J) + tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * kron_buffer2 jacc = [zero(tmp) for _ in 1:size(data_in_deviations,2)] @@ -11586,26 +11679,29 @@ function rrule(::typeof(calculate_loglikelihood), λ[1] = copy(tmp' \ x[1] * 2) - fXλp_tmp = [reshape(2 * 𝐒ⁱ²ᵉ' * λ[1], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - 2 * ℒ.I(size(𝐒ⁱ, 2)) tmp' + top_tmp = zeros(T.nExo, T.nExo) + compressed_pair_hessian!(top_tmp, 2 * (𝐒ⁱ²ᵉ' * λ[1])) + top_tmp .-= 2 .* Matrix(ℒ.I(T.nExo)) + fXλp_tmp = [top_tmp tmp' -tmp zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] fXλp = [zero(fXλp_tmp) for _ in 1:size(data_in_deviations,2)] - kronxλ_tmp = ℒ.kron(x[1], λ[1]) + kronxλ_tmp = zeros(T.nExo * size(𝐒ⁱ, 1)) kronxλ = [zero(kronxλ_tmp) for _ in 1:size(data_in_deviations,2)] - kronstate¹⁻_vol = zeros((T.nPast_not_future_and_mixed + 1)^2) + n_state_vol = T.nPast_not_future_and_mixed + 1 + kronstate¹⁻_vol = zeros(n_state_vol * (n_state_vol + 1) ÷ 2) - kronaug_state₁ = zeros(length(aug_state₁[1])^2) + n_aug = length(aug_state₁[1]) + kronaug_state₁ = zeros(n_aug * (n_aug + 1) ÷ 2) shock_independent = zeros(size(data_in_deviations,1)) init_guess = zeros(size(𝐒ⁱ, 2)) - tmp = zeros(size(𝐒ⁱ, 2) * size(𝐒ⁱ, 2)) - - lI = -2 * vec(ℒ.I(size(𝐒ⁱ, 2))) + tmp = zeros(T.nExo, T.nExo) # end # timeit_debug # @timeit_debug timer "Main loop" begin @@ -11625,7 +11721,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) @@ -11657,7 +11753,7 @@ function rrule(::typeof(calculate_loglikelihood), end # jacc[i] = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(length(x[i])), x[i]) - ℒ.kron!(kron_buffer2, J, x[i]) + compressed_kron²!(kron_buffer2, x[i], J) ℒ.mul!(jacc[i], 𝐒ⁱ²ᵉ, kron_buffer2) @@ -11691,15 +11787,17 @@ function rrule(::typeof(calculate_loglikelihood), # fXλp[i] = [reshape(2 * 𝐒ⁱ²ᵉ' * λ[i], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - 2 * ℒ.I(size(𝐒ⁱ, 2)) jacc[i]' # -jacc[i] zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] - ℒ.mul!(tmp, 𝐒ⁱ²ᵉ', λ[i]) - ℒ.axpby!(1, lI, 2, tmp) + compressed_pair_hessian!(tmp, 2 * (𝐒ⁱ²ᵉ' * λ[i])) + tmp .-= 2 .* Matrix(ℒ.I(T.nExo)) fXλp[i][1:size(𝐒ⁱ, 2), 1:size(𝐒ⁱ, 2)] = tmp fXλp[i][size(𝐒ⁱ, 2)+1:end, 1:size(𝐒ⁱ, 2)] = -jacc[i] fXλp[i][1:size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)+1:end] = jacct - ℒ.kron!(kronxx[i], x[i], x[i]) + compressed_kron²_power!(kronxx[i], x[i]) + # Retained as a rectangular KKT scratch only; it is not a policy + # tensor coordinate. ℒ.kron!(kronxλ[i], x[i], λ[i]) if i > presample_periods @@ -11727,7 +11825,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁[i]) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂[i]) - ℒ.kron!(kronaug_state₁, aug_state₁[i], aug_state₁[i]) + compressed_kron²_power!(kronaug_state₁, aug_state₁[i]) ℒ.mul!(state₂, 𝐒⁻², kronaug_state₁, 1/2, 1) end @@ -11740,13 +11838,13 @@ function rrule(::typeof(calculate_loglikelihood), ∂aug_state₂ = zero(aug_state₂[1]) - ∂kronaug_state₁ = zeros(length(aug_state₁[1])^2) + ∂kronaug_state₁ = zeros(n_aug * (n_aug + 1) ÷ 2) - ∂kronIx = zero(ℒ.kron(ℒ.I(length(x[1])), x[1])) + ∂kronIx = zeros(n_exo², T.nExo) ∂kronIstate¹⁻_vol = zero(ℒ.kron(J, state¹⁻_vol)) - ∂kronstate¹⁻_vol = zero(ℒ.kron(state¹⁻_vol, state¹⁻_vol)) + ∂kronstate¹⁻_vol = zeros(n_state_vol * (n_state_vol + 1) ÷ 2) ∂𝐒ⁱ = zero(𝐒ⁱ) @@ -11784,11 +11882,8 @@ function rrule(::typeof(calculate_loglikelihood), ∂jacc_buf = zero(jacc[1]) ∂xλ_buf = zeros(T.nExo + size(jacc[1], 1)) S_buf = zeros(T.nExo + size(jacc[1], 1)) - kron_xλ = zeros(T.nExo * length(λ[1])) # ℒ.kron(x[i], λ[i]) - kron_S1_xλ = zeros(T.nExo * length(kron_xλ)) # ℒ.kron(S[1:T.nExo], kron(x, λ)) - kron_xx_S2 = zeros(length(kronxx[1]) * size(jacc[1], 1)) # ℒ.kron(kronxx[i], S[T.nExo+1:end]) - function inversion_filter_loglikelihood_pullback(∂llh) + function inversion_filter_loglikelihood_pullback(∂llh) # @timeit_debug timer "Inversion filter pruned 2nd - pullback" begin # @timeit_debug timer "Preallocation" begin @@ -11818,6 +11913,9 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # @timeit_debug timer "Main loop" begin + # Scratch for the compressed pair-basis KKT cotangent inside the loop. + pair_top = zeros(T.nExo * (T.nExo + 1) ÷ 2) + for i in reverse(axes(data_in_deviations,2)) # state₁, state₂ = [𝐒⁻¹ * aug_state₁[i], 𝐒⁻¹ * aug_state₂[i] + 𝐒⁻² * ℒ.kron(aug_state₁[i], aug_state₁[i]) / 2] # state₁ = 𝐒⁻¹ * aug_state₁[i] @@ -11835,7 +11933,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(∂aug_state₂, 𝐒⁻¹', ∂state[2]) # ∂𝐒⁻² += ∂state[2] * ℒ.kron(aug_state₁[i], aug_state₁[i])' / 2 - ℒ.kron!(kronaug_state₁, aug_state₁[i], aug_state₁[i]) + compressed_kron²_power!(kronaug_state₁, aug_state₁[i]) ℒ.mul!(∂𝐒⁻², ∂state[2], kronaug_state₁', 1/2, 1) # ∂kronaug_state₁ = 𝐒⁻²' * ∂state[2] / 2 @@ -11895,14 +11993,10 @@ function rrule(::typeof(calculate_loglikelihood), # ∂kronIx = 𝐒ⁱ²ᵉ' * ∂jacc ℒ.mul!(∂kronIx, 𝐒ⁱ²ᵉ', ∂jacc) - if i < size(data_in_deviations,2) - fill_kron_adjoint_∂B!(∂kronIx, ∂x, -J) - else - fill_kron_adjoint_∂B!(∂kronIx, ∂x, J) - end + accumulate_sym_kron_jacobian_pullback!(∂x, ∂kronIx, x[i], i < size(data_in_deviations, 2) ? -1 : 1) # ∂𝐒ⁱ²ᵉ -= ∂jacc * ℒ.kron(ℒ.I(T.nExo), x[i])' - ℒ.kron!(kron_buffer2, J, x[i]) + compressed_kron²!(kron_buffer2, x[i], J) ℒ.mul!(∂𝐒ⁱ²ᵉ, ∂jacc, kron_buffer2', -1, 1) @@ -11931,12 +12025,27 @@ function rrule(::typeof(calculate_loglikelihood), # ∂𝐒ⁱ -= ∂jacc / 2 ℒ.axpy!(-1/2, ∂jacc, ∂𝐒ⁱ) - # ∂𝐒ⁱ²ᵉ += reshape(2 * ℒ.kron(S[1:T.nExo], ℒ.kron(x[i], λ[i])) - ℒ.kron(kronxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ²ᵉ)) - ℒ.kron!(kron_xλ, x[i], λ[i]) - ℒ.kron!(kron_S1_xλ, S1, kron_xλ) - ℒ.kron!(kron_xx_S2, kronxx[i], S2) - ℒ.axpby!(-1, kron_xx_S2, 2, kron_S1_xλ) - ∂𝐒ⁱ²ᵉ .+= reshape(kron_S1_xλ, size(∂𝐒ⁱ²ᵉ)) + # KKT tensor cotangent in the compressed pair basis. The full-coordinate + # form was + # ∂𝐒ⁱ²ᵉ += reshape(2 * ℒ.kron(S[1:T.nExo], ℒ.kron(x[i], λ[i])) - ℒ.kron(kronxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ²ᵉ)) + # Two preallocated rank-1 updates rather than a column loop. The + # loop form read the target column back on the right of a `.+=`, which + # materialises the column: measured in isolation, 240 B per call at + # nExo = 2 rising to 328 kB at nExo = 40, and 1.6-5.8x slower than + # the two `ger!`s. End to end this block is a small share of the + # pullback — on Smets-Wouters at nExo = 7 over 80 periods it is + # 0.5 MB of 150 MB — but it grows with nExo and there is no reason + # to pay it. + xᵢ = x[i] + pair_column = 0 + @inbounds for p in 1:T.nExo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? S1[p] * xᵢ[q] : S1[p] * xᵢ[q] + S1[q] * xᵢ[p] + end + end + ℒ.mul!(∂𝐒ⁱ²ᵉ, λ[i], pair_top', 2, 1) + ℒ.mul!(∂𝐒ⁱ²ᵉ, S2, kronxx[i]', -1, 1) # 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol) fill!(∂state¹⁻_vol, 0) @@ -11976,7 +12085,7 @@ function rrule(::typeof(calculate_loglikelihood), # ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, ℒ.kron(state¹⁻_vol, state¹⁻_vol), -1/2, 1) # ∂𝐒²⁻ᵛ -= ∂shock_independent * ℒ.kron(state¹⁻_vol, state¹⁻_vol)' / 2 - ℒ.kron!(∂kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(∂kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, ∂kronstate¹⁻_vol', -1/2, 1) # ∂kronstate¹⁻_vol = -𝐒²⁻ᵛ' * ∂shock_independent / 2 @@ -12061,12 +12170,12 @@ function rrule(::typeof(calculate_loglikelihood), fill!(∂𝐒[2], 0) ∂𝐒[1][cond_var_idx,end-T.nExo+1:end] .+= ∂𝐒¹ᵉ - ∂𝐒[2][cond_var_idx,shockvar²_idxs] .+= ∂𝐒²⁻ᵉ + ∂𝐒[2][cond_var_idx,shockvar²_cols] .+= ∂𝐒²⁻ᵉ ℒ.rdiv!(∂𝐒ⁱ²ᵉ, 2) - ∂𝐒[2][cond_var_idx,shock²_idxs] .+= ∂𝐒ⁱ²ᵉ# / 2 + ∂𝐒[2][cond_var_idx,shock²_cols] .+= ∂𝐒ⁱ²ᵉ# / 2 ∂𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] .+= ∂𝐒¹⁻ᵛ - ∂𝐒[2][cond_var_idx,var_vol²_idxs] .+= ∂𝐒²⁻ᵛ + ∂𝐒[2][cond_var_idx,var_vol²_cols] .+= ∂𝐒²⁻ᵛ ∂𝐒[1][T.past_not_future_and_mixed_idx,:] .+= ∂𝐒⁻¹ ∂𝐒[2][T.past_not_future_and_mixed_idx,:] .+= ∂𝐒⁻² @@ -12124,18 +12233,24 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ensure_inversion_rrule_buffers!(ws, n_exo, n_past, n_cond, Tt; order = :second_order) cc = ensure_conditional_forecast_constants!(constants) + pair_index_map = cc.compressed_pair_index_map shock_idxs = cc.shock_idxs shock²_idxs = cc.shock²_idxs shockvar²_idxs = cc.shockvar²_idxs var_vol²_idxs = cc.var_vol²_idxs var²_idxs = cc.var²_idxs + n_global = n_past + 1 + n_exo + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols 𝐒⁻¹ = 𝐒[1][Tcc.past_not_future_and_mixed_idx, :] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_idxs]) - 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_idxs]) - 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_idxs]) + 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_cols]) + 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_cols]) + 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_cols]) 𝐒⁻² = collect(𝐒[2][Tcc.past_not_future_and_mixed_idx, :]) 𝐒ⁱ²ᵉ = 𝐒²ᵉ ./ 2 @@ -12201,7 +12316,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(aug_state_seq[1], 1, st, 1) aug_state_seq[1][n_past + 1] = 1.0 copyto!(aug_state_seq[1], n_past + 2, view(warmup_shocks, :, w), 1) - ℒ.kron!(kronaug_state, aug_state_seq[1], aug_state_seq[1]) + compressed_kron²_power!(kronaug_state, aug_state_seq[1]) ℒ.mul!(st, 𝐒⁻¹, aug_state_seq[1]) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) end @@ -12221,7 +12336,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(shock_independent, view(data_in_deviations, :, t)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) ℒ.kron!(kron_buffer3, J, state¹⁻_vol) @@ -12252,7 +12367,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end if t > presample_periods jac_v = similar(𝐒ⁱ_v) - ℒ.kron!(kron_buffer2, J, x) + compressed_kron²!(kron_buffer2, x, J) ℒ.mul!(jac_v, 𝐒ⁱ²ᵉ_v, kron_buffer2) ℒ.axpby!(1, 𝐒ⁱ_v, 2, jac_v) logabsdets += m == n_exo ? ℒ.logabsdet(jac_v)[1] : ℒ.logabsdet(jac_v * jac_v')[1] / 2 @@ -12272,7 +12387,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # state ← 𝐒⁻¹ aug + 0.5 𝐒⁻² kron(aug, aug) ℒ.mul!(st, 𝐒⁻¹, aug_state_seq[t]) - ℒ.kron!(kronaug_state, aug_state_seq[t], aug_state_seq[t]) + compressed_kron²_power!(kronaug_state, aug_state_seq[t]) ℒ.mul!(st, 𝐒⁻², kronaug_state, 1/2, 1) copyto!(st_seq[t+1], st) end @@ -12295,10 +12410,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ²ᵉ = zero(𝐒ⁱ²ᵉ) ∂data_in_deviations = zeros(size(data_in_deviations)) ∂st_next = zeros(n_past) - kronaug_buf = zeros((n_past + 1 + n_exo)^2) - ∂kronaug = zeros((n_past + 1 + n_exo)^2) + n_state_pair = (n_past + 1) * (n_past + 2) ÷ 2 + n_aug_pair = (n_past + 1 + n_exo) * (n_past + 2 + n_exo) ÷ 2 + n_exo² = n_exo * (n_exo + 1) ÷ 2 + kronaug_buf = zeros(n_aug_pair) + ∂kronaug = zeros(n_aug_pair) ∂aug_state = zeros(n_past + 1 + n_exo) - ∂kronstate = zeros((n_past + 1)^2) + ∂kronstate = zeros(n_state_pair) ∂state¹⁻_vol = zeros(n_past + 1) ∂𝐒ⁱ_full_buf = zeros(length(cond_var_idx), n_exo) ∂shock_independent = zeros(length(cond_var_idx)) @@ -12329,7 +12447,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # st_next = 𝐒⁻¹ * aug + 0.5 * 𝐒⁻² * kron(aug, aug) ℒ.mul!(∂𝐒⁻¹, ∂st_next, aug_state', 1, 1) ℒ.mul!(∂aug_state, 𝐒⁻¹', ∂st_next) - ℒ.kron!(kronaug_buf, aug_state, aug_state) + compressed_kron²_power!(kronaug_buf, aug_state) ℒ.mul!(∂𝐒⁻², ∂st_next, kronaug_buf', 1/2, 1) ℒ.mul!(∂kronaug, 𝐒⁻²', ∂st_next) ℒ.rdiv!(∂kronaug, 2) @@ -12349,11 +12467,12 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # shocks² and logabsdet contributions (only if t > presample and m > 0) ∂jac_v = view(∂jac_v_buf, 1:m, :); fill!(∂jac_v, 0) jac_v_local = zeros(m, n_exo) - 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo^2) + 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo²) if m > 0 𝐒ⁱ_v_local = 𝐒ⁱ_full_seq[t][idx, :] 𝐒ⁱ²ᵉ_v_local = 𝐒ⁱ²ᵉ[idx, :] - jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * ℒ.kron(J, x) + compressed_kron²!(kron_buffer2, x, J) + jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * kron_buffer2 end if m > 0 && t > presample_periods @inbounds for k in 1:n_exo @@ -12371,7 +12490,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} @inbounds for l in 1:n_exo s = 0.0 for r in 1:n_exo - col = (r-1) * n_exo + l + col = pair_index_map[r, l] for i_local in 1:m s += ∂jac_v[i_local, r] * 𝐒ⁱ²ᵉ_v_local[i_local, col] end @@ -12395,7 +12514,8 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} λ = 2 * (Gloc * (jac_v * x)) end - M = reshape(𝐒ⁱ²ᵉ_v' * λ, n_exo, n_exo) + M = zeros(n_exo, n_exo) + compressed_pair_hessian!(M, 𝐒ⁱ²ᵉ_v' * λ) topL = 2 * ℒ.I(n_exo) - 2 * M fXλp = [topL -jac_v' jac_v zeros(m, m)] @@ -12408,15 +12528,25 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂v_v = Sλ ∂𝐒ⁱ_v = λ * Sx' - Sλ * x' - xSx = x * Sx' - xx_outer = x * x' - ∂𝐒ⁱ²ᵉ_v_top = 2 * λ * vec(xSx)' - ∂𝐒ⁱ²ᵉ_v_F = -Sλ * vec(xx_outer)' - ∂𝐒ⁱ²ᵉ_v_kkt = ∂𝐒ⁱ²ᵉ_v_top + ∂𝐒ⁱ²ᵉ_v_F + ∂xx = compressed_kron²_power(x) + pair_top = Vector{Float64}(undef, n_exo²) + pair_column = 0 + @inbounds for p in 1:n_exo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? Sx[p] * x[q] : Sx[p] * x[q] + Sx[q] * x[p] + end + end + # Two rank-1 updates into one matrix, rather than building the + # two terms separately and adding them. + ∂𝐒ⁱ²ᵉ_v_kkt = zeros(m, n_exo²) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, λ, pair_top', 2, 0) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, Sλ, ∂xx', -1, 1) if t > presample_periods ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v + ∂jac_v - ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * ℒ.kron(J, x)' + compressed_kron²!(kron_buffer2, x, J) + ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * kron_buffer2' else ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt @@ -12429,7 +12559,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ_full[idx[i_local], j] = ∂𝐒ⁱ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^2 + @inbounds for j in 1:n_exo² for i_local in 1:m ∂𝐒ⁱ²ᵉ[idx[i_local], j] += ∂𝐒ⁱ²ᵉ_v_total[i_local, j] end @@ -12460,7 +12590,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end ℒ.mul!(∂𝐒¹⁻ᵛ, ∂shock_independent, state¹⁻_vol', -1, 1) ℒ.mul!(∂state¹⁻_vol, 𝐒¹⁻ᵛ', ∂shock_independent, -1, 1) - kron_sv = ℒ.kron(state¹⁻_vol, state¹⁻_vol) + kron_sv = compressed_kron²_power(state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, kron_sv', -1/2, 1) ∂kron_sv = -(𝐒²⁻ᵛ' * ∂shock_independent) ./ 2 fill!(∂kronstate, 0) @@ -12495,7 +12625,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂warmup_jac = zeros(size(warmup_jac)) - ∂𝐒²ᵉ_warmup = zeros(warmup_m, n_exo^2) + ∂𝐒²ᵉ_warmup = zeros(warmup_m, n_exo²) second_order_joint_warmup_solver_pullback!( ∂warmup_state0, view(∂data_in_deviations, warmup_idx0, 1), @@ -12532,9 +12662,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒_2[Tcc.past_not_future_and_mixed_idx, :] .+= ∂𝐒⁻² ∂𝐒_1[cond_var_idx, 1:n_past+1] .+= ∂𝐒¹⁻ᵛ ∂𝐒_1[cond_var_idx, end-n_exo+1:end] .+= ∂𝐒¹ᵉ - ∂𝐒_2[cond_var_idx, var_vol²_idxs] .+= ∂𝐒²⁻ᵛ - ∂𝐒_2[cond_var_idx, shockvar²_idxs] .+= ∂𝐒²⁻ᵉ - ∂𝐒_2[cond_var_idx, shock²_idxs] .+= ∂𝐒ⁱ²ᵉ ./ 2 + ∂𝐒_2[cond_var_idx, var_vol²_cols] .+= ∂𝐒²⁻ᵛ + ∂𝐒_2[cond_var_idx, shockvar²_cols] .+= ∂𝐒²⁻ᵉ + ∂𝐒_2[cond_var_idx, shock²_cols] .+= ∂𝐒ⁱ²ᵉ ./ 2 ℒ.rmul!(∂𝐒_1, ∂llh) ℒ.rmul!(∂𝐒_2, ∂llh) @@ -12593,6 +12723,11 @@ function rrule(::typeof(calculate_loglikelihood), shockvar²_idxs = cc.shockvar²_idxs var_vol²_idxs = cc.var_vol²_idxs var²_idxs = cc.var²_idxs + n_global = T.nPast_not_future_and_mixed + 1 + T.nExo + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒⁻¹ᵉ = 𝐒[1][T.past_not_future_and_mixed_idx,end-T.nExo+1:end] @@ -12600,10 +12735,10 @@ function rrule(::typeof(calculate_loglikelihood), 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -12612,11 +12747,12 @@ function rrule(::typeof(calculate_loglikelihood), 𝐒²ᵉ = nnz(𝐒²ᵉ) / length(𝐒²ᵉ) > .1 ? collect(𝐒²ᵉ) : 𝐒²ᵉ 𝐒⁻² = nnz(𝐒⁻²) / length(𝐒⁻²) > .1 ? collect(𝐒⁻²) : 𝐒⁻² - kronxx = [zeros(T.nExo^2) for _ in 1:size(data_in_deviations,2)] + n_exo² = T.nExo * (T.nExo + 1) ÷ 2 + kronxx = [zeros(n_exo²) for _ in 1:size(data_in_deviations,2)] J = ℒ.I(T.nExo) - kron_buffer2 = ℒ.kron(J, zeros(T.nExo)) + kron_buffer2 = zeros(n_exo², T.nExo) kron_buffer3 = ℒ.kron(J, zeros(T.nPast_not_future_and_mixed + 1)) @@ -12630,7 +12766,7 @@ function rrule(::typeof(calculate_loglikelihood), state¹⁻_vol = vcat(state¹⁻, 1) - kronstate¹⁻_voltmp = ℒ.kron(state¹⁻_vol, state¹⁻_vol) + kronstate¹⁻_voltmp = compressed_kron²_power(state¹⁻_vol) kronstate¹⁻_vol = [kronstate¹⁻_voltmp for _ in 1:size(data_in_deviations,2)] @@ -12666,14 +12802,15 @@ function rrule(::typeof(calculate_loglikelihood), warmup_jac_full = copy(warmup_jac) warmup_aug = zeros(size(𝐒⁻¹, 2)) - warmup_kronaug = zeros(size(𝐒⁻¹, 2)^2) + n_aug = size(𝐒⁻¹, 2) + warmup_kronaug = zeros(n_aug * (n_aug + 1) ÷ 2) warmup_shocks = reshape(x_warmup, T.nExo, warmup_iterations) @inbounds for w in 1:warmup_iterations-1 copyto!(warmup_aug, 1, state¹⁻, 1, T.nPast_not_future_and_mixed) warmup_aug[T.nPast_not_future_and_mixed + 1] = 1.0 copyto!(warmup_aug, T.nPast_not_future_and_mixed + 2, view(warmup_shocks, :, w), 1, T.nExo) - ℒ.kron!(warmup_kronaug, warmup_aug, warmup_aug) + compressed_kron²_power!(warmup_kronaug, warmup_aug) ℒ.mul!(state¹⁻, 𝐒⁻¹, warmup_aug) ℒ.mul!(state¹⁻, 𝐒⁻², warmup_kronaug, 1/2, 1) end @@ -12688,9 +12825,11 @@ function rrule(::typeof(calculate_loglikelihood), aug_state = [[zeros(T.nPast_not_future_and_mixed); 1; zeros(T.nExo)] for _ in 1:size(data_in_deviations,2)] - kronaug_state = [zeros((T.nPast_not_future_and_mixed + 1 + T.nExo)^2) for _ in 1:size(data_in_deviations,2)] + n_aug = T.nPast_not_future_and_mixed + 1 + T.nExo + kronaug_state = [zeros(n_aug * (n_aug + 1) ÷ 2) for _ in 1:size(data_in_deviations,2)] - tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(length(x[1])), x[1]) + compressed_kron²!(kron_buffer2, x[1], J) + tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * kron_buffer2 jacc = [zero(tmp) for _ in 1:size(data_in_deviations,2)] @@ -12700,18 +12839,19 @@ function rrule(::typeof(calculate_loglikelihood), λ[1] = tmp' \ x[1] * 2 - fXλp_tmp = [reshape(2 * 𝐒ⁱ²ᵉ' * λ[1], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - 2 * ℒ.I(size(𝐒ⁱ, 2)) tmp' + top_tmp = zeros(T.nExo, T.nExo) + compressed_pair_hessian!(top_tmp, 2 * (𝐒ⁱ²ᵉ' * λ[1])) + top_tmp .-= 2 .* Matrix(ℒ.I(T.nExo)) + fXλp_tmp = [top_tmp tmp' -tmp zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] fXλp = [zero(fXλp_tmp) for _ in 1:size(data_in_deviations,2)] - kronxλ_tmp = ℒ.kron(x[1], λ[1]) + kronxλ_tmp = zeros(T.nExo * size(𝐒ⁱ, 1)) kronxλ = [kronxλ_tmp for _ in 1:size(data_in_deviations,2)] - tmp = zeros(size(𝐒ⁱ, 2) * size(𝐒ⁱ, 2)) - - lI = -2 * vec(ℒ.I(size(𝐒ⁱ, 2))) + tmp = zeros(T.nExo, T.nExo) init_guess = zeros(size(𝐒ⁱ, 2)) @@ -12729,7 +12869,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol[i], state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol[i], state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol[i], -1/2, 1) @@ -12760,7 +12900,7 @@ function rrule(::typeof(calculate_loglikelihood), return on_failure_loglikelihood, x -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) end - ℒ.kron!(kron_buffer2, J, x[i]) + compressed_kron²!(kron_buffer2, x[i], J) ℒ.mul!(jacc[i], 𝐒ⁱ²ᵉ, kron_buffer2) @@ -12797,14 +12937,14 @@ function rrule(::typeof(calculate_loglikelihood), # fXλp[i] = [reshape(2 * 𝐒ⁱ²ᵉ' * λ[i], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - 2 * ℒ.I(size(𝐒ⁱ, 2)) jacc[i]' # -jacc[i] zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] - ℒ.mul!(tmp, 𝐒ⁱ²ᵉ', λ[i]) - ℒ.axpby!(1, lI, 2, tmp) + compressed_pair_hessian!(tmp, 2 * (𝐒ⁱ²ᵉ' * λ[i])) + tmp .-= 2 .* Matrix(ℒ.I(T.nExo)) fXλp[i][1:size(𝐒ⁱ, 2), 1:size(𝐒ⁱ, 2)] = tmp fXλp[i][size(𝐒ⁱ, 2)+1:end, 1:size(𝐒ⁱ, 2)] = -jacc[i] fXλp[i][1:size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)+1:end] = jacct - ℒ.kron!(kronxx[i], x[i], x[i]) + compressed_kron²_power!(kronxx[i], x[i]) ℒ.kron!(kronxλ[i], x[i], λ[i]) @@ -12829,7 +12969,7 @@ function rrule(::typeof(calculate_loglikelihood), copyto!(aug_state[i], 1, state¹⁻, 1) copyto!(aug_state[i], length(state¹⁻) + 2, x[i], 1) - ℒ.kron!(kronaug_state[i], aug_state[i], aug_state[i]) + compressed_kron²_power!(kronaug_state[i], aug_state[i]) ℒ.mul!(state¹⁻, 𝐒⁻¹, aug_state[i]) ℒ.mul!(state¹⁻, 𝐒⁻², kronaug_state[i], 1/2 ,1) end @@ -12848,7 +12988,7 @@ function rrule(::typeof(calculate_loglikelihood), ∂data_in_deviations = similar(data_in_deviations) - ∂kronIx = zero(ℒ.kron(ℒ.I(length(x[1])), x[1])) + ∂kronIx = zeros(n_exo², T.nExo) ∂𝐒ⁱ = zero(𝐒ⁱ) @@ -12938,6 +13078,9 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # @timeit_debug timer "Main loop" begin + # Scratch for the compressed pair-basis KKT cotangent inside the loop. + pair_top = zeros(T.nExo * (T.nExo + 1) ÷ 2) + for i in reverse(axes(data_in_deviations,2)) # stt = 𝐒⁻¹ * aug_state + 𝐒⁻² * ℒ.kron(aug_state, aug_state) / 2 # ∂𝐒⁻¹ += ∂state * aug_state[i]' @@ -13000,14 +13143,10 @@ function rrule(::typeof(calculate_loglikelihood), # jacc = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ * ℒ.kron(ℒ.I(T.nExo), x[1]) ℒ.mul!(∂kronIx, 𝐒ⁱ²ᵉ', ∂jacc) - if i < size(data_in_deviations,2) - fill_kron_adjoint_∂B!(∂kronIx, ∂x, -J) - else - fill_kron_adjoint_∂B!(∂kronIx, ∂x, J) - end + accumulate_sym_kron_jacobian_pullback!(∂x, ∂kronIx, x[i], i < size(data_in_deviations, 2) ? -1 : 1) # ∂𝐒ⁱ²ᵉ -= ∂jacc * ℒ.kron(ℒ.I(T.nExo), x[i])' - ℒ.kron!(kron_buffer2, J, x[i]) + compressed_kron²!(kron_buffer2, x[i], J) ℒ.mul!(∂𝐒ⁱ²ᵉ, ∂jacc, kron_buffer2', -1, 1) @@ -13036,15 +13175,25 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.axpy!(-1/2, ∂jacc, ∂𝐒ⁱ) - # Dense second-order uses the same KKT tensor term as the missing-data - # implementation: 2 * λ * vec(x * S1')' - S2 * vec(x * x')'. - # The previous kron order built vec((x * λ') ⊗ S1), which permuted - # the second-order columns and produced stable FD mismatches. - ℒ.kron!(kron_S1_x, S1, x[i]) - ℒ.kron!(kron_S1_kxλ, kron_S1_x, λ[i]) - ℒ.kron!(kron_xx_S2, kronxx[i], S2) - ℒ.axpby!(-1, kron_xx_S2, 2, kron_S1_kxλ) - ∂𝐒ⁱ²ᵉ .+= reshape(kron_S1_kxλ, size(∂𝐒ⁱ²ᵉ)) + # KKT tensor cotangent in the compressed pair basis. + # Two preallocated rank-1 updates rather than a column loop. The + # loop form read the target column back on the right of a `.+=`, which + # materialises the column: measured in isolation, 240 B per call at + # nExo = 2 rising to 328 kB at nExo = 40, and 1.6-5.8x slower than + # the two `ger!`s. End to end this block is a small share of the + # pullback — on Smets-Wouters at nExo = 7 over 80 periods it is + # 0.5 MB of 150 MB — but it grows with nExo and there is no reason + # to pay it. + xᵢ = x[i] + pair_column = 0 + @inbounds for p in 1:T.nExo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? S1[p] * xᵢ[q] : S1[p] * xᵢ[q] + S1[q] * xᵢ[p] + end + end + ℒ.mul!(∂𝐒ⁱ²ᵉ, λ[i], pair_top', 2, 1) + ℒ.mul!(∂𝐒ⁱ²ᵉ, S2, kronxx[i]', -1, 1) # 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol) fill!(∂state¹⁻_vol, 0) @@ -13076,7 +13225,7 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(∂state¹⁻_vol, 𝐒¹⁻ᵛ', ∂shock_independent, -1, 1) # ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, ℒ.kron(state¹⁻_vol, state¹⁻_vol), -1/2, 1) - ℒ.kron!(kronstate¹⁻_vol[i], state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol[i], state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, kronstate¹⁻_vol[i]', -1/2, 1) # ∂𝐒²⁻ᵛ -= ∂shock_independent * ℒ.kron(state¹⁻_vol, state¹⁻_vol)' / 2 @@ -13151,10 +13300,10 @@ function rrule(::typeof(calculate_loglikelihood), fill!(∂𝐒[2], 0) ∂𝐒[1][cond_var_idx,end-T.nExo+1:end] += ∂𝐒¹ᵉ - ∂𝐒[2][cond_var_idx,shockvar²_idxs] += ∂𝐒²⁻ᵉ - ∂𝐒[2][cond_var_idx,shock²_idxs] += ∂𝐒ⁱ²ᵉ / 2 + ∂𝐒²ᵉ + ∂𝐒[2][cond_var_idx,shockvar²_cols] += ∂𝐒²⁻ᵉ + ∂𝐒[2][cond_var_idx,shock²_cols] += ∂𝐒ⁱ²ᵉ / 2 + ∂𝐒²ᵉ ∂𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] += ∂𝐒¹⁻ᵛ - ∂𝐒[2][cond_var_idx,var_vol²_idxs] += ∂𝐒²⁻ᵛ + ∂𝐒[2][cond_var_idx,var_vol²_cols] += ∂𝐒²⁻ᵛ ∂𝐒[1][T.past_not_future_and_mixed_idx,:] += ∂𝐒⁻¹ ∂𝐒[2][T.past_not_future_and_mixed_idx,:] += ∂𝐒⁻² @@ -13214,26 +13363,43 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} shock³_idxs = tc.shock³_idxs shockvar³2_idxs = tc.shockvar³2_idxs shockvar³_idxs = tc.shockvar³_idxs + shock_shock_state_indices = tc.shock_shock_state_idxs + shock_shock_state_rows = tc.shock_shock_state_rows + + n_aug = n_past + 1 + n_exo + n_state_vol = n_past + 1 + n_global = n_past + 1 + n_exo + n_exo² = n_exo * (n_exo + 1) ÷ 2 + n_exo³ = n_exo * (n_exo + 1) * (n_exo + 2) ÷ 6 + shockvar_cols = cc.shockvar_cols + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols + var_vol³_cols = tc.var_vol³_cols + shock³_cols = tc.shock³_cols + shockvar³2_cols = tc.shockvar³2_cols + shockvar³_cols = tc.shockvar³_cols 𝐒⁻¹ = 𝐒[1][Tcc.past_not_future_and_mixed_idx, :] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:n_past] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_idxs]) - 𝐒²⁻ = collect(𝐒[2][cond_var_idx, var²_idxs]) - 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_idxs]) - 𝐒²⁻ᵛᵉ = collect(𝐒[2][cond_var_idx, shockvar_idxs]) - 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_idxs]) + 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_cols]) + 𝐒²⁻ = collect(𝐒[2][cond_var_idx, var²_cols]) + 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_cols]) + 𝐒²⁻ᵛᵉ = collect(𝐒[2][cond_var_idx, shockvar_cols]) + 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_cols]) 𝐒⁻² = collect(𝐒[2][Tcc.past_not_future_and_mixed_idx, :]) - 𝐒³⁻ᵛ = collect(𝐒[3][cond_var_idx, var_vol³_idxs]) - 𝐒³⁻ᵉ² = collect(𝐒[3][cond_var_idx, shockvar³2_idxs]) - 𝐒³⁻ᵉ = collect(𝐒[3][cond_var_idx, shockvar³_idxs]) - 𝐒³ᵉ = collect(𝐒[3][cond_var_idx, shock³_idxs]) + 𝐒³⁻ᵛ = collect(𝐒[3][cond_var_idx, var_vol³_cols]) + 𝐒³⁻ᵉ² = collect(𝐒[3][cond_var_idx, shockvar³2_cols]) + 𝐒³⁻ᵉ = collect(𝐒[3][cond_var_idx, shockvar³_cols]) + 𝐒³ᵉ = collect(𝐒[3][cond_var_idx, shock³_cols]) 𝐒⁻³ = collect(𝐒[3][Tcc.past_not_future_and_mixed_idx, :]) 𝐒ⁱ³ᵉ = 𝐒³ᵉ ./ 6 J = ℒ.I(n_exo) - II = sparse(ℒ.I(n_exo^2)) + II = sparse(ℒ.I(n_exo²)) state₁ = copy(state[1][Tcc.past_not_future_and_mixed_idx]) state₂ = copy(state[2][Tcc.past_not_future_and_mixed_idx]) @@ -13310,7 +13476,11 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter rrule (pruned 3rd, missing) failed during warmup") end return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) @@ -13320,8 +13490,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} warmup_aug₁̂ = zeros(size(𝐒⁻¹, 2)) warmup_aug₂ = zeros(size(𝐒⁻¹, 2)) warmup_aug₃ = zeros(size(𝐒⁻¹, 2)) - warmup_kron_aug₁ = zeros(size(𝐒⁻¹, 2)^2) - warmup_kron_kron_aug₁ = zeros(size(𝐒⁻¹, 2)^3) + warmup_n_aug = size(𝐒⁻¹, 2) + warmup_kron_aug₁ = zeros(warmup_n_aug * (warmup_n_aug + 1) ÷ 2) + warmup_kron_kron_aug₁ = zeros(warmup_n_aug * (warmup_n_aug + 1) * (warmup_n_aug + 2) ÷ 6) warmup_shocks = reshape(x_warmup, n_exo, warmup_iterations) @inbounds for w in 1:warmup_iterations-1 copyto!(warmup_aug₁, 1, state₁, 1, n_past) @@ -13340,16 +13511,16 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} warmup_aug₃[n_past + 1] = 0.0 fill!(view(warmup_aug₃, n_past + 2:length(warmup_aug₃)), 0.0) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁, warmup_aug₁) + compressed_kron²_power!(warmup_kron_aug₁, warmup_aug₁) ℒ.mul!(state₁, 𝐒⁻¹, warmup_aug₁) ℒ.mul!(state₂, 𝐒⁻¹, warmup_aug₂) ℒ.mul!(state₂, 𝐒⁻², warmup_kron_aug₁, 1/2, 1) ℒ.mul!(state₃, 𝐒⁻¹, warmup_aug₃) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁̂, warmup_aug₂) + compressed_kron²!(warmup_kron_aug₁, warmup_aug₁̂, warmup_aug₂) ℒ.mul!(state₃, 𝐒⁻², warmup_kron_aug₁, 1, 1) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁, warmup_aug₁) - ℒ.kron!(warmup_kron_kron_aug₁, warmup_kron_aug₁, warmup_aug₁) + compressed_kron²_power!(warmup_kron_aug₁, warmup_aug₁) + compressed_kron³_power!(warmup_kron_kron_aug₁, warmup_aug₁) ℒ.mul!(state₃, 𝐒⁻³, warmup_kron_kron_aug₁, 1/6, 1) end @@ -13379,11 +13550,11 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₂, -1, 1) ℒ.mul!(shock_independent, 𝐒¹⁻, state₃, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) - kron_state₁_state₂ = ℒ.kron(state₁, state₂) + kron_state₁_state₂ = compressed_kron²(state₁, state₂) ℒ.mul!(shock_independent, 𝐒²⁻, kron_state₁_state₂, -1, 1) - ℒ.kron!(kron_kron_state¹⁻_vol, kronstate¹⁻_vol, state¹⁻_vol) + compressed_kron³_power!(kron_kron_state¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kron_kron_state¹⁻_vol, -1/6, 1) # 𝐒ⁱ_full = 𝐒¹ᵉ + 𝐒²⁻ᵉ k(I,s¹v) + 𝐒²⁻ᵛᵉ k(I,s²v) + 0.5 𝐒³⁻ᵉ² k(k(I,s¹v),s¹v) @@ -13392,13 +13563,19 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(𝐒ⁱ_full, 𝐒¹ᵉ) ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵉ, kron_J_s1v, 1, 1) ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵛᵉ, kron_J_s2v, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_J_s1v, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate¹⁻_vol) ℒ.mul!(𝐒ⁱ_full, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) # 𝐒ⁱ²ᵉ_full = 𝐒²ᵉ/2 + 𝐒³⁻ᵉ k(II, s¹v) / 2 - x_kron_II!(kron_buffer4sv, state¹⁻_vol) copyto!(𝐒ⁱ²ᵉ_full, 𝐒²ᵉ); ℒ.rdiv!(𝐒ⁱ²ᵉ_full, 2) - ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) copyto!(state¹⁻_vol_seq[t], state¹⁻_vol) 𝐒ⁱ_full_seq[t] .= 𝐒ⁱ_full @@ -13425,9 +13602,11 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) end if t > presample_periods - kron_J_x = ℒ.kron(J, x) - kron_xx = ℒ.kron(x, x) - kron_J_xx = ℒ.kron(J, kron_xx) + compressed_kron²!(kb3, x, J) + kron_J_x = kb3 + kron_xx = compressed_kron²_power(x) + compressed_kron³!(kb4, x, x, J) + kron_J_xx = kb4 jac_v = 𝐒ⁱ_v + 2 * 𝐒ⁱ²ᵉ_v * kron_J_x + 3 * 𝐒ⁱ³ᵉ_v * kron_J_xx logabsdets += m == n_exo ? ℒ.logabsdet(jac_v)[1] : ℒ.logabsdet(jac_v * jac_v')[1] / 2 shocks² += sum(abs2, x) @@ -13445,13 +13624,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} copyto!(aug_state₂_seq[t], 1, state₂, 1); aug_state₂_seq[t][n_past+1] = 0.0 copyto!(aug_state₃_seq[t], 1, state₃, 1); aug_state₃_seq[t][n_past+1] = 0.0 - ℒ.kron!(kron_aug_state₁, aug_state₁_seq[t], aug_state₁_seq[t]) - ℒ.kron!(kron_kron_aug_state₁, kron_aug_state₁, aug_state₁_seq[t]) + compressed_kron²_power!(kron_aug_state₁, aug_state₁_seq[t]) + compressed_kron³_power!(kron_kron_aug_state₁, aug_state₁_seq[t]) ℒ.mul!(state₁, 𝐒⁻¹, aug_state₁_seq[t]) ℒ.mul!(state₂, 𝐒⁻¹, aug_state₂_seq[t]); ℒ.mul!(state₂, 𝐒⁻², kron_aug_state₁, 1/2, 1) ℒ.mul!(state₃, 𝐒⁻¹, aug_state₃_seq[t]) - kron_aug₁̂_aug₂ = ℒ.kron(aug_state₁̂_seq[t], aug_state₂_seq[t]) + kron_aug₁̂_aug₂ = compressed_kron²(aug_state₁̂_seq[t], aug_state₂_seq[t]) ℒ.mul!(state₃, 𝐒⁻², kron_aug₁̂_aug₂, 1, 1) ℒ.mul!(state₃, 𝐒⁻³, kron_kron_aug_state₁, 1/6, 1) @@ -13489,18 +13668,27 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂state₁_next = zeros(n_past) ∂state₂_next = zeros(n_past) ∂state₃_next = zeros(n_past) - kronaug_buf = zeros((n_past + 1 + n_exo)^2) - ∂kronaug = zeros((n_past + 1 + n_exo)^2) + n_state_pair = (n_past + 1) * (n_past + 2) ÷ 2 + n_aug_pair = (n_past + 1 + n_exo) * (n_past + 2 + n_exo) ÷ 2 + n_state_triple = (n_past + 1) * (n_past + 2) * (n_past + 3) ÷ 6 + n_aug_triple = (n_past + 1 + n_exo) * (n_past + 2 + n_exo) * (n_past + 3 + n_exo) ÷ 6 + n_exo² = n_exo * (n_exo + 1) ÷ 2 + n_exo³ = n_exo * (n_exo + 1) * (n_exo + 2) ÷ 6 + kronaug_buf = zeros(n_aug_pair) + ∂kronaug = zeros(n_aug_pair) ∂aug_state₁ = zeros(n_past + 1 + n_exo) ∂aug_state₁̂ = zeros(n_past + 1 + n_exo) ∂aug_state₂ = zeros(n_past + 1 + n_exo) ∂aug_state₃ = zeros(n_past + 1 + n_exo) - ∂kronstate = zeros((n_past + 1)^2) + ∂aug_state₂_cross = zeros(n_past + 1 + n_exo) + ∂kronstate = zeros(n_state_pair) ∂state¹⁻_vol = zeros(n_past + 1) + ∂state¹⁻_vol_pair = zeros(n_past + 1) + ∂state¹⁻_vol_cubic = zeros(n_past + 1) ∂𝐒ⁱ_full_buf = zeros(n_cond, n_exo) - ∂𝐒ⁱ²ᵉ_full_buf = zeros(n_cond, n_exo^2) + ∂𝐒ⁱ²ᵉ_full_buf = zeros(n_cond, n_exo²) ∂shock_independent = zeros(n_cond) - ∂kronaug_for3 = zeros((n_past + 1 + n_exo)^2) + ∂kronaug_for3 = zeros(n_aug_pair) ∂jac_v_buf = zeros(n_cond, n_exo) function pruned3_missing_pullback(∂llh) @@ -13513,7 +13701,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} fill!(∂state₁_next, 0); fill!(∂state₂_next, 0); fill!(∂state₃_next, 0) fill!(kronaug_buf, 0); fill!(∂kronaug, 0) fill!(∂aug_state₁, 0); fill!(∂aug_state₁̂, 0); fill!(∂aug_state₂, 0); fill!(∂aug_state₃, 0) + fill!(∂aug_state₂_cross, 0) fill!(∂kronstate, 0); fill!(∂state¹⁻_vol, 0) + fill!(∂state¹⁻_vol_pair, 0) + fill!(∂state¹⁻_vol_cubic, 0) fill!(∂𝐒ⁱ_full_buf, 0); fill!(∂𝐒ⁱ²ᵉ_full_buf, 0); fill!(∂shock_independent, 0) fill!(∂kronaug_for3, 0) fill!(∂jac_v_buf, 0) @@ -13536,18 +13727,22 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # state₃_next = 𝐒⁻¹ aug₃ + 𝐒⁻² kron(aug₁̂, aug₂) + (1/6) 𝐒⁻³ kron(kron(aug₁,aug₁), aug₁) ℒ.mul!(∂𝐒⁻¹, ∂state₃_next, aug_state₃', 1, 1) ℒ.mul!(∂aug_state₃, 𝐒⁻¹', ∂state₃_next) - kron_aug₁̂_aug₂ = ℒ.kron(aug_state₁̂, aug_state₂) + kron_aug₁̂_aug₂ = compressed_kron²(aug_state₁̂, aug_state₂) ℒ.mul!(∂𝐒⁻², ∂state₃_next, kron_aug₁̂_aug₂', 1, 1) ∂kronaug₁̂₂ = 𝐒⁻²' * ∂state₃_next fill!(∂aug_state₁̂, 0) - fill_kron_adjoint!(∂aug_state₁̂, ∂aug_state₂, ∂kronaug₁̂₂, aug_state₁̂, aug_state₂) - ℒ.kron!(kronaug_buf, aug_state₁, aug_state₁) - kron_kron_aug₁ = ℒ.kron(kronaug_buf, aug_state₁) + compressed_kron²_vjp!(∂aug_state₁̂, + ∂aug_state₂_cross, + ∂kronaug₁̂₂, + aug_state₁̂, + aug_state₂) + ∂aug_state₂ .+= ∂aug_state₂_cross + compressed_kron²_power!(kronaug_buf, aug_state₁) + kron_kron_aug₁ = compressed_kron³_power(aug_state₁) ℒ.mul!(∂𝐒⁻³, ∂state₃_next, kron_kron_aug₁', 1/6, 1) ∂kronkronaug₁ = (𝐒⁻³' * ∂state₃_next) ./ 6 fill!(∂aug_state₁, 0) - fill!(∂kronaug_for3, 0) - fill_kron_adjoint!(∂aug_state₁, ∂kronaug_for3, ∂kronkronaug₁, aug_state₁, kronaug_buf) + compressed_kron³_power_vjp!(∂aug_state₁, ∂kronkronaug₁, aug_state₁) # state₂_next = 𝐒⁻¹ aug₂ + 0.5 𝐒⁻² kron(aug₁, aug₁) ℒ.mul!(∂𝐒⁻¹, ∂state₂_next, aug_state₂', 1, 1) @@ -13589,15 +13784,15 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # shocks² and logabsdet contributions (only if t > presample and m > 0) ∂jac_v = view(∂jac_v_buf, 1:m, :); fill!(∂jac_v, 0) jac_v_local = zeros(m, n_exo) - 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo^2) - 𝐒ⁱ³ᵉ_v_local = zeros(m, n_exo^3) + 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo²) + 𝐒ⁱ³ᵉ_v_local = zeros(m, n_exo³) if m > 0 𝐒ⁱ_v_local = 𝐒ⁱ_full_seq[t][idx, :] 𝐒ⁱ²ᵉ_v_local = 𝐒ⁱ²ᵉ_full_seq[t][idx, :] 𝐒ⁱ³ᵉ_v_local = 𝐒ⁱ³ᵉ[idx, :] - kron_J_x_local = ℒ.kron(J, x) - kron_xx_local = ℒ.kron(x, x) - kron_J_xx_local = ℒ.kron(J, kron_xx_local) + kron_J_x_local = compressed_kron²(x, J) + kron_xx_local = compressed_kron²_power(x) + kron_J_xx_local = compressed_kron³(x, x, J) jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * kron_J_x_local + 3 * 𝐒ⁱ³ᵉ_v_local * kron_J_xx_local end if m > 0 && t > presample_periods @@ -13611,31 +13806,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} G = inv(jac_v_local * jac_v_local') ∂jac_v .+= (-1.0) .* (G * jac_v_local) end - # Indirect channel: ∂jac_v → ∂x via the (J⊗x) and (J⊗ kron(x,x)) terms in jac_v. - # d jac_v[i,r]/dx_l = 2 𝐒ⁱ²ᵉ_v[i,(r-1)n+l] + 3 (Σ_q 𝐒ⁱ³ᵉ_v[i,(r-1)n²+(l-1)n+q] x_q + Σ_p 𝐒ⁱ³ᵉ_v[i,(r-1)n²+(p-1)n+l] x_p) - @inbounds for l in 1:n_exo - s = 0.0 - for r in 1:n_exo - # 2nd order channel - for i_local in 1:m - s += 2 * ∂jac_v[i_local, r] * 𝐒ⁱ²ᵉ_v_local[i_local, (r-1)*n_exo + l] - end - # 3rd order channel — symmetric in p,q so two terms - for q in 1:n_exo - col = (r-1)*n_exo^2 + (l-1)*n_exo + q - for i_local in 1:m - s += 3 * ∂jac_v[i_local, r] * 𝐒ⁱ³ᵉ_v_local[i_local, col] * x[q] - end - end - for p in 1:n_exo - col = (r-1)*n_exo^2 + (p-1)*n_exo + l - for i_local in 1:m - s += 3 * ∂jac_v[i_local, r] * 𝐒ⁱ³ᵉ_v_local[i_local, col] * x[p] - end - end - end - ∂x[l] += s - end + # Differentiate the compressed Jacobian kernels directly. + accumulate_sym_kron_jacobian_pullback!(∂x, 2 .* (𝐒ⁱ²ᵉ_v_local' * ∂jac_v), x) + compressed_kron³_identity_vjp!(∂x, 3 .* (𝐒ⁱ³ᵉ_v_local' * ∂jac_v), x) end fill!(∂shock_independent, 0) @@ -13654,10 +13827,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} λ = 2 * (Gloc * (jac_v * x)) end - # KKT topL = 2I - 2 M, M = reshape((𝐒ⁱ²ᵉ_v + 3 𝐒ⁱ³ᵉ_v kron(II, x))' λ, n, n) - kron_II_x = ℒ.kron(II, x) - M = reshape((𝐒ⁱ²ᵉ_v + 3 * 𝐒ⁱ³ᵉ_v * kron_II_x)' * λ, n_exo, n_exo) - topL = 2 * ℒ.I(n_exo) - 2 * M + # KKT top block from the compressed quadratic/cubic Hessians. + M = zeros(n_exo, n_exo) + compressed_pair_hessian!(M, 𝐒ⁱ²ᵉ_v' * λ) + cubic_hessian = zeros(n_exo, n_exo) + compressed_triple_hessian!(cubic_hessian, 𝐒ⁱ³ᵉ_v' * λ, x) + M .+= 3 .* cubic_hessian + topL = 2 * Matrix(ℒ.I(n_exo)) - 2 * M fXλp = [topL -jac_v' jac_v zeros(m, m)] @@ -13669,28 +13845,34 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂v_v = Sλ ∂𝐒ⁱ_v = λ * Sx' - Sλ * x' - # ∂𝐒ⁱ²ᵉ_v_kkt: same structure as 2nd order - xSx = x * Sx' - xx_outer = x * x' - ∂𝐒ⁱ²ᵉ_v_top = 2 * λ * vec(xSx)' - ∂𝐒ⁱ²ᵉ_v_F = -Sλ * vec(xx_outer)' - ∂𝐒ⁱ²ᵉ_v_kkt = ∂𝐒ⁱ²ᵉ_v_top + ∂𝐒ⁱ²ᵉ_v_F + # Pair and triple KKT cotangents in compressed coordinates. + pair_xx = compressed_kron²_power(x) + pair_top = Vector{Float64}(undef, n_exo²) + pair_column = 0 + @inbounds for p in 1:n_exo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? Sx[p] * x[q] : Sx[p] * x[q] + Sx[q] * x[p] + end + end + # Two rank-1 updates into one matrix, rather than building the + # two terms separately and adding them. + ∂𝐒ⁱ²ᵉ_v_kkt = zeros(m, n_exo²) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, λ, pair_top', 2, 0) + ℒ.mul!(∂𝐒ⁱ²ᵉ_v_kkt, Sλ, pair_xx', -1, 1) # ∂𝐒ⁱ³ᵉ_v_kkt: - # Top: [i, (r-1)n²+(p-1)n+q] = 3 λ[i] Sx[r] x_p x_q → 3 λ * vec(Sx_outer_with_xx)' - # with kron(Sx, kron(x,x))[(r-1)n²+(p-1)n+q] = Sx[r] x_p x_q - # F : [i, k] = -Sλ[i] kron(x, kron(x,x))[k] (which has entry x_r x_p x_q with index (r-1)n²+(p-1)n+q) - kron_Sx_xx = ℒ.kron(Sx, kron_xx_local) # length n³ - kron_x_xx = ℒ.kron(x, kron_xx_local) # length n³ (= kron(x,x,x)) - ∂𝐒ⁱ³ᵉ_v_top = 3 * λ * kron_Sx_xx' - ∂𝐒ⁱ³ᵉ_v_F = -Sλ * kron_x_xx' - ∂𝐒ⁱ³ᵉ_v_kkt = ∂𝐒ⁱ³ᵉ_v_top + ∂𝐒ⁱ³ᵉ_v_F + triple_xx = compressed_kron³(x, x, Sx) + triple_xxx = compressed_kron³_power(x) + ∂𝐒ⁱ³ᵉ_v_kkt = zeros(m, n_exo³) + ℒ.mul!(∂𝐒ⁱ³ᵉ_v_kkt, λ, triple_xx', 3, 0) + ℒ.mul!(∂𝐒ⁱ³ᵉ_v_kkt, Sλ, triple_xxx', -1, 1) # Add direct ∂jac_v contributions for periods past presample if t > presample_periods ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v + ∂jac_v - ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * ℒ.kron(J, x)' - ∂𝐒ⁱ³ᵉ_v_total = ∂𝐒ⁱ³ᵉ_v_kkt + 3 * ∂jac_v * ℒ.kron(J, kron_xx_local)' + ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * kron_J_x_local' + ∂𝐒ⁱ³ᵉ_v_total = ∂𝐒ⁱ³ᵉ_v_kkt + 3 * ∂jac_v * kron_J_xx_local' else ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt @@ -13707,12 +13889,12 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ_full[idx[i_local], j] = ∂𝐒ⁱ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^2 + @inbounds for j in 1:n_exo² for i_local in 1:m ∂𝐒ⁱ²ᵉ_full[idx[i_local], j] = ∂𝐒ⁱ²ᵉ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^3 + @inbounds for j in 1:n_exo³ for i_local in 1:m ∂𝐒ⁱ³ᵉ[idx[i_local], j] += ∂𝐒ⁱ³ᵉ_v_total[i_local, j] end @@ -13730,8 +13912,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} kron_J_s2v = ℒ.kron(J, state²⁻_vol_local) ℒ.mul!(∂𝐒²⁻ᵉ, ∂𝐒ⁱ_full, kron_J_s1v', 1, 1) ℒ.mul!(∂𝐒²⁻ᵛᵉ, ∂𝐒ⁱ_full, kron_J_s2v', 1, 1) - kron_kron_J_s1v_s1v = ℒ.kron(kron_J_s1v, state¹⁻_vol) - ℒ.mul!(∂𝐒³⁻ᵉ², ∂𝐒ⁱ_full, kron_kron_J_s1v_s1v', 1/2, 1) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate¹⁻_vol) + ℒ.mul!(∂𝐒³⁻ᵉ², ∂𝐒ⁱ_full, kron_buffer3sv', 1/2, 1) # Propagate to ∂state¹⁻_vol via (I⊗s¹v) and via (k(I,s¹v),s¹v) ∂kronIs1v_a = 𝐒²⁻ᵉ' * ∂𝐒ⁱ_full # n_exo*(n_past+1) × n_exo @@ -13743,28 +13926,14 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end ∂state¹⁻_vol[p] += s end - # 𝐒³⁻ᵉ² contribution: (1/2) 𝐒³⁻ᵉ² k(k(I,s¹v), s¹v). - # Use full kron-adjoint: ∂(k(k(I,s¹v),s¹v)) = 0.5 𝐒³⁻ᵉ²' ∂𝐒ⁱ_full (a (n_exo·(n_past+1)²) × n_exo matrix). - # Decompose: u := k(I, s¹v) (shape n_exo·(n_past+1) × n_exo), then kron(u, s¹v). + # 𝐒³⁻ᵉ² contribution uses the compressed state-pair block + # for each shock column; differentiate each block analytically. ∂kron_u_s1v = (𝐒³⁻ᵉ²' * ∂𝐒ⁱ_full) ./ 2 - # Apply Kronecker adjoint: ∂u = sum_{q,j} ∂kron_u_s1v[(q-1)·(n_past+1)+r, j]·s1v[r] etc. - u_mat = ℒ.kron(J, state¹⁻_vol) # (n_exo·(n_past+1)) × n_exo - ∂u_mat = zeros(size(u_mat)) + ∂state_pair = zeros(n_past + 1) @inbounds for j in 1:n_exo - for q in 1:(n_exo*(n_past+1)) - for r in 1:(n_past+1) - ∂u_mat[q, j] += ∂kron_u_s1v[(q-1)*(n_past+1) + r, j] * state¹⁻_vol[r] - ∂state¹⁻_vol[r] += ∂kron_u_s1v[(q-1)*(n_past+1) + r, j] * u_mat[q, j] - end - end - end - # Now propagate ∂u_mat through u_mat = kron(J, s¹v) (J fixed I_n) - @inbounds for p in 1:(n_past + 1) - s = 0.0 - for j in 1:n_exo - s += ∂u_mat[(j-1)*(n_past+1) + p, j] - end - ∂state¹⁻_vol[p] += s + rows = (j - 1) * n_state_pair + 1:j * n_state_pair + compressed_kron²_power_vjp!(∂state_pair, view(∂kron_u_s1v, rows, j), state¹⁻_vol) + ∂state¹⁻_vol .+= ∂state_pair end # Propagate to ∂state²⁻_vol via (I⊗s²v) and then to ∂state₂_now (since state²⁻_vol = vcat(state₂, 0)) @@ -13791,7 +13960,7 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # kron(II, s¹v)[(j-1)(n_past+1)+p, k] = II[j,k] * s¹v[p] = δ_{jk} s¹v[p] @inbounds for p in 1:(n_past + 1) s = 0.0 - for j in 1:n_exo^2 + for j in 1:n_exo² s += ∂kronIIs1v[(j-1)*(n_past+1) + p, j] end ∂state¹⁻_vol[p] += s @@ -13818,13 +13987,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂state₃_next[j] += -∂stm3_contrib[j] end # 0.5 𝐒²⁻ᵛ k(s¹v, s¹v) - kron_sv = ℒ.kron(state¹⁻_vol, state¹⁻_vol) + kron_sv = compressed_kron²_power(state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, kron_sv', -1/2, 1) ∂kron_sv = -(𝐒²⁻ᵛ' * ∂shock_independent) ./ 2 fill!(∂kronstate, 0) ∂kronstate .+= ∂kron_sv # 𝐒²⁻ k(stm1, stm2) - kron_s1_s2 = ℒ.kron(stm1, stm2) + kron_s1_s2 = compressed_kron²(stm1, stm2) ℒ.mul!(∂𝐒²⁻, ∂shock_independent, kron_s1_s2', -1, 1) ∂kron_s1_s2 = -(𝐒²⁻' * ∂shock_independent) ∂stm1_s12 = zeros(n_past) @@ -13835,17 +14004,18 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂state₂_next[j] += ∂stm2_s12[j] end # (1/6) 𝐒³⁻ᵛ k(s¹v, k(s¹v, s¹v)) - kron_s1v_3 = ℒ.kron(state¹⁻_vol, kron_sv) + kron_s1v_3 = compressed_kron³_power(state¹⁻_vol) ℒ.mul!(∂𝐒³⁻ᵛ, ∂shock_independent, kron_s1v_3', -1/6, 1) ∂kron_s1v_3 = -(𝐒³⁻ᵛ' * ∂shock_independent) ./ 6 # Decompose kron(s¹v, kron(s¹v, s¹v)): use chain - first kron(a, b) with a=s¹v, b=kron(s¹v, s¹v) - ∂a_outer = zeros(n_past + 1) - ∂b_outer = zeros((n_past + 1)^2) - fill_kron_adjoint!(∂a_outer, ∂b_outer, ∂kron_s1v_3, state¹⁻_vol, kron_sv) - ∂state¹⁻_vol .+= ∂a_outer - ∂kronstate .+= ∂b_outer - # Now ∂kron_sv = ∂kronstate (both contributions accumulated) - fill_kron_adjoint!(∂state¹⁻_vol, ∂state¹⁻_vol, ∂kronstate, state¹⁻_vol, state¹⁻_vol) + compressed_kron³_power_vjp!(∂state¹⁻_vol_cubic, + ∂kron_s1v_3, + state¹⁻_vol) + compressed_kron²_power_vjp!(∂state¹⁻_vol_pair, + ∂kronstate, + state¹⁻_vol) + ∂state¹⁻_vol .+= ∂state¹⁻_vol_cubic + ∂state¹⁻_vol .+= ∂state¹⁻_vol_pair # state¹⁻_vol = vcat(state₁, 1) → ∂state₁_next += ∂state¹⁻_vol[1:n_past] @inbounds for j in 1:n_past @@ -13932,6 +14102,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} cc.I_aug, cc.I_state_vol, cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows, ) ∂𝐒¹⁻ᵛ[warmup_idx0, :] .+= ∂𝐒¹⁻ᵛ_w @@ -13955,16 +14129,16 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒_1[cond_var_idx, 1:n_past+1] .+= ∂𝐒¹⁻ᵛ ∂𝐒_1[cond_var_idx, 1:n_past] .+= ∂𝐒¹⁻ ∂𝐒_1[cond_var_idx, end-n_exo+1:end] .+= ∂𝐒¹ᵉ - ∂𝐒_2[cond_var_idx, var_vol²_idxs] .+= ∂𝐒²⁻ᵛ - ∂𝐒_2[cond_var_idx, var²_idxs] .+= ∂𝐒²⁻ - ∂𝐒_2[cond_var_idx, shockvar²_idxs] .+= ∂𝐒²⁻ᵉ - ∂𝐒_2[cond_var_idx, shockvar_idxs] .+= ∂𝐒²⁻ᵛᵉ - ∂𝐒_2[cond_var_idx, shock²_idxs] .+= ∂𝐒²ᵉ - ∂𝐒_3[cond_var_idx, var_vol³_idxs] .+= ∂𝐒³⁻ᵛ - ∂𝐒_3[cond_var_idx, shockvar³2_idxs] .+= ∂𝐒³⁻ᵉ² - ∂𝐒_3[cond_var_idx, shockvar³_idxs] .+= ∂𝐒³⁻ᵉ + ∂𝐒_2[cond_var_idx, var_vol²_cols] .+= ∂𝐒²⁻ᵛ + ∂𝐒_2[cond_var_idx, var²_cols] .+= ∂𝐒²⁻ + ∂𝐒_2[cond_var_idx, shockvar²_cols] .+= ∂𝐒²⁻ᵉ + ∂𝐒_2[cond_var_idx, shockvar_cols] .+= ∂𝐒²⁻ᵛᵉ + ∂𝐒_2[cond_var_idx, shock²_cols] .+= ∂𝐒²ᵉ + ∂𝐒_3[cond_var_idx, var_vol³_cols] .+= ∂𝐒³⁻ᵛ + ∂𝐒_3[cond_var_idx, shockvar³2_cols] .+= ∂𝐒³⁻ᵉ² + ∂𝐒_3[cond_var_idx, shockvar³_cols] .+= ∂𝐒³⁻ᵉ # 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 → ∂𝐒³ᵉ = ∂𝐒ⁱ³ᵉ / 6 - ∂𝐒_3[cond_var_idx, shock³_idxs] .+= ∂𝐒ⁱ³ᵉ ./ 6 + ∂𝐒_3[cond_var_idx, shock³_cols] .+= ∂𝐒ⁱ³ᵉ ./ 6 ℒ.rmul!(∂𝐒_1, ∂llh) ℒ.rmul!(∂𝐒_2, ∂llh) @@ -14036,17 +14210,29 @@ function rrule(::typeof(calculate_loglikelihood), shockvar3_idxs = tc.shockvar3_idxs shockvar³2_idxs = tc.shockvar³2_idxs shockvar³_idxs = tc.shockvar³_idxs + n_global = T.nPast_not_future_and_mixed + 1 + T.nExo + n_exo² = T.nExo * (T.nExo + 1) ÷ 2 + n_exo³ = T.nExo * (T.nExo + 1) * (T.nExo + 2) ÷ 6 + shockvar_cols = cc.shockvar_cols + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var²_cols = cc.var²_cols + var_vol³_cols = tc.var_vol³_cols + shock³_cols = tc.shock³_cols + shockvar³2_cols = tc.shockvar³2_cols + shockvar³_cols = tc.shockvar³_cols 𝐒⁻¹ = 𝐒[1][T.past_not_future_and_mixed_idx,:] 𝐒¹⁻ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx,end-T.nExo+1:end] - 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_idxs] - 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_idxs] - 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_idxs] - 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,shockvar_idxs] - 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_idxs] + 𝐒²⁻ᵛ = 𝐒[2][cond_var_idx,var_vol²_cols] + 𝐒²⁻ = 𝐒[2][cond_var_idx,var²_cols] + 𝐒²⁻ᵉ = 𝐒[2][cond_var_idx,shockvar²_cols] + 𝐒²⁻ᵛᵉ = 𝐒[2][cond_var_idx,shockvar_cols] + 𝐒²ᵉ = 𝐒[2][cond_var_idx,shock²_cols] 𝐒⁻² = 𝐒[2][T.past_not_future_and_mixed_idx,:] 𝐒²⁻ᵛ = nnz(𝐒²⁻ᵛ) / length(𝐒²⁻ᵛ) > .1 ? collect(𝐒²⁻ᵛ) : 𝐒²⁻ᵛ @@ -14056,10 +14242,10 @@ function rrule(::typeof(calculate_loglikelihood), 𝐒²ᵉ = nnz(𝐒²ᵉ) / length(𝐒²ᵉ) > .1 ? collect(𝐒²ᵉ) : 𝐒²ᵉ 𝐒⁻² = nnz(𝐒⁻²) / length(𝐒⁻²) > .1 ? collect(𝐒⁻²) : 𝐒⁻² - 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_idxs] - 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_idxs] - 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_idxs] - 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_idxs] + 𝐒³⁻ᵛ = 𝐒[3][cond_var_idx,var_vol³_cols] + 𝐒³⁻ᵉ² = 𝐒[3][cond_var_idx,shockvar³2_cols] + 𝐒³⁻ᵉ = 𝐒[3][cond_var_idx,shockvar³_cols] + 𝐒³ᵉ = 𝐒[3][cond_var_idx,shock³_cols] 𝐒⁻³ = 𝐒[3][T.past_not_future_and_mixed_idx,:] 𝐒³⁻ᵛ = nnz(𝐒³⁻ᵛ) / length(𝐒³⁻ᵛ) > .1 ? collect(𝐒³⁻ᵛ) : 𝐒³⁻ᵛ @@ -14071,23 +14257,25 @@ function rrule(::typeof(calculate_loglikelihood), state₂ = state[2][T.past_not_future_and_mixed_idx] state₃ = state[3][T.past_not_future_and_mixed_idx] - kronxx = [zeros(T.nExo^2) for _ in 1:size(data_in_deviations,2)] + kronxx = [zeros(n_exo²) for _ in 1:size(data_in_deviations,2)] J = ℒ.I(T.nExo) - kronxxx = [zeros(T.nExo^3) for _ in 1:size(data_in_deviations,2)] + kronxxx = [zeros(n_exo³) for _ in 1:size(data_in_deviations,2)] - kron_buffer2 = ℒ.kron(J, zeros(T.nExo)) + kron_buffer2 = zeros(n_exo², T.nExo) - kron_buffer3 = ℒ.kron(J, zeros(T.nExo^2)) + kron_buffer3 = zeros(n_exo³, T.nExo) - kron_buffer4 = ℒ.kron(ℒ.kron(J, J), zeros(T.nExo)) + kron_buffer4 = zeros(n_exo³, n_exo²) + kron_buffer3sv = ws.kron_buffer3sv x = [zeros(T.nExo) for _ in 1:size(data_in_deviations,2)] state¹⁻ = state₁ state¹⁻_vol = vcat(state¹⁻, 1) + kronstate¹⁻_vol = ws.kronstate_vol state²⁻ = state₂#[T.past_not_future_and_mixed_idx] @@ -14102,9 +14290,11 @@ function rrule(::typeof(calculate_loglikelihood), aug_state₂ = [zeros(size(𝐒⁻¹,2)) for _ in 1:size(data_in_deviations,2)] aug_state₃ = [zeros(size(𝐒⁻¹,2)) for _ in 1:size(data_in_deviations,2)] - kron_aug_state₁ = [zeros(size(𝐒⁻¹,2)^2) for _ in 1:size(data_in_deviations,2)] + n_aug = size(𝐒⁻¹, 2) + kron_aug_state₁ = [zeros(n_aug * (n_aug + 1) ÷ 2) for _ in 1:size(data_in_deviations,2)] - jacc_tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ[1] * ℒ.kron(ℒ.I(T.nExo), x[1]) + compressed_kron²!(kron_buffer2, x[1], J) + jacc_tmp = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ[1] * kron_buffer2 jacc = [zero(jacc_tmp) for _ in 1:size(data_in_deviations,2)] @@ -14112,22 +14302,18 @@ function rrule(::typeof(calculate_loglikelihood), λ[1] = jacc_tmp' \ x[1] * 2 - fXλp_tmp = [reshape(2 * 𝐒ⁱ²ᵉ[1]' * λ[1], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - 2 * ℒ.I(size(𝐒ⁱ, 2)) jacc_tmp' + top_tmp = zeros(T.nExo, T.nExo) + compressed_pair_hessian!(top_tmp, 2 * (𝐒ⁱ²ᵉ[1]' * λ[1])) + top_tmp .-= 2 .* Matrix(ℒ.I(T.nExo)) + fXλp_tmp = [top_tmp jacc_tmp' -jacc_tmp zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] fXλp = [zero(fXλp_tmp) for _ in 1:size(data_in_deviations,2)] - kronxλ_tmp = ℒ.kron(x[1], λ[1]) - - kronxλ = [kronxλ_tmp for _ in 1:size(data_in_deviations,2)] - - kronxxλ_tmp = ℒ.kron(x[1], kronxλ_tmp) - - kronxxλ = [kronxxλ_tmp for _ in 1:size(data_in_deviations,2)] - II = sparse(ℒ.I(T.nExo^2)) + II = sparse(ℒ.I(n_exo²)) - lI = 2 * ℒ.I(size(𝐒ⁱ, 2)) + lI = 2 * Matrix(ℒ.I(T.nExo)) 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 @@ -14161,7 +14347,11 @@ function rrule(::typeof(calculate_loglikelihood), ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter rrule (pruned 3rd) failed during warmup") end @@ -14172,8 +14362,9 @@ function rrule(::typeof(calculate_loglikelihood), warmup_aug₁̂ = zeros(size(𝐒⁻¹, 2)) warmup_aug₂ = zeros(size(𝐒⁻¹, 2)) warmup_aug₃ = zeros(size(𝐒⁻¹, 2)) - warmup_kron_aug₁ = zeros(size(𝐒⁻¹, 2)^2) - warmup_kron_kron_aug₁ = zeros(size(𝐒⁻¹, 2)^3) + warmup_n_aug = size(𝐒⁻¹, 2) + warmup_kron_aug₁ = zeros(warmup_n_aug * (warmup_n_aug + 1) ÷ 2) + warmup_kron_kron_aug₁ = zeros(warmup_n_aug * (warmup_n_aug + 1) * (warmup_n_aug + 2) ÷ 6) warmup_shocks = reshape(x_warmup, T.nExo, warmup_iterations) @inbounds for w in 1:warmup_iterations-1 copyto!(warmup_aug₁, 1, state₁, 1, T.nPast_not_future_and_mixed) @@ -14192,16 +14383,16 @@ function rrule(::typeof(calculate_loglikelihood), warmup_aug₃[T.nPast_not_future_and_mixed + 1] = 0.0 fill!(view(warmup_aug₃, T.nPast_not_future_and_mixed + 2:length(warmup_aug₃)), 0.0) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁, warmup_aug₁) + compressed_kron²_power!(warmup_kron_aug₁, warmup_aug₁) ℒ.mul!(state₁, 𝐒⁻¹, warmup_aug₁) ℒ.mul!(state₂, 𝐒⁻¹, warmup_aug₂) ℒ.mul!(state₂, 𝐒⁻², warmup_kron_aug₁, 1/2, 1) ℒ.mul!(state₃, 𝐒⁻¹, warmup_aug₃) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁̂, warmup_aug₂) - ℒ.mul!(state₃, 𝐒⁻², warmup_kron_aug₁, 1, 1) - ℒ.kron!(warmup_kron_aug₁, warmup_aug₁, warmup_aug₁) - ℒ.kron!(warmup_kron_kron_aug₁, warmup_kron_aug₁, warmup_aug₁) + compressed_kron²!(warmup_kron_aug₁, warmup_aug₁̂, warmup_aug₂) + ℒ.mul!(state₃, 𝐒⁻², warmup_kron_aug₁, 1, 1) + compressed_kron²_power!(warmup_kron_aug₁, warmup_aug₁) + compressed_kron³_power!(warmup_kron_kron_aug₁, warmup_aug₁) ℒ.mul!(state₃, 𝐒⁻³, warmup_kron_kron_aug₁, 1/6, 1) end @@ -14230,13 +14421,15 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(shock_independent, 𝐒¹⁻, state³⁻, -1, 1) - ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, ℒ.kron(state¹⁻_vol, state¹⁻_vol), -1/2, 1) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) + ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) - ℒ.mul!(shock_independent, 𝐒²⁻, ℒ.kron(state¹⁻, state²⁻), -1, 1) + ℒ.mul!(shock_independent, 𝐒²⁻, compressed_kron²(state¹⁻, state²⁻), -1, 1) - ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, ℒ.kron(state¹⁻_vol, ℒ.kron(state¹⁻_vol, state¹⁻_vol)), -1/6, 1) + ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, compressed_kron³_power(state¹⁻_vol), -1/6, 1) - 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol) + 𝐒²⁻ᵛᵉ * ℒ.kron(ℒ.I(T.nExo), state²⁻_vol) + 𝐒³⁻ᵉ² * ℒ.kron(ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol), state¹⁻_vol) / 2 + ℒ.kron!(kron_buffer3sv, J, kronstate¹⁻_vol) + 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(J, state¹⁻_vol) + 𝐒²⁻ᵛᵉ * ℒ.kron(J, state²⁻_vol) + 𝐒³⁻ᵉ² * kron_buffer3sv / 2 𝐒ⁱ²ᵉ[i] = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * ℒ.kron(II, state¹⁻_vol) / 2 @@ -14266,21 +14459,25 @@ function rrule(::typeof(calculate_loglikelihood), return on_failure_loglikelihood, x -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) end - jacc[i] = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ[i] * ℒ.kron(ℒ.I(T.nExo), x[i]) + 3 * 𝐒ⁱ³ᵉ * ℒ.kron(ℒ.I(T.nExo), kronxx[i]) + compressed_kron²!(kron_buffer2, x[i], J) + compressed_kron³!(kron_buffer3, x[i], x[i], J) + jacc[i] = 𝐒ⁱ + 2 * 𝐒ⁱ²ᵉ[i] * kron_buffer2 + 3 * 𝐒ⁱ³ᵉ * kron_buffer3 λ[i] = jacc[i]' \ x[i] * 2 # ℒ.ldiv!(λ[i], tmp', x[i]) # ℒ.rmul!(λ[i], 2) - fXλp[i] = [reshape((2 * 𝐒ⁱ²ᵉ[i] + 6 * 𝐒ⁱ³ᵉ * ℒ.kron(II, x[i]))' * λ[i], size(𝐒ⁱ, 2), size(𝐒ⁱ, 2)) - lI jacc[i]' + top_i = zeros(T.nExo, T.nExo) + compressed_pair_hessian!(top_i, 2 * (𝐒ⁱ²ᵉ[i]' * λ[i])) + cubic_i = zeros(T.nExo, T.nExo) + compressed_triple_hessian!(cubic_i, 𝐒ⁱ³ᵉ' * λ[i], x[i]) + top_i .+= 6 .* cubic_i + top_i .-= lI + fXλp[i] = [top_i jacc[i]' -jacc[i] zeros(size(𝐒ⁱ, 1),size(𝐒ⁱ, 1))] - ℒ.kron!(kronxx[i], x[i], x[i]) - - ℒ.kron!(kronxλ[i], x[i], λ[i]) - - ℒ.kron!(kronxxλ[i], x[i], kronxλ[i]) + compressed_kron²_power!(kronxx[i], x[i]) - ℒ.kron!(kronxxx[i], x[i], kronxx[i]) + compressed_kron³_power!(kronxxx[i], x[i]) if i > presample_periods # due to change of variables: jacobian determinant adjustment @@ -14302,9 +14499,9 @@ function rrule(::typeof(calculate_loglikelihood), aug_state₂[i] = [state₂; 0; zeros(T.nExo)] aug_state₃[i] = [state₃; 0; zeros(T.nExo)] - kron_aug_state₁[i] = ℒ.kron(aug_state₁[i], aug_state₁[i]) + compressed_kron²_power!(kron_aug_state₁[i], aug_state₁[i]) - state₁, state₂, state₃ = [𝐒⁻¹ * aug_state₁[i], 𝐒⁻¹ * aug_state₂[i] + 𝐒⁻² * kron_aug_state₁[i] / 2, 𝐒⁻¹ * aug_state₃[i] + 𝐒⁻² * ℒ.kron(aug_state₁̂[i], aug_state₂[i]) + 𝐒⁻³ * ℒ.kron(kron_aug_state₁[i], aug_state₁[i]) / 6] + state₁, state₂, state₃ = [𝐒⁻¹ * aug_state₁[i], 𝐒⁻¹ * aug_state₂[i] + 𝐒⁻² * kron_aug_state₁[i] / 2, 𝐒⁻¹ * aug_state₃[i] + 𝐒⁻² * compressed_kron²(aug_state₁̂[i], aug_state₂[i]) + 𝐒⁻³ * compressed_kron³_power(aug_state₁[i]) / 6] end # end # timeit_debug @@ -14351,6 +14548,9 @@ function rrule(::typeof(calculate_loglikelihood), ∂𝐒⁻³ = zero(𝐒⁻³) ∂aug_state₁̂ = zero(aug_state₁̂[1]) + ∂aug_state₂_cross = zeros(size(∂aug_state₁̂)) + ∂aug_state₁_cubic = zeros(size(∂aug_state₁̂)) + ∂aug_state₁_pair = zeros(size(∂aug_state₁̂)) ∂state¹⁻_vol = zero(state¹⁻_vol) @@ -14358,7 +14558,10 @@ function rrule(::typeof(calculate_loglikelihood), ∂kronxx = zero(kronxx[1]) - ∂kronstate¹⁻_vol = zeros(length(state¹⁻_vol)^2) + n_state_vol = length(state¹⁻_vol) + n_state_pair = n_state_vol * (n_state_vol + 1) ÷ 2 + ∂kronstate¹⁻_vol = zeros(n_state_pair) + kronstate¹⁻_vol_buf = zeros(n_state_pair) ∂state = [zeros(T.nPast_not_future_and_mixed), zeros(T.nPast_not_future_and_mixed), zeros(T.nPast_not_future_and_mixed)] @@ -14368,15 +14571,11 @@ function rrule(::typeof(calculate_loglikelihood), S_buf = zeros(T.nExo + size(jacc[1], 1)) kronSλ = zeros(length(cond_var_idx) * T.nExo) kronxS = zeros(T.nExo * length(cond_var_idx)) - kron_S1_kxλ = zeros(T.nExo * length(kronxλ[1])) - kron_xx_S2 = zeros(length(kronxx[1]) * size(jacc[1], 1)) - kron_S1_kxxλ = zeros(T.nExo * length(kronxxλ[1])) - kron_xxx_S2 = zeros(length(kronxxx[1]) * size(jacc[1], 1)) - kron_xλ = zero(kronxλ[1]) - kron_xxλ = zero(kronxxλ[1]) - kron_Ix = zero(ℒ.kron(ℒ.I(T.nExo), x[1])) - kron_Ixx = zero(ℒ.kron(ℒ.I(T.nExo), kronxx[1])) + kron_Ix = zeros(n_exo², T.nExo) + kron_Ixx = zeros(n_exo³, T.nExo) ∂𝐒ⁱ²ᵉ_tmp = zero(𝐒ⁱ²ᵉ[1]) + ∂state_pair_left = zeros(T.nPast_not_future_and_mixed) + ∂state_pair_right = zeros(T.nPast_not_future_and_mixed) function inversion_filter_loglikelihood_pullback(∂llh) # @timeit_debug timer "Inversion filter - pullback" begin @@ -14401,6 +14600,8 @@ function rrule(::typeof(calculate_loglikelihood), fill!(∂𝐒⁻³, 0) fill!(∂aug_state₁̂, 0) + fill!(∂aug_state₂_cross, 0) + fill!(∂aug_state₁_cubic, 0) fill!(∂state¹⁻_vol, 0) fill!(∂x, 0) fill!(∂kronxx, 0) @@ -14410,6 +14611,9 @@ function rrule(::typeof(calculate_loglikelihood), fill!(∂state[3], 0) # @timeit_debug timer "Loop" begin + # Scratch for the compressed pair-basis KKT cotangent inside the loop. + pair_top = zeros(T.nExo * (T.nExo + 1) ÷ 2) + for i in reverse(axes(data_in_deviations,2)) # state₁ = 𝐒⁻¹ * aug_state₁[i] ∂𝐒⁻¹ += ∂state[1] * aug_state₁[i]' @@ -14430,22 +14634,31 @@ function rrule(::typeof(calculate_loglikelihood), ∂aug_state₃ = 𝐒⁻¹' * ∂state[3] - ∂𝐒⁻² += ∂state[3] * ℒ.kron(aug_state₁̂[i], aug_state₂[i])' + ∂𝐒⁻² += ∂state[3] * compressed_kron²(aug_state₁̂[i], aug_state₂[i])' ∂aug_state₁̂ *= 0 ∂kronaug_state₁̂₂ = 𝐒⁻²' * ∂state[3] - fill_kron_adjoint!(∂aug_state₁̂, ∂aug_state₂, ∂kronaug_state₁̂₂, aug_state₁̂[i], aug_state₂[i]) + compressed_kron²_vjp!(∂aug_state₁̂, + ∂aug_state₂_cross, + ∂kronaug_state₁̂₂, + aug_state₁̂[i], + aug_state₂[i]) + ∂aug_state₂ .+= ∂aug_state₂_cross - ∂𝐒⁻³ += ∂state[3] * ℒ.kron(kron_aug_state₁[i],aug_state₁[i])' / 6 + ∂𝐒⁻³ += ∂state[3] * compressed_kron³_power(aug_state₁[i])' / 6 ∂kronkronaug_state₁ = 𝐒⁻³' * ∂state[3] / 6 - fill_kron_adjoint!(∂aug_state₁, ∂kronaug_state₁, ∂kronkronaug_state₁, aug_state₁[i], kron_aug_state₁[i]) + compressed_kron³_power_vjp!(∂aug_state₁_cubic, + ∂kronkronaug_state₁, + aug_state₁[i]) + ∂aug_state₁ .+= ∂aug_state₁_cubic - # kron_aug_state₁[i] = ℒ.kron(aug_state₁[i], aug_state₁[i]) - fill_kron_adjoint!(∂aug_state₁, ∂aug_state₁, ∂kronaug_state₁, aug_state₁[i], aug_state₁[i]) + # kron_aug_state₁[i] = compressed_kron²_power(aug_state₁[i]) + compressed_kron²_power_vjp!(∂aug_state₁_pair, ∂kronaug_state₁, aug_state₁[i]) + ∂aug_state₁ .+= ∂aug_state₁_pair if i < size(data_in_deviations,2) ∂state[1] *= 0 @@ -14507,29 +14720,20 @@ function rrule(::typeof(calculate_loglikelihood), ℒ.mul!(kron_Ix, 𝐒ⁱ²ᵉ[i]', ∂jacc) ∂kronIx = kron_Ix - if i < size(data_in_deviations,2) - fill_kron_adjoint_∂B!(∂kronIx, ∂x, -ℒ.I(T.nExo)) - else - fill_kron_adjoint_∂B!(∂kronIx, ∂x, ℒ.I(T.nExo)) - end + accumulate_sym_kron_jacobian_pullback!(∂x, ∂kronIx, x[i], i < size(data_in_deviations, 2) ? -1 : 1) - ℒ.kron!(kron_Ix, ℒ.I(T.nExo), x[i]) + compressed_kron²!(kron_Ix, x[i], J) ℒ.mul!(∂𝐒ⁱ²ᵉ_tmp, ∂jacc, kron_Ix', -1, 0) ℒ.mul!(kron_Ixx, 𝐒ⁱ³ᵉ', ∂jacc, 3/2, 0) ∂kronIxx = kron_Ixx fill!(∂kronxx, 0) + ∂x_cubic = zeros(T.nExo) + compressed_kron³_identity_vjp!(∂x_cubic, ∂kronIxx, x[i], i < size(data_in_deviations, 2) ? -1 : 1) + ∂x .+= ∂x_cubic - if i < size(data_in_deviations,2) - fill_kron_adjoint_∂B!(∂kronIxx, ∂kronxx, -ℒ.I(T.nExo)) - else - fill_kron_adjoint_∂B!(∂kronIxx, ∂kronxx, ℒ.I(T.nExo)) - end - - fill_kron_adjoint!(∂x, ∂x, ∂kronxx, x[i], x[i]) - - ℒ.kron!(kron_Ixx, ℒ.I(T.nExo), kronxx[i]) + compressed_kron³!(kron_Ixx, x[i], x[i], J) ℒ.mul!(∂𝐒ⁱ³ᵉ, ∂jacc, kron_Ixx', -3/2, 1) # find_shocks @@ -14556,20 +14760,29 @@ function rrule(::typeof(calculate_loglikelihood), copyto!(∂𝐒ⁱ, kronSλ) ℒ.axpy!(-1/2, ∂jacc, ∂𝐒ⁱ) - # ∂𝐒ⁱ²ᵉ += reshape(2 * ℒ.kron(S[1:T.nExo], ℒ.kron(x[i], λ[i])) - ℒ.kron(kronxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ²ᵉ)) - ℒ.kron!(kron_xλ, x[i], λ[i]) - ℒ.kron!(kron_S1_kxλ, S1, kron_xλ) - ℒ.kron!(kron_xx_S2, kronxx[i], S2) - ℒ.axpby!(-1, kron_xx_S2, 2, kron_S1_kxλ) - ∂𝐒ⁱ²ᵉ_tmp .+= reshape(kron_S1_kxλ, size(∂𝐒ⁱ²ᵉ_tmp)) + # Pair KKT cotangent in the compressed pair basis. The full-coordinate form was + # ∂𝐒ⁱ²ᵉ += reshape(2 * ℒ.kron(S[1:T.nExo], ℒ.kron(x[i], λ[i])) - ℒ.kron(kronxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ²ᵉ)) + # Two preallocated rank-1 updates rather than a column loop. The + # loop form read `∂𝐒ⁱ²ᵉ_tmp[:, c]` back on the right of a `.+=`, which + # materialises the column: 240 B per call at nExo = 2 rising to + # 328 kB at nExo = 40, and 1.6-5.8x slower than `ger!`. + xᵢ = x[i] + pair_column = 0 + @inbounds for p in 1:T.nExo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? S1[p] * xᵢ[q] : S1[p] * xᵢ[q] + S1[q] * xᵢ[p] + end + end + ℒ.mul!(∂𝐒ⁱ²ᵉ_tmp, λ[i], pair_top', 2, 1) + ℒ.mul!(∂𝐒ⁱ²ᵉ_tmp, S2, kronxx[i]', -1, 1) ∂𝐒ⁱ²ᵉ = ∂𝐒ⁱ²ᵉ_tmp - # ∂𝐒ⁱ³ᵉ += reshape(3 * ℒ.kron(S[1:T.nExo], ℒ.kron(ℒ.kron(x[i], x[i]), λ[i])) - ℒ.kron(kronxxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ³ᵉ)) - ℒ.kron!(kron_xxλ, kronxx[i], λ[i]) - ℒ.kron!(kron_S1_kxxλ, S1, kron_xxλ) - ℒ.kron!(kron_xxx_S2, kronxxx[i], S2) - ℒ.axpby!(-1, kron_xxx_S2, 3, kron_S1_kxxλ) - ∂𝐒ⁱ³ᵉ .+= reshape(kron_S1_kxxλ, size(∂𝐒ⁱ³ᵉ)) + # Triple KKT cotangent in the compressed triple basis. The full-coordinate form was + # ∂𝐒ⁱ³ᵉ += reshape(3 * ℒ.kron(S[1:T.nExo], ℒ.kron(ℒ.kron(x[i], x[i]), λ[i])) - ℒ.kron(kronxxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ³ᵉ)) + triple_xxS1 = compressed_kron³(x[i], x[i], S1) + ℒ.mul!(∂𝐒ⁱ³ᵉ, λ[i], triple_xxS1', 3, 1) + ℒ.mul!(∂𝐒ⁱ³ᵉ, S2, kronxxx[i]', -1, 1) # 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol) + 𝐒²⁻ᵛᵉ * ℒ.kron(ℒ.I(T.nExo), state²⁻_vol) + 𝐒³⁻ᵉ² * ℒ.kron(ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol), state¹⁻_vol) / 2 ∂kronstate¹⁻_vol *= 0 @@ -14600,11 +14813,18 @@ function rrule(::typeof(calculate_loglikelihood), ∂state[2] += ∂state²⁻_vol[1:end-1] + # Forward value of the state-vol pair; must not touch the + # ∂kronstate¹⁻_vol cotangent, which is still being accumulated. + compressed_kron²_power!(kronstate¹⁻_vol_buf, state¹⁻_vol) + ℒ.kron!(kron_buffer3sv, J, kronstate¹⁻_vol_buf) + ∂𝐒³⁻ᵉ² += ∂𝐒ⁱ * kron_buffer3sv' / 2 ∂kronIstate¹⁻_volstate¹⁻_vol = 𝐒³⁻ᵉ²' * ∂𝐒ⁱ / 2 - - fill_kron_adjoint_∂A!(∂kronIstate¹⁻_volstate¹⁻_vol, ∂kronstate¹⁻_vol, ℒ.I(T.nExo)) - - ∂𝐒³⁻ᵉ² += ∂𝐒ⁱ * ℒ.kron(ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol), state¹⁻_vol)' / 2 + ∂state_pair = zeros(n_state_vol) + @inbounds for j in 1:T.nExo + rows = (j - 1) * n_state_pair + 1:j * n_state_pair + compressed_kron²_power_vjp!(∂state_pair, view(∂kronIstate¹⁻_volstate¹⁻_vol, rows, j), state¹⁻_vol) + ∂state¹⁻_vol .+= ∂state_pair + end # 𝐒ⁱ²ᵉ[i] = 𝐒²ᵉ / 2 + 𝐒³⁻ᵉ * ℒ.kron(II, state¹⁻_vol) / 2 ∂𝐒²ᵉ += ∂𝐒ⁱ²ᵉ / 2 @@ -14634,25 +14854,35 @@ function rrule(::typeof(calculate_loglikelihood), ∂state[3] -= 𝐒¹⁻' * ∂shock_independent # ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, ℒ.kron(state¹⁻_vol, state¹⁻_vol), -1/2, 1) - ∂𝐒²⁻ᵛ -= ∂shock_independent * ℒ.kron(state¹⁻_vol, state¹⁻_vol)' / 2 + kron_sv = compressed_kron²_power(state¹⁻_vol) + ∂𝐒²⁻ᵛ -= ∂shock_independent * kron_sv' / 2 ∂kronstate¹⁻_vol -= 𝐒²⁻ᵛ' * ∂shock_independent / 2 # ℒ.mul!(shock_independent, 𝐒²⁻, ℒ.kron(state¹⁻, state²⁻), -1, 1) - ∂𝐒²⁻ -= ∂shock_independent * ℒ.kron(state¹⁻, state²⁻)' + ∂𝐒²⁻ -= ∂shock_independent * compressed_kron²(state¹⁻, state²⁻)' ∂kronstate¹⁻²⁻ = -𝐒²⁻' * ∂shock_independent - fill_kron_adjoint!(∂state[1], ∂state[2], ∂kronstate¹⁻²⁻, state¹⁻, state²⁻) + compressed_kron²_vjp!(∂state_pair_left, + ∂state_pair_right, + ∂kronstate¹⁻²⁻, + state¹⁻, + state²⁻) + ∂state[1] .+= ∂state_pair_left + ∂state[2] .+= ∂state_pair_right # ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, ℒ.kron(state¹⁻_vol, ℒ.kron(state¹⁻_vol, state¹⁻_vol)), -1/6, 1) - ∂𝐒³⁻ᵛ -= ∂shock_independent * ℒ.kron(ℒ.kron(state¹⁻_vol, state¹⁻_vol), state¹⁻_vol)' / 6 + ∂𝐒³⁻ᵛ -= ∂shock_independent * compressed_kron³_power(state¹⁻_vol)' / 6 ∂kronstate¹⁻_volstate¹⁻_vol = -𝐒³⁻ᵛ' * ∂shock_independent / 6 - fill_kron_adjoint!(∂kronstate¹⁻_vol, ∂state¹⁻_vol, ∂kronstate¹⁻_volstate¹⁻_vol, ℒ.kron(state¹⁻_vol, state¹⁻_vol), state¹⁻_vol) - - fill_kron_adjoint!(∂state¹⁻_vol, ∂state¹⁻_vol, ∂kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + ∂state_cubic = zeros(n_state_vol) + compressed_kron³_power_vjp!(∂state_cubic, ∂kronstate¹⁻_volstate¹⁻_vol, state¹⁻_vol) + ∂state¹⁻_vol .+= ∂state_cubic + ∂state_pair = zeros(n_state_vol) + compressed_kron²_power_vjp!(∂state_pair, ∂kronstate¹⁻_vol, state¹⁻_vol) + ∂state¹⁻_vol .+= ∂state_pair # state¹⁻_vol = vcat(state¹⁻, 1) ∂state[1] += ∂state¹⁻_vol[1:end-1] @@ -14729,6 +14959,10 @@ function rrule(::typeof(calculate_loglikelihood), cc.I_aug, cc.I_state_vol, cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows, ) ∂𝐒²ᵉ .+= ∂𝐒²ᵉ_warmup ∂𝐒ⁱ³ᵉ .+= 6 .* ∂𝐒³ᵉ_warmup @@ -14744,17 +14978,17 @@ function rrule(::typeof(calculate_loglikelihood), ∂𝐒[1][cond_var_idx,end-T.nExo+1:end] += ∂𝐒¹ᵉ ∂𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed] += ∂𝐒¹⁻ - ∂𝐒[2][cond_var_idx,var²_idxs] += ∂𝐒²⁻ - ∂𝐒[2][cond_var_idx,shockvar²_idxs] += ∂𝐒²⁻ᵉ - ∂𝐒[2][cond_var_idx,shock²_idxs] += ∂𝐒²ᵉ - ∂𝐒[2][cond_var_idx,shockvar_idxs] += ∂𝐒²⁻ᵛᵉ - ∂𝐒[3][cond_var_idx,shockvar³2_idxs] += ∂𝐒³⁻ᵉ² - ∂𝐒[3][cond_var_idx,shockvar³_idxs] += ∂𝐒³⁻ᵉ - ∂𝐒[3][cond_var_idx,shock³_idxs] += ∂𝐒ⁱ³ᵉ / 6 # 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 + ∂𝐒[2][cond_var_idx,var²_cols] += ∂𝐒²⁻ + ∂𝐒[2][cond_var_idx,shockvar²_cols] += ∂𝐒²⁻ᵉ + ∂𝐒[2][cond_var_idx,shock²_cols] += ∂𝐒²ᵉ + ∂𝐒[2][cond_var_idx,shockvar_cols] += ∂𝐒²⁻ᵛᵉ + ∂𝐒[3][cond_var_idx,shockvar³2_cols] += ∂𝐒³⁻ᵉ² + ∂𝐒[3][cond_var_idx,shockvar³_cols] += ∂𝐒³⁻ᵉ + ∂𝐒[3][cond_var_idx,shock³_cols] += ∂𝐒ⁱ³ᵉ / 6 # 𝐒ⁱ³ᵉ = 𝐒³ᵉ / 6 ∂𝐒[1][cond_var_idx, 1:T.nPast_not_future_and_mixed+1] += ∂𝐒¹⁻ᵛ - ∂𝐒[2][cond_var_idx,var_vol²_idxs] += ∂𝐒²⁻ᵛ - ∂𝐒[3][cond_var_idx,var_vol³_idxs] += ∂𝐒³⁻ᵛ + ∂𝐒[2][cond_var_idx,var_vol²_cols] += ∂𝐒²⁻ᵛ + ∂𝐒[3][cond_var_idx,var_vol³_cols] += ∂𝐒³⁻ᵛ ∂𝐒[1][T.past_not_future_and_mixed_idx,:] += ∂𝐒⁻¹ ∂𝐒[2][T.past_not_future_and_mixed_idx,:] += ∂𝐒⁻² @@ -14812,23 +15046,37 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} shock³_idxs = tc.shock³_idxs shockvar³2_idxs = tc.shockvar³2_idxs shockvar³_idxs = tc.shockvar³_idxs + shock_shock_state_indices = tc.shock_shock_state_idxs + shock_shock_state_rows = tc.shock_shock_state_rows + + n_aug = n_past + 1 + n_exo + n_state_vol = n_past + 1 + shock²_cols = cc.shock²_cols + shockvar²_cols = cc.shockvar²_cols + var_vol²_cols = cc.var_vol²_cols + var_vol³_cols = tc.var_vol³_cols + shock³_cols = tc.shock³_cols + shockvar³2_cols = tc.shockvar³2_cols + shockvar³_cols = tc.shockvar³_cols 𝐒⁻¹ = 𝐒[1][Tcc.past_not_future_and_mixed_idx, :] 𝐒¹⁻ᵛ = 𝐒[1][cond_var_idx, 1:n_past+1] 𝐒¹ᵉ = 𝐒[1][cond_var_idx, end-n_exo+1:end] - 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_idxs]) - 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_idxs]) - 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_idxs]) + 𝐒²⁻ᵛ = collect(𝐒[2][cond_var_idx, var_vol²_cols]) + 𝐒²⁻ᵉ = collect(𝐒[2][cond_var_idx, shockvar²_cols]) + 𝐒²ᵉ = collect(𝐒[2][cond_var_idx, shock²_cols]) 𝐒⁻² = collect(𝐒[2][Tcc.past_not_future_and_mixed_idx, :]) - 𝐒³⁻ᵛ = collect(𝐒[3][cond_var_idx, var_vol³_idxs]) - 𝐒³⁻ᵉ² = collect(𝐒[3][cond_var_idx, shockvar³2_idxs]) - 𝐒³⁻ᵉ = collect(𝐒[3][cond_var_idx, shockvar³_idxs]) - 𝐒³ᵉ = collect(𝐒[3][cond_var_idx, shock³_idxs]) + 𝐒³⁻ᵛ = collect(𝐒[3][cond_var_idx, var_vol³_cols]) + 𝐒³⁻ᵉ² = collect(𝐒[3][cond_var_idx, shockvar³2_cols]) + 𝐒³⁻ᵉ = collect(𝐒[3][cond_var_idx, shockvar³_cols]) + 𝐒³ᵉ = collect(𝐒[3][cond_var_idx, shock³_cols]) 𝐒⁻³ = collect(𝐒[3][Tcc.past_not_future_and_mixed_idx, :]) 𝐒ⁱ³ᵉ = 𝐒³ᵉ ./ 6 J = ℒ.I(n_exo) - II = sparse(ℒ.I(n_exo^2)) + n_exo² = n_exo * (n_exo + 1) ÷ 2 + n_exo³ = n_exo * (n_exo + 1) * (n_exo + 2) ÷ 6 + II = sparse(ℒ.I(n_exo²)) st = copy(state[Tcc.past_not_future_and_mixed_idx]) @@ -14896,7 +15144,11 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter rrule (3rd, missing) failed during warmup") end @@ -14909,8 +15161,8 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} aug_state[n_past + 1] = 1.0 copyto!(aug_state, n_past + 2, view(warmup_shocks, :, w), 1, n_exo) - ℒ.kron!(kron_aug_state, aug_state, aug_state) - ℒ.kron!(kron_kron_aug_state, kron_aug_state, aug_state) + compressed_kron²_power!(kron_aug_state, aug_state) + compressed_kron³_power!(kron_kron_aug_state, aug_state) ℒ.mul!(st, 𝐒⁻¹, aug_state) ℒ.mul!(st, 𝐒⁻², kron_aug_state, 1/2, 1) ℒ.mul!(st, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) @@ -14935,22 +15187,34 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # shock_independent = data - 𝐒¹⁻ᵛ s¹v - 0.5 𝐒²⁻ᵛ k(s¹v,s¹v) - (1/6) 𝐒³⁻ᵛ k(s¹v,k(s¹v,s¹v)) copyto!(shock_independent, view(data_in_deviations, :, t)) ℒ.mul!(shock_independent, 𝐒¹⁻ᵛ, state¹⁻_vol, -1, 1) - ℒ.kron!(kronstate¹⁻_vol, state¹⁻_vol, state¹⁻_vol) + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒²⁻ᵛ, kronstate¹⁻_vol, -1/2, 1) - ℒ.kron!(kron_kron_state¹⁻_vol, kronstate¹⁻_vol, state¹⁻_vol) + compressed_kron³_power!(kron_kron_state¹⁻_vol, state¹⁻_vol) ℒ.mul!(shock_independent, 𝐒³⁻ᵛ, kron_kron_state¹⁻_vol, -1/6, 1) # 𝐒ⁱ_full = 𝐒¹ᵉ + 𝐒²⁻ᵉ k(I,s¹v) + 0.5 𝐒³⁻ᵉ² k(k(I,s¹v),s¹v) kron_J_s1v = ℒ.kron(J, state¹⁻_vol) copyto!(𝐒ⁱ_full, 𝐒¹ᵉ) ℒ.mul!(𝐒ⁱ_full, 𝐒²⁻ᵉ, kron_J_s1v, 1, 1) - ℒ.kron!(kron_buffer3sv, kron_J_s1v, state¹⁻_vol) - ℒ.mul!(𝐒ⁱ_full, 𝐒³⁻ᵉ², kron_buffer3sv, 1/2, 1) + compressed_triple_state_pair_to_shock!(kron_buffer3sv, + kronstate¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shockvar³2_cols, + n_past + 1) + ℒ.mul!(𝐒ⁱ_full, 𝐒³⁻ᵉ², kron_buffer3sv, 1, 1) # 𝐒ⁱ²ᵉ_full = 𝐒²ᵉ/2 + 𝐒³⁻ᵉ k(II, s¹v)/2 - x_kron_II!(kron_buffer4sv, state¹⁻_vol) copyto!(𝐒ⁱ²ᵉ_full, 𝐒²ᵉ); ℒ.rdiv!(𝐒ⁱ²ᵉ_full, 2) - ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1/2, 1) + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(𝐒ⁱ²ᵉ_full, 𝐒³⁻ᵉ, kron_buffer4sv, 1, 1) copyto!(state¹⁻_vol_seq[t], state¹⁻_vol) 𝐒ⁱ_full_seq[t] .= 𝐒ⁱ_full @@ -14976,9 +15240,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} return on_failure_loglikelihood, _ -> (NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent(), NoTangent()) end if t > presample_periods - kron_J_x = ℒ.kron(J, x) - kron_xx = ℒ.kron(x, x) - kron_J_xx = ℒ.kron(J, kron_xx) + kron_J_x = compressed_kron²(x, J) + kron_xx = compressed_kron²_power(x) + kron_J_xx = compressed_kron³(x, x, J) jac_v = 𝐒ⁱ_v + 2 * 𝐒ⁱ²ᵉ_v * kron_J_x + 3 * 𝐒ⁱ³ᵉ_v * kron_J_xx logabsdets += m == n_exo ? ℒ.logabsdet(jac_v)[1] : ℒ.logabsdet(jac_v * jac_v')[1] / 2 shocks² += sum(abs2, x) @@ -14991,8 +15255,8 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} x_seq[t] .= x copyto!(aug_state_seq[t], 1, st, 1); aug_state_seq[t][n_past+1] = 1.0; copyto!(aug_state_seq[t], n_past+2, x, 1, n_exo) - ℒ.kron!(kron_aug_state, aug_state_seq[t], aug_state_seq[t]) - ℒ.kron!(kron_kron_aug_state, kron_aug_state, aug_state_seq[t]) + compressed_kron²_power!(kron_aug_state, aug_state_seq[t]) + compressed_kron³_power!(kron_kron_aug_state, aug_state_seq[t]) ℒ.mul!(st, 𝐒⁻¹, aug_state_seq[t]) ℒ.mul!(st, 𝐒⁻², kron_aug_state, 1/2, 1) ℒ.mul!(st, 𝐒⁻³, kron_kron_aug_state, 1/6, 1) @@ -15023,17 +15287,21 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ³ᵉ = zero(𝐒ⁱ³ᵉ) ∂data_in_deviations = zeros(size(data_in_deviations)) ∂st_next = zeros(n_past) - kronaug_buf = zeros((n_past + 1 + n_exo)^2) - ∂kronaug = zeros((n_past + 1 + n_exo)^2) + kronaug_buf = zeros(n_aug * (n_aug + 1) ÷ 2) + ∂kronaug = zeros(n_aug * (n_aug + 1) ÷ 2) ∂aug_state = zeros(n_past + 1 + n_exo) - ∂kronstate = zeros((n_past + 1)^2) + ∂aug_state_pair_third = zeros(n_past + 1 + n_exo) + ∂aug_state_cubic_third = zeros(n_past + 1 + n_exo) + ∂kronstate = zeros(n_state_vol * (n_state_vol + 1) ÷ 2) ∂state¹⁻_vol = zeros(n_past + 1) + ∂state¹⁻_vol_pair_third = zeros(n_past + 1) + ∂state¹⁻_vol_cubic_third = zeros(n_past + 1) ∂𝐒ⁱ_full_buf = zeros(length(cond_var_idx), n_exo) - ∂𝐒ⁱ²ᵉ_full_buf = zeros(length(cond_var_idx), n_exo^2) + ∂𝐒ⁱ²ᵉ_full_buf = zeros(length(cond_var_idx), n_exo²) ∂shock_independent = zeros(length(cond_var_idx)) kron_Isv_buf = zeros(n_exo * (n_past + 1), n_exo) ∂kronIstate_local = zeros(n_exo * (n_past + 1), n_exo) - ∂kronaug_for3 = zeros((n_past + 1 + n_exo)^2) + ∂kronaug_for3 = zeros(n_aug * (n_aug + 1) ÷ 2) ∂u_mat = zeros(n_exo * (n_past + 1), n_exo) ∂jac_v_buf = zeros(length(cond_var_idx), n_exo) @@ -15047,6 +15315,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} fill!(∂st_next, 0) fill!(kronaug_buf, 0); fill!(∂kronaug, 0) fill!(∂aug_state, 0); fill!(∂kronstate, 0); fill!(∂state¹⁻_vol, 0) + fill!(∂aug_state_pair_third, 0) + fill!(∂aug_state_cubic_third, 0) + fill!(∂state¹⁻_vol_pair_third, 0) + fill!(∂state¹⁻_vol_cubic_third, 0) fill!(∂𝐒ⁱ_full_buf, 0); fill!(∂𝐒ⁱ²ᵉ_full_buf, 0); fill!(∂shock_independent, 0) fill!(kron_Isv_buf, 0); fill!(∂kronIstate_local, 0) fill!(∂kronaug_for3, 0); fill!(∂u_mat, 0) @@ -15059,21 +15331,24 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} idx = obs_idx_per_t[t] m = length(idx) + # kronstate¹⁻_vol is a shared workspace buffer, so it still holds the + # value from the last forward step. Refresh it for this period before + # it is read back below. + compressed_kron²_power!(kronstate¹⁻_vol, state¹⁻_vol) + # State recursion: st_next = 𝐒⁻¹ aug + 0.5 𝐒⁻² kron(aug,aug) + (1/6) 𝐒⁻³ kron(aug, kron(aug,aug)) ℒ.mul!(∂𝐒⁻¹, ∂st_next, aug_state', 1, 1) ℒ.mul!(∂aug_state, 𝐒⁻¹', ∂st_next) - ℒ.kron!(kronaug_buf, aug_state, aug_state) + compressed_kron²_power!(kronaug_buf, aug_state) ℒ.mul!(∂𝐒⁻², ∂st_next, kronaug_buf', 1/2, 1) ∂kronaug2 = (𝐒⁻²' * ∂st_next) ./ 2 - kron_kron_aug = ℒ.kron(kronaug_buf, aug_state) + kron_kron_aug = compressed_kron³_power(aug_state) ℒ.mul!(∂𝐒⁻³, ∂st_next, kron_kron_aug', 1/6, 1) ∂kronkronaug = (𝐒⁻³' * ∂st_next) ./ 6 - fill!(∂kronaug_for3, 0) - fill_kron_adjoint!(∂aug_state, ∂kronaug_for3, ∂kronkronaug, aug_state, kronaug_buf) - fill!(∂kronaug, 0) - ∂kronaug .+= ∂kronaug_for3 - ∂kronaug .+= ∂kronaug2 - fill_kron_adjoint!(∂aug_state, ∂aug_state, ∂kronaug, aug_state, aug_state) + compressed_kron³_power_vjp!(∂aug_state_cubic_third, ∂kronkronaug, aug_state) + compressed_kron²_power_vjp!(∂aug_state_pair_third, ∂kronaug2, aug_state) + ∂aug_state .+= ∂aug_state_cubic_third + ∂aug_state .+= ∂aug_state_pair_third fill!(∂st_next, 0) ∂x = ∂aug_state[n_past+2:end] @@ -15086,15 +15361,15 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} # shocks² and logabsdet contributions (only if t > presample and m > 0) ∂jac_v = view(∂jac_v_buf, 1:m, :); fill!(∂jac_v, 0) jac_v_local = zeros(m, n_exo) - 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo^2) - 𝐒ⁱ³ᵉ_v_local = zeros(m, n_exo^3) + 𝐒ⁱ²ᵉ_v_local = zeros(m, n_exo²) + 𝐒ⁱ³ᵉ_v_local = zeros(m, n_exo³) if m > 0 𝐒ⁱ_v_local = 𝐒ⁱ_full_seq[t][idx, :] 𝐒ⁱ²ᵉ_v_local = 𝐒ⁱ²ᵉ_full_seq[t][idx, :] 𝐒ⁱ³ᵉ_v_local = 𝐒ⁱ³ᵉ[idx, :] - kron_J_x_local = ℒ.kron(J, x) - kron_xx_local = ℒ.kron(x, x) - kron_J_xx_local = ℒ.kron(J, kron_xx_local) + kron_J_x_local = compressed_kron²(x, J) + kron_xx_local = compressed_kron²_power(x) + kron_J_xx_local = compressed_kron³(x, x, J) jac_v_local = 𝐒ⁱ_v_local + 2 * 𝐒ⁱ²ᵉ_v_local * kron_J_x_local + 3 * 𝐒ⁱ³ᵉ_v_local * kron_J_xx_local end if m > 0 && t > presample_periods @@ -15108,28 +15383,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} G = inv(jac_v_local * jac_v_local') ∂jac_v .+= (-1.0) .* (G * jac_v_local) end - # Indirect channel: ∂jac_v → ∂x - @inbounds for l in 1:n_exo - s = 0.0 - for r in 1:n_exo - for i_local in 1:m - s += 2 * ∂jac_v[i_local, r] * 𝐒ⁱ²ᵉ_v_local[i_local, (r-1)*n_exo + l] - end - for q in 1:n_exo - col = (r-1)*n_exo^2 + (l-1)*n_exo + q - for i_local in 1:m - s += 3 * ∂jac_v[i_local, r] * 𝐒ⁱ³ᵉ_v_local[i_local, col] * x[q] - end - end - for p in 1:n_exo - col = (r-1)*n_exo^2 + (p-1)*n_exo + l - for i_local in 1:m - s += 3 * ∂jac_v[i_local, r] * 𝐒ⁱ³ᵉ_v_local[i_local, col] * x[p] - end - end - end - ∂x[l] += s - end + # Indirect channel: ∂jac_v → ∂x, using compressed pair/triple coordinates. + compressed_kron²_identity_vjp!(∂x, 𝐒ⁱ²ᵉ_v_local' * ∂jac_v, x, 2) + compressed_kron³_identity_vjp!(∂x, 𝐒ⁱ³ᵉ_v_local' * ∂jac_v, x, 3) end fill!(∂shock_independent, 0) @@ -15148,8 +15404,9 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} λ = 2 * (Gloc * (jac_v * x)) end - kron_II_x = ℒ.kron(II, x) - M = reshape((𝐒ⁱ²ᵉ_v + 3 * 𝐒ⁱ³ᵉ_v * kron_II_x)' * λ, n_exo, n_exo) + M = zeros(Float64, n_exo, n_exo) + compressed_pair_hessian!(M, 𝐒ⁱ²ᵉ_v' * λ) + compressed_triple_hessian!(M, 3 * (𝐒ⁱ³ᵉ_v' * λ), x) topL = 2 * ℒ.I(n_exo) - 2 * M fXλp = [topL -jac_v' jac_v zeros(m, m)] @@ -15164,16 +15421,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} xSx = x * Sx' xx_outer = x * x' - ∂𝐒ⁱ²ᵉ_v_kkt = 2 * λ * vec(xSx)' - Sλ * vec(xx_outer)' - - kron_Sx_xx = ℒ.kron(Sx, kron_xx_local) - kron_x_xx = ℒ.kron(x, kron_xx_local) - ∂𝐒ⁱ³ᵉ_v_kkt = 3 * λ * kron_Sx_xx' - Sλ * kron_x_xx' + ∂𝐒ⁱ²ᵉ_v_kkt = 2 * λ * (compressed_kron²(x, Sx))' - Sλ * (compressed_kron²_power(x))' + ∂𝐒ⁱ³ᵉ_v_kkt = 3 * λ * (compressed_kron³(x, x, Sx))' - Sλ * (compressed_kron³_power(x))' if t > presample_periods ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v + ∂jac_v - ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * ℒ.kron(J, x)' - ∂𝐒ⁱ³ᵉ_v_total = ∂𝐒ⁱ³ᵉ_v_kkt + 3 * ∂jac_v * ℒ.kron(J, kron_xx_local)' + ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt + 2 * ∂jac_v * compressed_kron²(x, J)' + ∂𝐒ⁱ³ᵉ_v_total = ∂𝐒ⁱ³ᵉ_v_kkt + 3 * ∂jac_v * compressed_kron³(x, x, J)' else ∂𝐒ⁱ_v_total = ∂𝐒ⁱ_v ∂𝐒ⁱ²ᵉ_v_total = ∂𝐒ⁱ²ᵉ_v_kkt @@ -15189,12 +15443,12 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒ⁱ_full[idx[i_local], j] = ∂𝐒ⁱ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^2 + @inbounds for j in 1:n_exo² for i_local in 1:m ∂𝐒ⁱ²ᵉ_full[idx[i_local], j] = ∂𝐒ⁱ²ᵉ_v_total[i_local, j] end end - @inbounds for j in 1:n_exo^3 + @inbounds for j in 1:n_exo³ for i_local in 1:m ∂𝐒ⁱ³ᵉ[idx[i_local], j] += ∂𝐒ⁱ³ᵉ_v_total[i_local, j] end @@ -15208,8 +15462,14 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒¹ᵉ .+= ∂𝐒ⁱ_full kron_J_s1v = ℒ.kron(J, state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵉ, ∂𝐒ⁱ_full, kron_J_s1v', 1, 1) - kron_kron_J_s1v_s1v = ℒ.kron(kron_J_s1v, state¹⁻_vol) - ℒ.mul!(∂𝐒³⁻ᵉ², ∂𝐒ⁱ_full, kron_kron_J_s1v_s1v', 1/2, 1) + compressed_triple_state_pair_to_shock!(kron_buffer3sv, + kronstate¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shockvar³2_cols, + n_past + 1) + ℒ.mul!(∂𝐒³⁻ᵉ², ∂𝐒ⁱ_full, kron_buffer3sv', 1, 1) ∂kronIs1v_a = 𝐒²⁻ᵉ' * ∂𝐒ⁱ_full fill!(∂state¹⁻_vol, 0) @@ -15220,37 +15480,34 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end ∂state¹⁻_vol[p] += s end - ∂kron_u_s1v = (𝐒³⁻ᵉ²' * ∂𝐒ⁱ_full) ./ 2 - u_mat = ℒ.kron(J, state¹⁻_vol) - fill!(∂u_mat, 0) - @inbounds for j in 1:n_exo - for q in 1:(n_exo*(n_past+1)) - for r in 1:(n_past+1) - ∂u_mat[q, j] += ∂kron_u_s1v[(q-1)*(n_past+1) + r, j] * state¹⁻_vol[r] - ∂state¹⁻_vol[r] += ∂kron_u_s1v[(q-1)*(n_past+1) + r, j] * u_mat[q, j] - end - end - end - @inbounds for p in 1:(n_past + 1) - s = 0.0 - for j in 1:n_exo - s += ∂u_mat[(j-1)*(n_past+1) + p, j] - end - ∂state¹⁻_vol[p] += s - end + compressed_triple_state_pair_to_shock_vjp!( + ∂state¹⁻_vol, + 𝐒³⁻ᵉ²' * ∂𝐒ⁱ_full, + state¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shockvar³2_cols) # Propagate ∂𝐒ⁱ²ᵉ_full back: 𝐒ⁱ²ᵉ_full = 𝐒²ᵉ/2 + 𝐒³⁻ᵉ k(II, s¹v)/2 ∂𝐒²ᵉ .+= ∂𝐒ⁱ²ᵉ_full ./ 2 - kron_II_s1v = ℒ.kron(II, state¹⁻_vol) - ℒ.mul!(∂𝐒³⁻ᵉ, ∂𝐒ⁱ²ᵉ_full, kron_II_s1v', 1/2, 1) - ∂kronIIs1v = (𝐒³⁻ᵉ' * ∂𝐒ⁱ²ᵉ_full) ./ 2 - @inbounds for p in 1:(n_past + 1) - s = 0.0 - for j in 1:n_exo^2 - s += ∂kronIIs1v[(j-1)*(n_past+1) + p, j] - end - ∂state¹⁻_vol[p] += s - end + compressed_triple_state_to_pair!(kron_buffer4sv, + state¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(∂𝐒³⁻ᵉ, ∂𝐒ⁱ²ᵉ_full, kron_buffer4sv', 1, 1) + compressed_triple_state_to_pair_vjp!( + ∂state¹⁻_vol, + 𝐒³⁻ᵉ' * ∂𝐒ⁱ²ᵉ_full, + state¹⁻_vol, + n_aug, + n_past + 1, + n_exo, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) else fill!(∂state¹⁻_vol, 0) end @@ -15261,20 +15518,23 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} end ℒ.mul!(∂𝐒¹⁻ᵛ, ∂shock_independent, state¹⁻_vol', -1, 1) ℒ.mul!(∂state¹⁻_vol, 𝐒¹⁻ᵛ', ∂shock_independent, -1, 1) - kron_sv = ℒ.kron(state¹⁻_vol, state¹⁻_vol) + kron_sv = compressed_kron²_power(state¹⁻_vol) ℒ.mul!(∂𝐒²⁻ᵛ, ∂shock_independent, kron_sv', -1/2, 1) ∂kron_sv = -(𝐒²⁻ᵛ' * ∂shock_independent) ./ 2 fill!(∂kronstate, 0) ∂kronstate .+= ∂kron_sv - kron_s1v_3 = ℒ.kron(state¹⁻_vol, kron_sv) + kron_s1v_3 = compressed_kron³_power(state¹⁻_vol) ℒ.mul!(∂𝐒³⁻ᵛ, ∂shock_independent, kron_s1v_3', -1/6, 1) ∂kron_s1v_3 = -(𝐒³⁻ᵛ' * ∂shock_independent) ./ 6 - ∂a_outer = zeros(n_past + 1) - ∂b_outer = zeros((n_past + 1)^2) - fill_kron_adjoint!(∂a_outer, ∂b_outer, ∂kron_s1v_3, state¹⁻_vol, kron_sv) - ∂state¹⁻_vol .+= ∂a_outer - ∂kronstate .+= ∂b_outer - fill_kron_adjoint!(∂state¹⁻_vol, ∂state¹⁻_vol, ∂kronstate, state¹⁻_vol, state¹⁻_vol) + compressed_kron³_power_vjp!(∂state¹⁻_vol_cubic_third, + ∂kron_s1v_3, + state¹⁻_vol) + compressed_kron²_power_vjp!(∂state¹⁻_vol_pair_third, + ∂kron_sv, + state¹⁻_vol, + 1) + ∂state¹⁻_vol .+= ∂state¹⁻_vol_cubic_third + ∂state¹⁻_vol .+= ∂state¹⁻_vol_pair_third # state¹⁻_vol = vcat(st, 1) → ∂st_next += ∂state¹⁻_vol[1:n_past] @inbounds for j in 1:n_past @@ -15353,6 +15613,10 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} cc.I_aug, cc.I_state_vol, cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows, ) ∂𝐒¹⁻ᵛ[warmup_idx0, :] .+= ∂𝐒¹⁻ᵛ_w @@ -15372,13 +15636,13 @@ function rrule(::typeof(calculate_loglikelihood_with_missing), ::Val{:inversion} ∂𝐒_3[Tcc.past_not_future_and_mixed_idx, :] .+= ∂𝐒⁻³ ∂𝐒_1[cond_var_idx, 1:n_past+1] .+= ∂𝐒¹⁻ᵛ ∂𝐒_1[cond_var_idx, end-n_exo+1:end] .+= ∂𝐒¹ᵉ - ∂𝐒_2[cond_var_idx, var_vol²_idxs] .+= ∂𝐒²⁻ᵛ - ∂𝐒_2[cond_var_idx, shockvar²_idxs] .+= ∂𝐒²⁻ᵉ - ∂𝐒_2[cond_var_idx, shock²_idxs] .+= ∂𝐒²ᵉ - ∂𝐒_3[cond_var_idx, var_vol³_idxs] .+= ∂𝐒³⁻ᵛ - ∂𝐒_3[cond_var_idx, shockvar³2_idxs] .+= ∂𝐒³⁻ᵉ² - ∂𝐒_3[cond_var_idx, shockvar³_idxs] .+= ∂𝐒³⁻ᵉ - ∂𝐒_3[cond_var_idx, shock³_idxs] .+= ∂𝐒ⁱ³ᵉ ./ 6 + ∂𝐒_2[cond_var_idx, var_vol²_cols] .+= ∂𝐒²⁻ᵛ + ∂𝐒_2[cond_var_idx, shockvar²_cols] .+= ∂𝐒²⁻ᵉ + ∂𝐒_2[cond_var_idx, shock²_cols] .+= ∂𝐒²ᵉ + ∂𝐒_3[cond_var_idx, var_vol³_cols] .+= ∂𝐒³⁻ᵛ + ∂𝐒_3[cond_var_idx, shockvar³2_cols] .+= ∂𝐒³⁻ᵉ² + ∂𝐒_3[cond_var_idx, shockvar³_cols] .+= ∂𝐒³⁻ᵉ + ∂𝐒_3[cond_var_idx, shock³_cols] .+= ∂𝐒ⁱ³ᵉ ./ 6 ℒ.rmul!(∂𝐒_1, ∂llh) ℒ.rmul!(∂𝐒_2, ∂llh) @@ -15429,20 +15693,9 @@ function accumulate_cubic_kron_jacobian_pullback!( vec::AbstractVector{Float64}, I_mat::AbstractMatrix{Float64}, ) - seed = ∂jac_term / 6 - jac2_full = ℒ.kron(I_mat, vec) + ℒ.kron(vec, I_mat) - kronvv = ℒ.kron(vec, vec) - - ∂jac2_full = zeros(Float64, size(jac2_full)) - ∂vec_local = zeros(Float64, length(vec)) - fill_kron_adjoint_matrix_vector_rhs!(∂jac2_full, ∂vec_local, seed, jac2_full, vec) - - ∂kronvv = zeros(Float64, length(kronvv)) - fill_kron_adjoint_∂A!(seed, ∂kronvv, I_mat) - fill_kron_adjoint!(∂vec_local, ∂vec_local, ∂kronvv, vec, vec) - - accumulate_sym_kron_jacobian_pullback!(∂vec, 2 .* ∂jac2_full, vec) - ℒ.axpy!(1, ∂vec_local, ∂vec) + # compressed_kron³_identity_vjp! is already the exact VJP of + # compressed_kron³(vec, vec, I); the caller scales ∂jac_term, so no extra weight. + compressed_kron³_identity_vjp!(∂vec, ∂jac_term, vec) return nothing end @@ -15476,8 +15729,8 @@ function third_order_warmup_state_pullback!( @inbounds for i in 1:warmup_iterations-1 aug_state = [st; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) - kronkronaug_state = ℒ.kron(kronaug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) + kronkronaug_state = compressed_kron³_power(aug_state) st = 𝐒⁻¹ * aug_state + 𝐒⁻² * kronaug_state / 2 + 𝐒⁻³ * kronkronaug_state / 6 state_hist[i + 1] = copy(st) end @@ -15486,8 +15739,8 @@ function third_order_warmup_state_pullback!( @inbounds for i in warmup_iterations-1:-1:1 aug_state = [state_hist[i]; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) - kronkronaug_state = ℒ.kron(kronaug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) + kronkronaug_state = compressed_kron³_power(aug_state) ℒ.mul!(∂𝐒⁻¹, ∂state, aug_state', 1, 1) ℒ.mul!(∂𝐒⁻², ∂state, kronaug_state', 1/2, 1) @@ -15496,8 +15749,12 @@ function third_order_warmup_state_pullback!( ∂aug_state = 𝐒⁻¹' * ∂state ∂kronaug_state = 𝐒⁻²' * ∂state / 2 ∂kronkronaug_state = 𝐒⁻³' * ∂state / 6 - fill_kron_adjoint!(∂aug_state, ∂kronaug_state, ∂kronkronaug_state, aug_state, kronaug_state) - fill_kron_adjoint!(∂aug_state, ∂aug_state, ∂kronaug_state, aug_state, aug_state) + ∂aug_state_cubic = similar(∂aug_state) + ∂aug_state_pair = similar(∂aug_state) + compressed_kron³_power_vjp!(∂aug_state_cubic, ∂kronkronaug_state, aug_state) + compressed_kron²_power_vjp!(∂aug_state_pair, ∂kronaug_state, aug_state) + ∂aug_state .+= ∂aug_state_cubic + ∂aug_state .+= ∂aug_state_pair copyto!(∂state, 1, ∂aug_state, 1, n_past) @views ℒ.axpy!(1, ∂aug_state[n_past + 2:end], ∂warmup_x[(i - 1) * n_exo + 1:i * n_exo]) @@ -15541,7 +15798,11 @@ function third_order_warmup_observation_and_jacobian_pullback!( ∂jac_seed::AbstractMatrix{Float64}, I_aug::AbstractMatrix{Float64}, I_state_vol::AbstractMatrix{Float64}, - I_exo::AbstractMatrix{Float64} + I_exo::AbstractMatrix{Float64}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing ) n_past = length(state0) n_exo = size(𝐒¹ᵉ, 2) @@ -15566,15 +15827,15 @@ function third_order_warmup_observation_and_jacobian_pullback!( ds_hist[i] = copy(ds_dz) aug_state = [st; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) - kronkronaug_state = ℒ.kron(kronaug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) + kronkronaug_state = compressed_kron³_power(aug_state) state_next = 𝐒⁻¹ * aug_state + 𝐒⁻² * kronaug_state / 2 + 𝐒⁻³ * kronkronaug_state / 6 - jac_aug2_term = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 - jac_aug3_term = (ℒ.kron(ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug), aug_state) + ℒ.kron(kronaug_state, I_aug)) / 6 + jac_aug2_term = compressed_kron²(aug_state, I_aug) + jac_aug3_term = compressed_kron³(aug_state, aug_state, I_aug) - Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2_term[:, 1:n_past] + 𝐒⁻³ * jac_aug3_term[:, 1:n_past] - Fu = 𝐒⁻¹[:, (n_past + 2):end] + 𝐒⁻² * jac_aug2_term[:, (n_past + 2):end] + 𝐒⁻³ * jac_aug3_term[:, (n_past + 2):end] + Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2_term[:, 1:n_past] + 𝐒⁻³ * (jac_aug3_term[:, 1:n_past] / 2) + Fu = 𝐒⁻¹[:, (n_past + 2):end] + 𝐒⁻² * jac_aug2_term[:, (n_past + 2):end] + 𝐒⁻³ * (jac_aug3_term[:, (n_past + 2):end] / 2) ds_dz = Fs * ds_dz @views ds_dz[:, (i - 1) * n_exo + 1:i * n_exo] .+= Fu @@ -15585,22 +15846,41 @@ function third_order_warmup_observation_and_jacobian_pullback!( state_vol = [state_hist[end]; 1.0] final_shock = copy(view(warmup_shocks, :, n_warm)) - kronstate_vol = ℒ.kron(state_vol, state_vol) - kronstate_vol3 = ℒ.kron(state_vol, kronstate_vol) + kronstate_vol = compressed_kron²_power(state_vol) + kronstate_vol3 = compressed_kron³_power(state_vol) kron_shock_state = ℒ.kron(final_shock, state_vol) - kron_shock_shock = ℒ.kron(final_shock, final_shock) - kron_shock_state2 = ℒ.kron(kron_shock_state, state_vol) - kron_shock2_state = ℒ.kron(kron_shock_shock, state_vol) - kron_shock3 = ℒ.kron(final_shock, kron_shock_shock) - - jac_state2_term = (ℒ.kron(I_state_vol, state_vol) + ℒ.kron(state_vol, I_state_vol)) / 2 - jac_state3_term = (ℒ.kron(ℒ.kron(I_state_vol, state_vol) + ℒ.kron(state_vol, I_state_vol), state_vol) + ℒ.kron(kronstate_vol, I_state_vol)) / 6 + kron_shock_shock = compressed_kron²_power(final_shock) + shock_offset = n_past + 1 + # The caller normally hands these down from the model's `third_order_indices`; + # build them only if it did not. + if isnothing(shock_state_state_indices) + shock_state_state_indices, shock_state_state_rows = + compressed_shock_state_state_index_map(n_state_vol, n_exo) + end + if isnothing(shock_shock_state_indices) + shock_shock_state_indices, shock_shock_state_rows = + compressed_shock_shock_state_index_map(n_state_vol, n_exo) + end + kron_shock_state2 = compressed_triple_shock_state_state( + final_shock, state_vol, shock_offset, shock_state_state_indices; + index_rows = shock_state_state_rows) + kron_shock2_state = compressed_triple_shock_shock_state( + final_shock, state_vol, shock_offset, shock_shock_state_indices; + index_rows = shock_shock_state_rows) + kron_shock3 = compressed_kron³_power(final_shock) + + jac_state2_term = compressed_kron²(state_vol, I_state_vol) + jac_state3_term = compressed_kron³(state_vol, state_vol, I_state_vol) kron_shock_I = ℒ.kron(final_shock, I_state_vol) kron_I_state = ℒ.kron(I_exo, state_vol) ∂state = zeros(Float64, n_past) ∂state_vol = zeros(Float64, n_state_vol) + ∂state_vol_pair = zeros(Float64, n_state_vol) + ∂state_vol_cubic = zeros(Float64, n_state_vol) ∂final_shock = zeros(Float64, n_exo) + ∂final_shock_pair = zeros(Float64, n_exo) + ∂final_shock_cubic = zeros(Float64, n_exo) ∂ds_dz = zeros(Float64, n_past, n_z) ℒ.mul!(∂𝐒¹⁻ᵛ, ∂y_pred, state_vol', 1, 1) @@ -15608,15 +15888,15 @@ function third_order_warmup_observation_and_jacobian_pullback!( ℒ.mul!(∂𝐒²⁻ᵛ, ∂y_pred, kronstate_vol', 1/2, 1) ∂kronstate_vol = 𝐒²⁻ᵛ' * ∂y_pred / 2 - fill_kron_adjoint!(∂state_vol, ∂state_vol, ∂kronstate_vol, state_vol, state_vol) + # compressed_kron²_power_vjp! overwrites its output, so it needs the scratch + # vector; writing straight into ∂state_vol would drop the 𝐒¹⁻ᵛ term above. + compressed_kron²_power_vjp!(∂state_vol_pair, ∂kronstate_vol, state_vol) + ∂state_vol .+= ∂state_vol_pair ℒ.mul!(∂𝐒³⁻ᵛ, ∂y_pred, kronstate_vol3', 1/6, 1) ∂kronstate_vol3 = 𝐒³⁻ᵛ' * ∂y_pred / 6 - ∂state_vol_outer = zeros(Float64, n_state_vol) - ∂kronstate_vol_outer = zeros(Float64, length(kronstate_vol)) - fill_kron_adjoint!(∂kronstate_vol_outer, ∂state_vol_outer, ∂kronstate_vol3, kronstate_vol, state_vol) - ℒ.axpy!(1, ∂state_vol_outer, ∂state_vol) - fill_kron_adjoint!(∂state_vol, ∂state_vol, ∂kronstate_vol_outer, state_vol, state_vol) + compressed_kron³_power_vjp!(∂state_vol_cubic, ∂kronstate_vol3, state_vol) + ∂state_vol .+= ∂state_vol_cubic ℒ.mul!(∂𝐒¹ᵉ, ∂y_pred, final_shock', 1, 1) ℒ.mul!(∂final_shock, 𝐒¹ᵉ', ∂y_pred, 1, 1) @@ -15629,38 +15909,56 @@ function third_order_warmup_observation_and_jacobian_pullback!( ∂kron_shock_state2 = 𝐒³⁻ᵉ²' * ∂y_pred / 2 ∂kron_shock_state_from3e2 = zeros(Float64, length(kron_shock_state)) ∂state_vol_from3e2 = zeros(Float64, n_state_vol) - fill_kron_adjoint!(∂state_vol_from3e2, ∂kron_shock_state_from3e2, ∂kron_shock_state2, state_vol, kron_shock_state) - ℒ.axpy!(1, ∂state_vol_from3e2, ∂state_vol) - fill_kron_adjoint!(∂state_vol, ∂final_shock, ∂kron_shock_state_from3e2, state_vol, final_shock) + compressed_triple_shock_state_state_vjp!( + ∂final_shock, + ∂state_vol, + ∂kron_shock_state2, + final_shock, + state_vol, + shock_offset, + shock_state_state_indices, + 1; + index_rows = shock_state_state_rows) ℒ.mul!(∂𝐒²ᵉ, ∂y_pred, kron_shock_shock', 1/2, 1) ∂kron_shock_shock = 𝐒²ᵉ' * ∂y_pred / 2 - fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock, final_shock, final_shock) + compressed_kron²_power_vjp!(∂final_shock_pair, ∂kron_shock_shock, final_shock) + ∂final_shock .+= ∂final_shock_pair ℒ.mul!(∂𝐒³⁻ᵉ, ∂y_pred, kron_shock2_state', 1/2, 1) ∂kron_shock2_state = 𝐒³⁻ᵉ' * ∂y_pred / 2 ∂kron_shock_shock_from3e = zeros(Float64, length(kron_shock_shock)) ∂state_vol_from3e = zeros(Float64, n_state_vol) - fill_kron_adjoint!(∂state_vol_from3e, ∂kron_shock_shock_from3e, ∂kron_shock2_state, state_vol, kron_shock_shock) - ℒ.axpy!(1, ∂state_vol_from3e, ∂state_vol) - fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock_from3e, final_shock, final_shock) + compressed_triple_shock_shock_state_vjp!( + ∂final_shock, + ∂state_vol, + ∂kron_shock2_state, + final_shock, + state_vol, + shock_offset, + shock_shock_state_indices, + 1; + index_rows = shock_shock_state_rows) ℒ.mul!(∂𝐒³ᵉ, ∂y_pred, kron_shock3', 1/6, 1) ∂kron_shock3 = 𝐒³ᵉ' * ∂y_pred / 6 - ∂final_shock_outer = zeros(Float64, n_exo) - ∂kron_shock_shock_outer = zeros(Float64, length(kron_shock_shock)) - fill_kron_adjoint!(∂kron_shock_shock_outer, ∂final_shock_outer, ∂kron_shock3, kron_shock_shock, final_shock) - ℒ.axpy!(1, ∂final_shock_outer, ∂final_shock) - fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock_outer, final_shock, final_shock) + compressed_kron³_power_vjp!(∂final_shock_cubic, ∂kron_shock3, final_shock) + ∂final_shock .+= ∂final_shock_cubic ∂jac_y_state = zeros(Float64, n_obs, n_state_vol) if n_past > 0 @views ∂jac_y_state[:, 1:n_past] .+= ∂jac_seed * ds_dz' ℒ.mul!(∂ds_dz, view(𝐒¹⁻ᵛ, :, 1:n_past)', zeros(Float64, n_obs, n_z), 0, 0) - jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * jac_state2_term + 𝐒³⁻ᵛ * jac_state3_term + # d/ds of 𝐒³⁻ᵛ·compressed_kron³_power(s)/6 is 𝐒³⁻ᵛ·compressed_kron³(s,s,I)/2, + # matching third_order_warmup_observation_and_jacobian. + jac_y_state = 𝐒¹⁻ᵛ + 𝐒²⁻ᵛ * jac_state2_term + 𝐒³⁻ᵛ * (jac_state3_term / 2) jac_y_state += 𝐒²⁻ᵉ * kron_shock_I - jac_y_state += 𝐒³⁻ᵉ² * (ℒ.kron(kron_shock_I, state_vol) + ℒ.kron(kron_shock_state, I_state_vol)) / 2 - jac_y_state += 𝐒³⁻ᵉ * ℒ.kron(kron_shock_shock, I_state_vol) / 2 + jac_y_state += 𝐒³⁻ᵉ² * compressed_triple_shock_state_to_state( + final_shock, state_vol, shock_offset, shock_state_state_indices; + index_rows = shock_state_state_rows) + jac_y_state += 𝐒³⁻ᵉ * compressed_triple_shock_shock_state_to_state( + final_shock, state_vol, shock_offset, shock_shock_state_indices; + index_rows = shock_shock_state_rows) / 2 ℒ.mul!(∂ds_dz, view(jac_y_state, :, 1:n_past)', ∂jac_seed, 1, 1) end @@ -15672,37 +15970,42 @@ function third_order_warmup_observation_and_jacobian_pullback!( ∂kron_I_state = 𝐒²⁻ᵉ' * ∂jac_x fill_kron_adjoint_∂A!(∂kron_I_state, ∂state_vol, I_exo) - kron_I_state_state = ℒ.kron(kron_I_state, state_vol) - ℒ.mul!(∂𝐒³⁻ᵉ², ∂jac_x, kron_I_state_state', 1/2, 1) - ∂kron_I_state_state = 𝐒³⁻ᵉ²' * ∂jac_x / 2 - ∂kron_I_state_from3e2 = zeros(Float64, size(kron_I_state)) - ∂state_vol_from_jac3e2 = zeros(Float64, n_state_vol) - fill_kron_adjoint_matrix_vector_rhs!(∂kron_I_state_from3e2, ∂state_vol_from_jac3e2, ∂kron_I_state_state, kron_I_state, state_vol) - ℒ.axpy!(1, ∂state_vol_from_jac3e2, ∂state_vol) - fill_kron_adjoint_∂A!(∂kron_I_state_from3e2, ∂state_vol, I_exo) - - sym_kron_shock = (ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo)) / 2 + state_pair_to_shock = compressed_triple_state_pair_to_shock( + kronstate_vol, n_aug, shock_offset, n_exo, shock_state_state_indices; + index_rows = shock_state_state_rows) + ℒ.mul!(∂𝐒³⁻ᵉ², ∂jac_x, state_pair_to_shock', 1, 1) + compressed_triple_state_pair_to_shock_vjp!( + ∂state_vol, + 𝐒³⁻ᵉ²' * ∂jac_x, + state_vol, + n_aug, + shock_offset, + n_exo, + shock_state_state_indices; + index_rows = shock_state_state_rows) + + sym_kron_shock = compressed_kron²(final_shock, I_exo) ℒ.mul!(∂𝐒²ᵉ, ∂jac_x, sym_kron_shock', 1, 1) ∂sym_kron_shock = 𝐒²ᵉ' * ∂jac_x - accumulate_sym_kron_jacobian_pullback!(∂final_shock, ∂sym_kron_shock, final_shock) - - kron_I_kron_shock_state = ℒ.kron(I_exo, kron_shock_state) - kron_shock_kron_I_state = ℒ.kron(final_shock, kron_I_state) - ℒ.mul!(∂𝐒³⁻ᵉ, ∂jac_x, ((kron_I_kron_shock_state + kron_shock_kron_I_state) / 2)', 1, 1) - ∂jac_x3e = 𝐒³⁻ᵉ' * ∂jac_x / 2 - ∂kron_shock_state_from_jac3e = zeros(Float64, length(kron_shock_state)) - fill_kron_adjoint_∂A!(∂jac_x3e, ∂kron_shock_state_from_jac3e, I_exo) - fill_kron_adjoint!(∂state_vol, ∂final_shock, ∂kron_shock_state_from_jac3e, state_vol, final_shock) - ∂kron_I_state_from_jac3e = zeros(Float64, size(kron_I_state)) - ∂final_shock_from_jac3e = zeros(Float64, n_exo) - fill_kron_adjoint_matrix_vector!(∂kron_I_state_from_jac3e, ∂final_shock_from_jac3e, ∂jac_x3e, kron_I_state, final_shock) - ℒ.axpy!(1, ∂final_shock_from_jac3e, ∂final_shock) - fill_kron_adjoint_∂A!(∂kron_I_state_from_jac3e, ∂state_vol, I_exo) - - jac_x3_term = (ℒ.kron(ℒ.kron(I_exo, final_shock) + ℒ.kron(final_shock, I_exo), final_shock) + ℒ.kron(kron_shock_shock, I_exo)) / 6 + compressed_kron²_identity_vjp!(∂final_shock, ∂sym_kron_shock, final_shock) + + state_shock_to_shock = compressed_triple_state_shock_to_shock( + state_vol, final_shock, shock_offset, shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(∂𝐒³⁻ᵉ, ∂jac_x, state_shock_to_shock', 1, 1) + compressed_triple_state_shock_to_shock_vjp!( + ∂state_vol, + ∂final_shock, + 𝐒³⁻ᵉ' * ∂jac_x, + state_vol, + final_shock, + shock_offset, + shock_shock_state_indices; + index_rows = shock_shock_state_rows) + + jac_x3_term = compressed_kron³(final_shock, final_shock, I_exo) / 2 ℒ.mul!(∂𝐒³ᵉ, ∂jac_x, jac_x3_term', 1, 1) - ∂jac_x3_term = 𝐒³ᵉ' * ∂jac_x - accumulate_cubic_kron_jacobian_pullback!(∂final_shock, ∂jac_x3_term, final_shock, I_exo) + compressed_kron³_identity_vjp!(∂final_shock, 𝐒³ᵉ' * ∂jac_x / 2, final_shock) ∂𝐒¹⁻ᵛ .+= ∂jac_y_state @@ -15710,35 +16013,42 @@ function third_order_warmup_observation_and_jacobian_pullback!( ∂jac_state2_term = 𝐒²⁻ᵛ' * ∂jac_y_state accumulate_sym_kron_jacobian_pullback!(∂state_vol, ∂jac_state2_term, state_vol) - ℒ.mul!(∂𝐒³⁻ᵛ, ∂jac_y_state, jac_state3_term', 1, 1) - ∂jac_state3_term = 𝐒³⁻ᵛ' * ∂jac_y_state + # jac_y_state carries 𝐒³⁻ᵛ · jac_state3_term / 2, so both cotangents get the 1/2. + ℒ.mul!(∂𝐒³⁻ᵛ, ∂jac_y_state, jac_state3_term', 1/2, 1) + ∂jac_state3_term = (𝐒³⁻ᵛ' * ∂jac_y_state) ./ 2 accumulate_cubic_kron_jacobian_pullback!(∂state_vol, ∂jac_state3_term, state_vol, I_state_vol) ℒ.mul!(∂𝐒²⁻ᵉ, ∂jac_y_state, kron_shock_I', 1, 1) ∂kron_shock_I = 𝐒²⁻ᵉ' * ∂jac_y_state fill_kron_adjoint_∂B!(∂kron_shock_I, ∂final_shock, I_state_vol) - kron_shock_I_state = ℒ.kron(kron_shock_I, state_vol) - kron_shock_state_I = ℒ.kron(kron_shock_state, I_state_vol) - ℒ.mul!(∂𝐒³⁻ᵉ², ∂jac_y_state, ((kron_shock_I_state + kron_shock_state_I) / 2)', 1, 1) - ∂jac_y_3e2 = 𝐒³⁻ᵉ²' * ∂jac_y_state / 2 - ∂kron_shock_I_from_jac = zeros(Float64, size(kron_shock_I)) - ∂state_vol_from_jac = zeros(Float64, n_state_vol) - fill_kron_adjoint_matrix_vector_rhs!(∂kron_shock_I_from_jac, ∂state_vol_from_jac, ∂jac_y_3e2, kron_shock_I, state_vol) - ℒ.axpy!(1, ∂state_vol_from_jac, ∂state_vol) - fill_kron_adjoint_∂B!(∂kron_shock_I_from_jac, ∂final_shock, I_state_vol) - ∂I_dummy = zeros(Float64, size(I_state_vol)) - ∂kron_shock_state_from_jac = zeros(Float64, length(kron_shock_state)) - fill_kron_adjoint_matrix_vector!(∂I_dummy, ∂kron_shock_state_from_jac, ∂jac_y_3e2, I_state_vol, kron_shock_state) - fill_kron_adjoint!(∂state_vol, ∂final_shock, ∂kron_shock_state_from_jac, state_vol, final_shock) - - kron_shock_shock_I = ℒ.kron(kron_shock_shock, I_state_vol) - ℒ.mul!(∂𝐒³⁻ᵉ, ∂jac_y_state, (kron_shock_shock_I / 2)', 1, 1) - ∂jac_y_3e = 𝐒³⁻ᵉ' * ∂jac_y_state / 2 - fill!(∂I_dummy, 0) - ∂kron_shock_shock_from_jac = zeros(Float64, length(kron_shock_shock)) - fill_kron_adjoint_matrix_vector!(∂I_dummy, ∂kron_shock_shock_from_jac, ∂jac_y_3e, I_state_vol, kron_shock_shock) - fill_kron_adjoint!(∂final_shock, ∂final_shock, ∂kron_shock_shock_from_jac, final_shock, final_shock) + shock_state_state_jac = compressed_triple_shock_state_to_state( + final_shock, state_vol, shock_offset, shock_state_state_indices; + index_rows = shock_state_state_rows) + ℒ.mul!(∂𝐒³⁻ᵉ², ∂jac_y_state, shock_state_state_jac', 1, 1) + compressed_triple_shock_state_to_state_vjp!( + ∂final_shock, + ∂state_vol, + 𝐒³⁻ᵉ²' * ∂jac_y_state, + final_shock, + state_vol, + shock_offset, + shock_state_state_indices; + index_rows = shock_state_state_rows) + + shock_shock_state_jac = compressed_triple_shock_shock_state_to_state( + final_shock, state_vol, shock_offset, shock_shock_state_indices; + index_rows = shock_shock_state_rows) + ℒ.mul!(∂𝐒³⁻ᵉ, ∂jac_y_state, (shock_shock_state_jac / 2)', 1, 1) + compressed_triple_shock_shock_state_to_state_vjp!( + ∂final_shock, + 𝐒³⁻ᵉ' * ∂jac_y_state, + final_shock, + state_vol, + shock_offset, + shock_shock_state_indices, + 1/2; + index_rows = shock_shock_state_rows) @views ℒ.axpy!(1, ∂state_vol[1:n_past], ∂state) @views ℒ.axpy!(1, ∂final_shock, ∂warmup_x[(n_warm - 1) * n_exo + 1:n_warm * n_exo]) @@ -15749,13 +16059,13 @@ function third_order_warmup_observation_and_jacobian_pullback!( ds_before = ds_hist[i] aug_state = [state_before; 1.0; view(warmup_shocks, :, i)] - kronaug_state = ℒ.kron(aug_state, aug_state) - kronkronaug_state = ℒ.kron(kronaug_state, aug_state) + kronaug_state = compressed_kron²_power(aug_state) + kronkronaug_state = compressed_kron³_power(aug_state) - jac_aug2_term = (ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug)) / 2 - jac_aug3_term = (ℒ.kron(ℒ.kron(I_aug, aug_state) + ℒ.kron(aug_state, I_aug), aug_state) + ℒ.kron(kronaug_state, I_aug)) / 6 - Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2_term[:, 1:n_past] + 𝐒⁻³ * jac_aug3_term[:, 1:n_past] - Fu = 𝐒⁻¹[:, (n_past + 2):end] + 𝐒⁻² * jac_aug2_term[:, (n_past + 2):end] + 𝐒⁻³ * jac_aug3_term[:, (n_past + 2):end] + jac_aug2_term = compressed_kron²(aug_state, I_aug) + jac_aug3_term = compressed_kron³(aug_state, aug_state, I_aug) + Fs = 𝐒⁻¹[:, 1:n_past] + 𝐒⁻² * jac_aug2_term[:, 1:n_past] + 𝐒⁻³ * (jac_aug3_term[:, 1:n_past] / 2) + Fu = 𝐒⁻¹[:, (n_past + 2):end] + 𝐒⁻² * jac_aug2_term[:, (n_past + 2):end] + 𝐒⁻³ * (jac_aug3_term[:, (n_past + 2):end] / 2) ∂Fu = copy(view(∂ds_dz, :, block)) ∂Fs = ∂ds_dz * ds_before' @@ -15770,11 +16080,11 @@ function third_order_warmup_observation_and_jacobian_pullback!( @views ℒ.mul!(∂jac_aug2_term[:, 1:n_past], 𝐒⁻²', ∂Fs, 1, 0) @views ℒ.mul!(∂jac_aug2_term[:, (n_past + 2):n_aug], 𝐒⁻²', ∂Fu, 1, 0) - ℒ.mul!(∂𝐒⁻³, ∂Fs, jac_aug3_term[:, 1:n_past]', 1, 1) - ℒ.mul!(∂𝐒⁻³, ∂Fu, jac_aug3_term[:, (n_past + 2):end]', 1, 1) + ℒ.mul!(∂𝐒⁻³, ∂Fs, (jac_aug3_term[:, 1:n_past] / 2)', 1, 1) + ℒ.mul!(∂𝐒⁻³, ∂Fu, (jac_aug3_term[:, (n_past + 2):end] / 2)', 1, 1) ∂jac_aug3_term = zeros(Float64, size(jac_aug3_term)) - @views ℒ.mul!(∂jac_aug3_term[:, 1:n_past], 𝐒⁻³', ∂Fs, 1, 0) - @views ℒ.mul!(∂jac_aug3_term[:, (n_past + 2):n_aug], 𝐒⁻³', ∂Fu, 1, 0) + @views ℒ.mul!(∂jac_aug3_term[:, 1:n_past], 𝐒⁻³', ∂Fs, 1/2, 0) + @views ℒ.mul!(∂jac_aug3_term[:, (n_past + 2):n_aug], 𝐒⁻³', ∂Fu, 1/2, 0) ∂aug_state = 𝐒⁻¹' * ∂state ∂kronaug_state = 𝐒⁻²' * ∂state / 2 @@ -15785,10 +16095,16 @@ function third_order_warmup_observation_and_jacobian_pullback!( ℒ.mul!(∂𝐒⁻³, ∂state, kronkronaug_state', 1/6, 1) accumulate_sym_kron_jacobian_pullback!(∂aug_state, ∂jac_aug2_term, aug_state) - accumulate_cubic_kron_jacobian_pullback!(∂aug_state, ∂jac_aug3_term, aug_state, I_aug) + compressed_kron³_identity_vjp!(∂aug_state, ∂jac_aug3_term, aug_state) - fill_kron_adjoint!(∂aug_state, ∂kronaug_state, ∂kronkronaug_state, aug_state, kronaug_state) - fill_kron_adjoint!(∂aug_state, ∂aug_state, ∂kronaug_state, aug_state, aug_state) + # The *_power_vjp! helpers overwrite their output, so they need scratch + # vectors rather than the shared ∂aug_state accumulator. + ∂aug_state_cubic = zeros(Float64, n_aug) + compressed_kron³_power_vjp!(∂aug_state_cubic, ∂kronkronaug_state, aug_state) + ∂aug_state .+= ∂aug_state_cubic + ∂aug_state_pair = zeros(Float64, n_aug) + compressed_kron²_power_vjp!(∂aug_state_pair, ∂kronaug_state, aug_state) + ∂aug_state .+= ∂aug_state_pair copyto!(∂ds_dz, ∂ds_before) copyto!(∂state, 1, ∂aug_state, 1, n_past) @@ -15837,6 +16153,10 @@ function third_order_joint_warmup_solver_pullback!( I_aug::AbstractMatrix{Float64}, I_state_vol::AbstractMatrix{Float64}, I_exo::AbstractMatrix{Float64}; + shock_state_state_indices = nothing, + shock_state_state_rows = nothing, + shock_shock_state_indices = nothing, + shock_shock_state_rows = nothing, max_iter::Int = 80, tol::Float64 = 1e-10) warmup_iterations == 0 && return nothing @@ -15849,6 +16169,19 @@ function third_order_joint_warmup_solver_pullback!( ensure_inversion_buffers!(ws, n_exo, n_past; third_order = true) ensure_inversion_estimation_buffers!(ws, n_exo, n_obs; third_order = true) + n_state_vol = n_past + 1 + shock_offset = n_state_vol + # As above: the rrule passes the model's cached sets in, and these are only + # built when it did not. + if isnothing(shock_state_state_indices) + shock_state_state_indices, shock_state_state_rows = + compressed_shock_state_state_index_map(n_state_vol, n_exo) + end + if isnothing(shock_shock_state_indices) + shock_shock_state_indices, shock_shock_state_rows = + compressed_shock_shock_state_index_map(n_state_vol, n_exo) + end + if size(warmup_jac, 1) == size(warmup_jac, 2) n_z = length(warmup_x) ∂warmup_x_from_jac = zeros(Float64, n_z) @@ -15887,6 +16220,10 @@ function third_order_joint_warmup_solver_pullback!( I_aug, I_state_vol, I_exo, + shock_state_state_indices = shock_state_state_indices, + shock_state_state_rows = shock_state_state_rows, + shock_shock_state_indices = shock_shock_state_indices, + shock_shock_state_rows = shock_shock_state_rows, ) ∂z_total = copy(∂warmup_x) @@ -15928,6 +16265,10 @@ function third_order_joint_warmup_solver_pullback!( I_aug, I_state_vol, I_exo, + shock_state_state_indices = shock_state_state_indices, + shock_state_state_rows = shock_state_state_rows, + shock_shock_state_indices = shock_shock_state_indices, + shock_shock_state_rows = shock_shock_state_rows, ) return nothing @@ -16083,6 +16424,10 @@ function third_order_joint_warmup_solver_pullback!( I_aug, I_state_vol, I_exo, + shock_state_state_indices = shock_state_state_indices, + shock_state_state_rows = shock_state_state_rows, + shock_shock_state_indices = shock_shock_state_indices, + shock_shock_state_rows = shock_shock_state_rows, ) ℒ.axpy!(1, ∂state_eval, ∂state0) @@ -16125,6 +16470,10 @@ function third_order_joint_warmup_solver_pullback!( I_aug, I_state_vol, I_exo, + shock_state_state_indices = shock_state_state_indices, + shock_state_state_rows = shock_state_state_rows, + shock_shock_state_indices = shock_shock_state_indices, + shock_shock_state_rows = shock_shock_state_rows, ) ℒ.axpy!(1, ∂state_eval, ∂state0) @@ -16152,6 +16501,36 @@ function rrule(::typeof(calculate_loglikelihood), initial_covariance::Union{Symbol,AbstractMatrix{<:Real}} = :theoretical, opts::CalculationOptions = merge_calculation_options(), filter_algorithm::Symbol = :LagrangeNewton) + # The complete-data path is the missing-data path with an identical + # observable set at every period. Keeping one compressed third-order + # pullback avoids maintaining a second full-coordinate reverse pass. + # + # `obs_idx_per_t` holds *positions within the observable set* (rows of + # `data_in_deviations` and of the `cond_var_idx`-sliced solution matrices), + # not the global variable indices in `observables_index`. With complete data + # every period observes all of them, so the entry is `1:n_cond`. + all_obs = collect(1:length(observables_index)) + obs_idx_per_t = [copy(all_obs) for _ in axes(data_in_deviations, 2)] + llh, missing_pb = rrule(calculate_loglikelihood_with_missing, + Val(:inversion), Val(:third_order), + observables_index, 𝐒, data_in_deviations, + constants, state, workspaces, obs_idx_per_t; + warmup_iterations = warmup_iterations, + on_failure_loglikelihood = on_failure_loglikelihood, + presample_periods = presample_periods, + initial_covariance = initial_covariance, + opts = opts, + filter_algorithm = filter_algorithm) + pullback = ∂llh -> begin + tangents = missing_pb(∂llh) + (tangents[1], tangents[2], tangents[3], tangents[4], tangents[5], + tangents[6], tangents[7], tangents[8], tangents[9]) + end + return llh, pullback + + #= Legacy full-coordinate implementation retained in history only. The + complete-data third-order rrule delegates to the compressed missing-data + pullback above, so this code is intentionally unreachable. T = constants.post_model_macro ws = workspaces.inversion @@ -16293,7 +16672,11 @@ function rrule(::typeof(calculate_loglikelihood), ws, cc.I_aug, cc.I_state_vol, - cc.I_exo) + cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows) if !matched if opts.verbose println("Inversion filter rrule (3rd) failed during warmup") end @@ -16495,6 +16878,9 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # @timeit_debug timer "Main loop" begin + # Scratch for the compressed pair-basis KKT cotangent inside the loop. + pair_top = zeros(T.nExo * (T.nExo + 1) ÷ 2) + for i in reverse(axes(data_in_deviations,2)) # stt = 𝐒⁻¹ * aug_state[i] + 𝐒⁻² * ℒ.kron(aug_state[i], aug_state[i]) / 2 + 𝐒⁻³ * ℒ.kron(ℒ.kron(aug_state[i],aug_state[i]),aug_state[i]) / 6 ∂𝐒⁻¹ += ∂state * aug_state[i]' @@ -16581,7 +16967,7 @@ function rrule(::typeof(calculate_loglikelihood), fill_kron_adjoint!(∂x, ∂x, ∂kronxx, x[i], x[i]) - ℒ.kron!(kron_Ixx, ℒ.I(T.nExo), kronxx[i]) + compressed_kron³!(kron_Ixx, x[i], x[i], J) ℒ.mul!(∂𝐒ⁱ³ᵉ, ∂jacc, kron_Ixx', -3/2, 1) # find_shocks @@ -16608,20 +16994,25 @@ function rrule(::typeof(calculate_loglikelihood), copyto!(∂𝐒ⁱ, kronSλ) ℒ.axpy!(-1/2, ∂jacc, ∂𝐒ⁱ) - # ∂𝐒ⁱ²ᵉ += reshape(2 * ℒ.kron(S[1:T.nExo], ℒ.kron(x[i], λ[i])) - ℒ.kron(kronxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ²ᵉ)) - ℒ.kron!(kron_xλ, x[i], λ[i]) - ℒ.kron!(kron_S1_kxλ, S1, kron_xλ) - ℒ.kron!(kron_xx_S2, kronxx[i], S2) - ℒ.axpby!(-1, kron_xx_S2, 2, kron_S1_kxλ) - ∂𝐒ⁱ²ᵉ_tmp .+= reshape(kron_S1_kxλ, size(∂𝐒ⁱ²ᵉ_tmp)) + # Two preallocated rank-1 updates rather than a column loop. The + # loop form read `∂𝐒ⁱ²ᵉ_tmp[:, c]` back on the right of a `.+=`, which + # materialises the column: 240 B per call at nExo = 2 rising to + # 328 kB at nExo = 40, and 1.6-5.8x slower than `ger!`. + xᵢ = x[i] + pair_column = 0 + @inbounds for p in 1:T.nExo + for q in 1:p + pair_column += 1 + pair_top[pair_column] = p == q ? S1[p] * xᵢ[q] : S1[p] * xᵢ[q] + S1[q] * xᵢ[p] + end + end + ℒ.mul!(∂𝐒ⁱ²ᵉ_tmp, λ[i], pair_top', 2, 1) + ℒ.mul!(∂𝐒ⁱ²ᵉ_tmp, S2, kronxx[i]', -1, 1) ∂𝐒ⁱ²ᵉ = ∂𝐒ⁱ²ᵉ_tmp - # ∂𝐒ⁱ³ᵉ += reshape(3 * ℒ.kron(S[1:T.nExo], ℒ.kron(ℒ.kron(x[i], x[i]), λ[i])) - ℒ.kron(kronxxx[i], S[T.nExo+1:end]), size(∂𝐒ⁱ³ᵉ)) - ℒ.kron!(kron_xxλ, kronxx[i], λ[i]) - ℒ.kron!(kron_S1_kxxλ, S1, kron_xxλ) - ℒ.kron!(kron_xxx_S2, kronxxx[i], S2) - ℒ.axpby!(-1, kron_xxx_S2, 3, kron_S1_kxxλ) - ∂𝐒ⁱ³ᵉ .+= reshape(kron_S1_kxxλ, size(∂𝐒ⁱ³ᵉ)) + triple_Sx = compressed_kron³(x[i], x[i], S1) + triple_xxx = compressed_kron³_power(x[i]) + ∂𝐒ⁱ³ᵉ .+= 3 .* λ[i] .* triple_Sx' .- S2 .* triple_xxx' # 𝐒ⁱ = 𝐒¹ᵉ + 𝐒²⁻ᵉ * ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol) + 𝐒³⁻ᵉ² * ℒ.kron(ℒ.kron(ℒ.I(T.nExo), state¹⁻_vol), state¹⁻_vol) / 2 ∂kronstate¹⁻_vol *= 0 @@ -16744,6 +17135,10 @@ function rrule(::typeof(calculate_loglikelihood), cc.I_aug, cc.I_state_vol, cc.I_exo, + shock_state_state_indices = tc.shock_state_state_idxs, + shock_state_state_rows = tc.shock_state_state_rows, + shock_shock_state_indices = tc.shock_shock_state_idxs, + shock_shock_state_rows = tc.shock_shock_state_rows, ) ∂𝐒²ᵉ .+= ∂𝐒²ᵉ_warmup ∂𝐒ⁱ³ᵉ .+= 6 .* ∂𝐒³ᵉ_warmup @@ -16782,7 +17177,7 @@ function rrule(::typeof(calculate_loglikelihood), # end # timeit_debug # end # timeit_debug - return llh, inversion_filter_loglikelihood_pullback + =# end function rrule(::typeof(calculate_loglikelihood), @@ -18293,14 +18688,9 @@ end # Adjoint of Y = 𝐒₂ · kron(aug, aug) / 2 wrt aug, given d_new_state. # Returns (d_aug_contribution, d_𝐒₂_contribution). function accumulate_symmetric_kron_pullback!(d_aug::AbstractVector, d_kron::AbstractVector, aug::AbstractVector) - n_aug = length(aug) - α = one(eltype(d_aug)) - G = reshape(d_kron, n_aug, n_aug) - # `mul!(dest, A, x, α, β)` computes `dest = α * A * x + β * dest`, so the - # two calls accumulate the symmetric `G * aug + G' * aug` contribution - # directly into the preallocated cotangent. - ℒ.mul!(d_aug, G, aug, α, α) - ℒ.mul!(d_aug, transpose(G), aug, α, α) + contribution = similar(d_aug) + compressed_kron²_power_vjp!(contribution, d_kron, aug) + d_aug .+= contribution return d_aug end @@ -18316,11 +18706,8 @@ function quad_adjoint(𝐒₂, aug::AbstractVector, d_new_state::AbstractVector) fill!(d_aug, zero(T)) accumulate_symmetric_kron_pullback!(d_aug, g, aug) - kaa = Vector{T}(undef, n_aug^2) - ℒ.kron!(kaa, aug, aug) + kaa = compressed_kron²_power(aug) d_𝐒₂ = Matrix{T}(undef, size(𝐒₂)) - # The same scaled `g` and `kaa` buffers absorb the `/2` factor once, so the - # quadratic state and matrix cotangents are formed without extra temporaries. ℒ.mul!(d_𝐒₂, d_new_state, kaa', half, zero(T)) return d_aug, d_𝐒₂ end @@ -18617,19 +19004,14 @@ function filter_free_pullback_3rd( ℒ.mul!(d_kaug, 𝐒₂', d_new_state) ℒ.rmul!(d_kaug, half) # Cubic: 𝐒₃ * kron(kaug, aug) / 6 - kaug3 = Vector{eltype(d_aug)}(undef, length(kaug) * length(aug)) - ℒ.kron!(kaug3, kaug, aug) + kaug3 = compressed_kron³_power(aug) ℒ.mul!(d_𝐒₃, d_new_state, kaug3', sixth, one(eltype(d_𝐒₃))) d_kaug3 = Vector{eltype(d_aug)}(undef, length(kaug3)) ℒ.mul!(d_kaug3, 𝐒₃', d_new_state) ℒ.rmul!(d_kaug3, sixth) - # ∂kron(kaug, aug) → ∂kaug and ∂aug - # Using convention: kron(A,B)[(i-1)*nB+j] = A[i]*B[j]; - # reshape(d_k, nB, nA)[j,i] gives the gradient; d_A = mat' * B, d_B = mat * A. - d_kaug3_mat = reshape(d_kaug3, n_aug, n_aug^2) # nB=n_aug, nA=n_aug^2 - ℒ.mul!(d_kaug, d_kaug3_mat', aug, one(eltype(d_kaug)), one(eltype(d_kaug))) - ℒ.mul!(d_aug, d_kaug3_mat, kaug, one(eltype(d_aug)), one(eltype(d_aug))) - # ∂kron(aug, aug) → ∂aug (×2 via symmetric outer product) + d_aug_cubic = similar(d_aug) + compressed_kron³_power_vjp!(d_aug_cubic, d_kaug3, aug) + d_aug .+= d_aug_cubic accumulate_symmetric_kron_pullback!(d_aug, d_kaug, aug) # Split aug back d_past, d_shock = split_aug_adjoint(d_aug, npast, nExo) @@ -18693,31 +19075,25 @@ function filter_free_pullback_pruned3rd( d_aug₃ = similar(aug₃) ℒ.mul!(d_aug₃, 𝐒₁', d_new_3) # 𝐒₂ * kron(aug₁̂, aug₂) (no /2 factor here) - k12 = Vector{eltype(d_aug₁)}(undef, length(aug₁̂) * length(aug₂)) - ℒ.kron!(k12, aug₁̂, aug₂) + k12 = compressed_kron²(aug₁̂, aug₂) ℒ.mul!(d_𝐒₂, d_new_3, k12', one(eltype(d_𝐒₂)), one(eltype(d_𝐒₂))) d_k12 = Vector{eltype(d_aug₁)}(undef, length(k12)) ℒ.mul!(d_k12, 𝐒₂', d_new_3) - # reshape(d_k12, len(B)=n_aug, len(A)=n_aug); d_A=mat'*B, d_B=mat*A - d_k12_mat = reshape(d_k12, n_aug, n_aug) d_aug₁̂ = similar(aug₁̂) - ℒ.mul!(d_aug₁̂, d_k12_mat', aug₂) - ℒ.mul!(d_aug₂, d_k12_mat, aug₁̂, one(eltype(d_aug₂)), one(eltype(d_aug₂))) + d_aug₂_from_cross = similar(d_aug₂) + compressed_kron²_vjp!(d_aug₁̂, d_aug₂_from_cross, d_k12, aug₁̂, aug₂) + ℒ.axpy!(one(eltype(d_aug₂)), d_aug₂_from_cross, d_aug₂) # 𝐒₃ * kron(kaug₁, aug₁) / 6 sixth = one(eltype(d_aug₁)) / 6 - kaug3 = Vector{eltype(d_aug₁)}(undef, length(kaug₁) * length(aug₁)) - ℒ.kron!(kaug3, kaug₁, aug₁) + kaug3 = compressed_kron³_power(aug₁) ℒ.mul!(d_𝐒₃, d_new_3, kaug3', sixth, one(eltype(d_𝐒₃))) d_kaug3 = Vector{eltype(d_aug₁)}(undef, length(kaug3)) ℒ.mul!(d_kaug3, 𝐒₃', d_new_3) ℒ.rmul!(d_kaug3, sixth) # kron(kaug₁, aug₁): A=kaug₁ (n²), B=aug₁ (n); reshape (n, n²) - d_kaug3_mat = reshape(d_kaug3, n_aug, n_aug^2) - d_kaug₁_from_3 = similar(kaug₁) - ℒ.mul!(d_kaug₁_from_3, d_kaug3_mat', aug₁) - ℒ.mul!(d_aug₁, d_kaug3_mat, kaug₁, one(eltype(d_aug₁)), one(eltype(d_aug₁))) - # ∂kron(aug₁, aug₁) → ∂aug₁ (symmetric) - accumulate_symmetric_kron_pullback!(d_aug₁, d_kaug₁_from_3, aug₁) + d_aug₁_cubic = similar(d_aug₁) + compressed_kron³_power_vjp!(d_aug₁_cubic, d_kaug3, aug₁) + d_aug₁ .+= d_aug₁_cubic # Combine aug₁̂ into aug₁: aug₁̂ shares past_idx and shock with aug₁, constant slot is 0 ℒ.axpy!(one(eltype(d_aug₁)), view(d_aug₁̂, 1:npast), view(d_aug₁, 1:npast)) ℒ.axpy!(one(eltype(d_aug₁)), view(d_aug₁̂, npast+2:npast+1+nExo), view(d_aug₁, npast+2:npast+1+nExo)) @@ -18862,15 +19238,14 @@ function filter_free_warmup_pullback_3rd( d_kaug = Vector{eltype(d_aug)}(undef, length(kaug)) ℒ.mul!(d_kaug, 𝐒₂', d_new_state) ℒ.rmul!(d_kaug, half) - kaug3 = Vector{eltype(d_aug)}(undef, length(kaug) * length(aug)) - ℒ.kron!(kaug3, kaug, aug) + kaug3 = compressed_kron³_power(aug) ℒ.mul!(d_𝐒₃, d_new_state, kaug3', sixth, one(eltype(d_𝐒₃))) d_kaug3 = Vector{eltype(d_aug)}(undef, length(kaug3)) ℒ.mul!(d_kaug3, 𝐒₃', d_new_state) ℒ.rmul!(d_kaug3, sixth) - d_kaug3_mat = reshape(d_kaug3, n_aug, n_aug^2) - ℒ.mul!(d_kaug, d_kaug3_mat', aug, one(eltype(d_kaug)), one(eltype(d_kaug))) - ℒ.mul!(d_aug, d_kaug3_mat, kaug, one(eltype(d_aug)), one(eltype(d_aug))) + d_aug_cubic = similar(d_aug) + compressed_kron³_power_vjp!(d_aug_cubic, d_kaug3, aug) + d_aug .+= d_aug_cubic accumulate_symmetric_kron_pullback!(d_aug, d_kaug, aug) d_past, d_shock = split_aug_adjoint(d_aug, npast, nExo) copyto!(view(d_shocks, :, t), d_shock) @@ -18916,27 +19291,23 @@ function filter_free_warmup_pullback_pruned3rd( ℒ.mul!(d_𝐒₁, d_new_3, aug₃', one(eltype(d_𝐒₁)), one(eltype(d_𝐒₁))) d_aug₃ = similar(aug₃) ℒ.mul!(d_aug₃, 𝐒₁', d_new_3) - k12 = Vector{eltype(d_aug₁)}(undef, length(aug₁̂) * length(aug₂)) - ℒ.kron!(k12, aug₁̂, aug₂) + k12 = compressed_kron²(aug₁̂, aug₂) ℒ.mul!(d_𝐒₂, d_new_3, k12', one(eltype(d_𝐒₂)), one(eltype(d_𝐒₂))) d_k12 = Vector{eltype(d_aug₁)}(undef, length(k12)) ℒ.mul!(d_k12, 𝐒₂', d_new_3) - d_k12_mat = reshape(d_k12, n_aug, n_aug) d_aug₁̂ = similar(aug₁̂) - ℒ.mul!(d_aug₁̂, d_k12_mat', aug₂) - ℒ.mul!(d_aug₂, d_k12_mat, aug₁̂, one(eltype(d_aug₂)), one(eltype(d_aug₂))) + d_aug₂_from_cross = similar(d_aug₂) + compressed_kron²_vjp!(d_aug₁̂, d_aug₂_from_cross, d_k12, aug₁̂, aug₂) + ℒ.axpy!(one(eltype(d_aug₂)), d_aug₂_from_cross, d_aug₂) sixth = one(eltype(d_aug₁)) / 6 - kaug3 = Vector{eltype(d_aug₁)}(undef, length(kaug₁) * length(aug₁)) - ℒ.kron!(kaug3, kaug₁, aug₁) + kaug3 = compressed_kron³_power(aug₁) ℒ.mul!(d_𝐒₃, d_new_3, kaug3', sixth, one(eltype(d_𝐒₃))) d_kaug3 = Vector{eltype(d_aug₁)}(undef, length(kaug3)) ℒ.mul!(d_kaug3, 𝐒₃', d_new_3) ℒ.rmul!(d_kaug3, sixth) - d_kaug3_mat = reshape(d_kaug3, n_aug, n_aug^2) - d_kaug₁_from_3 = similar(kaug₁) - ℒ.mul!(d_kaug₁_from_3, d_kaug3_mat', aug₁) - ℒ.mul!(d_aug₁, d_kaug3_mat, kaug₁, one(eltype(d_aug₁)), one(eltype(d_aug₁))) - accumulate_symmetric_kron_pullback!(d_aug₁, d_kaug₁_from_3, aug₁) + d_aug₁_cubic = similar(d_aug₁) + compressed_kron³_power_vjp!(d_aug₁_cubic, d_kaug3, aug₁) + d_aug₁ .+= d_aug₁_cubic ℒ.axpy!(one(eltype(d_aug₁)), view(d_aug₁̂, 1:npast), view(d_aug₁, 1:npast)) ℒ.axpy!(one(eltype(d_aug₁)), view(d_aug₁̂, npast+2:npast+1+nExo), view(d_aug₁, npast+2:npast+1+nExo)) d_past₁, d_shock = split_aug_adjoint(d_aug₁, npast, nExo) @@ -19217,12 +19588,11 @@ function rrule(::typeof(get_loglikelihood), elseif algorithm == :second_order 𝐒₁_full = Matrix(𝐒[1]) - 𝐒₂_full = Matrix(𝐒[2]) nVars_full = size(𝐒₁_full, 1) ncols₁ = size(𝐒₁_full, 2) - ncols₂ = size(𝐒₂_full, 2) + ncols₂ = size(𝐒[2], 2) 𝐒₁ = 𝐒₁_full[needed, :] - 𝐒₂ = 𝐒₂_full[needed, :] + 𝐒₂ = 𝐒[2][needed, :] warmup_intermediates = Vector{NamedTuple{(:aug,), Tuple{Vector{R}}}}(undef, n_warm) intermediates = Vector{NamedTuple{(:aug, :new_state, :residual, :obs_idx), Tuple{Vector{R}, Vector{R}, Vector{R}, Vector{Int}}}}(undef, nT) @@ -19230,12 +19600,12 @@ function rrule(::typeof(get_loglikelihood), @inbounds for t in 1:n_warm aug = vcat(cur_state[past_in_needed], one(R), Vector{R}(aligned_shocks[:, t])) warmup_intermediates[t] = (; aug = aug) - cur_state = 𝐒₁ * aug + (𝐒₂ * kron(aug, aug)) ./ R(2) + cur_state = 𝐒₁ * aug + (𝐒₂ * compressed_kron²_power(aug)) ./ R(2) end @inbounds for t in 1:nT idx = obs_idx_per_t[t] aug = vcat(cur_state[past_in_needed], one(R), Vector{R}(aligned_shocks[:, n_warm + t])) - new_state = 𝐒₁ * aug + (𝐒₂ * kron(aug, aug)) ./ R(2) + new_state = 𝐒₁ * aug + (𝐒₂ * compressed_kron²_power(aug)) ./ R(2) residual = data_in_deviations[idx, t] - new_state[obs_in_needed[idx]] llh += filter_free_obs_logpdf(residual, period_me_std(aligned_me_std, idx, t)) intermediates[t] = (; aug = aug, new_state = new_state, residual = residual, obs_idx = idx) @@ -19302,12 +19672,11 @@ function rrule(::typeof(get_loglikelihood), elseif algorithm == :pruned_second_order 𝐒₁_full = Matrix(𝐒[1]) - 𝐒₂_full = Matrix(𝐒[2]) nVars_full = size(𝐒₁_full, 1) ncols₁ = size(𝐒₁_full, 2) - ncols₂ = size(𝐒₂_full, 2) + ncols₂ = size(𝐒[2], 2) 𝐒₁ = 𝐒₁_full[needed, :] - 𝐒₂ = 𝐒₂_full[needed, :] + 𝐒₂ = 𝐒[2][needed, :] warmup_intermediates = Vector{NamedTuple{(:aug₁, :aug₂), Tuple{Vector{R}, Vector{R}}}}(undef, n_warm) intermediates = Vector{NamedTuple{(:aug₁, :aug₂, :new_state, :residual, :obs_idx), Tuple{Vector{R}, Vector{R}, Vector{Vector{R}}, Vector{R}, Vector{Int}}}}(undef, nT) @@ -19317,7 +19686,7 @@ function rrule(::typeof(get_loglikelihood), aug₁ = vcat(cur_state[1][past_in_needed], one(R), ϵ) aug₂ = vcat(cur_state[2][past_in_needed], zero(R), zeros(R, nExo)) warmup_intermediates[t] = (; aug₁ = aug₁, aug₂ = aug₂) - cur_state = [𝐒₁ * aug₁, 𝐒₁ * aug₂ + (𝐒₂ * kron(aug₁, aug₁)) ./ R(2)] + cur_state = [𝐒₁ * aug₁, 𝐒₁ * aug₂ + (𝐒₂ * compressed_kron²_power(aug₁)) ./ R(2)] end @inbounds for t in 1:nT idx = obs_idx_per_t[t] @@ -19325,7 +19694,7 @@ function rrule(::typeof(get_loglikelihood), aug₁ = vcat(cur_state[1][past_in_needed], one(R), ϵ) aug₂ = vcat(cur_state[2][past_in_needed], zero(R), zeros(R, nExo)) new1 = 𝐒₁ * aug₁ - new2 = 𝐒₁ * aug₂ + (𝐒₂ * kron(aug₁, aug₁)) ./ R(2) + new2 = 𝐒₁ * aug₂ + (𝐒₂ * compressed_kron²_power(aug₁)) ./ R(2) new_state = [new1, new2] residual = data_in_deviations[idx, t] - (new1[obs_in_needed[idx]] + new2[obs_in_needed[idx]]) llh += filter_free_obs_logpdf(residual, period_me_std(aligned_me_std, idx, t)) @@ -19393,30 +19762,30 @@ function rrule(::typeof(get_loglikelihood), elseif algorithm == :third_order 𝐒₁_full = Matrix(𝐒[1]) - 𝐒₂_full = Matrix(𝐒[2]) - 𝐒₃_full = Matrix(𝐒[3]) nVars_full = size(𝐒₁_full, 1) ncols₁ = size(𝐒₁_full, 2) - ncols₂ = size(𝐒₂_full, 2) - ncols₃ = size(𝐒₃_full, 2) + ncols₂ = size(𝐒[2], 2) + ncols₃ = size(𝐒[3], 2) 𝐒₁ = 𝐒₁_full[needed, :] - 𝐒₂ = 𝐒₂_full[needed, :] - 𝐒₃ = 𝐒₃_full[needed, :] + 𝐒₂ = 𝐒[2][needed, :] + 𝐒₃ = 𝐒[3][needed, :] warmup_intermediates = Vector{NamedTuple{(:aug, :kaug), Tuple{Vector{R}, Vector{R}}}}(undef, n_warm) intermediates = Vector{NamedTuple{(:aug, :kaug, :new_state, :residual, :obs_idx), Tuple{Vector{R}, Vector{R}, Vector{R}, Vector{R}, Vector{Int}}}}(undef, nT) cur_state = convert(Vector{R}, state)[needed] @inbounds for t in 1:n_warm aug = vcat(cur_state[past_in_needed], one(R), Vector{R}(aligned_shocks[:, t])) - kaug = kron(aug, aug) + kaug = compressed_kron²_power(aug) + kaug3 = compressed_kron³_power(aug) warmup_intermediates[t] = (; aug = aug, kaug = kaug) - cur_state = 𝐒₁ * aug + (𝐒₂ * kaug) ./ R(2) + (𝐒₃ * kron(kaug, aug)) ./ R(6) + cur_state = 𝐒₁ * aug + (𝐒₂ * kaug) ./ R(2) + (𝐒₃ * kaug3) ./ R(6) end @inbounds for t in 1:nT idx = obs_idx_per_t[t] aug = vcat(cur_state[past_in_needed], one(R), Vector{R}(aligned_shocks[:, n_warm + t])) - kaug = kron(aug, aug) - new_state = 𝐒₁ * aug + (𝐒₂ * kaug) ./ R(2) + (𝐒₃ * kron(kaug, aug)) ./ R(6) + kaug = compressed_kron²_power(aug) + kaug3 = compressed_kron³_power(aug) + new_state = 𝐒₁ * aug + (𝐒₂ * kaug) ./ R(2) + (𝐒₃ * kaug3) ./ R(6) residual = data_in_deviations[idx, t] - new_state[obs_in_needed[idx]] llh += filter_free_obs_logpdf(residual, period_me_std(aligned_me_std, idx, t)) intermediates[t] = (; aug = aug, kaug = kaug, new_state = new_state, residual = residual, obs_idx = idx) @@ -19485,15 +19854,13 @@ function rrule(::typeof(get_loglikelihood), else # :pruned_third_order 𝐒₁_full = Matrix(𝐒[1]) - 𝐒₂_full = Matrix(𝐒[2]) - 𝐒₃_full = Matrix(𝐒[3]) nVars_full = size(𝐒₁_full, 1) ncols₁ = size(𝐒₁_full, 2) - ncols₂ = size(𝐒₂_full, 2) - ncols₃ = size(𝐒₃_full, 2) + ncols₂ = size(𝐒[2], 2) + ncols₃ = size(𝐒[3], 2) 𝐒₁ = 𝐒₁_full[needed, :] - 𝐒₂ = 𝐒₂_full[needed, :] - 𝐒₃ = 𝐒₃_full[needed, :] + 𝐒₂ = 𝐒[2][needed, :] + 𝐒₃ = 𝐒[3][needed, :] warmup_intermediates = Vector{NamedTuple{(:aug₁, :aug₁̂, :aug₂, :aug₃, :kaug₁), Tuple{Vector{R}, Vector{R}, Vector{R}, Vector{R}, Vector{R}}}}(undef, n_warm) intermediates = Vector{NamedTuple{(:aug₁, :aug₁̂, :aug₂, :aug₃, :kaug₁, :new_state, :residual, :obs_idx), Tuple{Vector{R}, Vector{R}, Vector{R}, Vector{R}, Vector{R}, Vector{Vector{R}}, Vector{R}, Vector{Int}}}}(undef, nT) @@ -19506,11 +19873,11 @@ function rrule(::typeof(get_loglikelihood), aug₁̂ = vcat(cur_state[1][past_in_needed], zero(R), ϵ) aug₂ = vcat(cur_state[2][past_in_needed], zero(R), zeros(R, nExo)) aug₃ = vcat(cur_state[3][past_in_needed], zero(R), zeros(R, nExo)) - kaug₁ = kron(aug₁, aug₁) + kaug₁ = compressed_kron²_power(aug₁) warmup_intermediates[t] = (; aug₁ = aug₁, aug₁̂ = aug₁̂, aug₂ = aug₂, aug₃ = aug₃, kaug₁ = kaug₁) cur_state = [𝐒₁ * aug₁, 𝐒₁ * aug₂ + (𝐒₂ * kaug₁) ./ R(2), - 𝐒₁ * aug₃ + 𝐒₂ * kron(aug₁̂, aug₂) + (𝐒₃ * kron(kaug₁, aug₁)) ./ R(6)] + 𝐒₁ * aug₃ + 𝐒₂ * compressed_kron²(aug₁̂, aug₂) + (𝐒₃ * compressed_kron³_power(aug₁)) ./ R(6)] end @inbounds for t in 1:nT idx = obs_idx_per_t[t] @@ -19519,10 +19886,10 @@ function rrule(::typeof(get_loglikelihood), aug₁̂ = vcat(cur_state[1][past_in_needed], zero(R), ϵ) aug₂ = vcat(cur_state[2][past_in_needed], zero(R), zeros(R, nExo)) aug₃ = vcat(cur_state[3][past_in_needed], zero(R), zeros(R, nExo)) - kaug₁ = kron(aug₁, aug₁) + kaug₁ = compressed_kron²_power(aug₁) new1 = 𝐒₁ * aug₁ new2 = 𝐒₁ * aug₂ + (𝐒₂ * kaug₁) ./ R(2) - new3 = 𝐒₁ * aug₃ + 𝐒₂ * kron(aug₁̂, aug₂) + (𝐒₃ * kron(kaug₁, aug₁)) ./ R(6) + new3 = 𝐒₁ * aug₃ + 𝐒₂ * compressed_kron²(aug₁̂, aug₂) + (𝐒₃ * compressed_kron³_power(aug₁)) ./ R(6) new_state = [new1, new2, new3] residual = data_in_deviations[idx, t] - (new1[obs_in_needed[idx]] + new2[obs_in_needed[idx]] + new3[obs_in_needed[idx]]) llh += filter_free_obs_logpdf(residual, period_me_std(aligned_me_std, idx, t)) diff --git a/src/steady_state/stochastic_steady_state.jl b/src/steady_state/stochastic_steady_state.jl index 26f1ae484..d71d1d3c7 100644 --- a/src/steady_state/stochastic_steady_state.jl +++ b/src/steady_state/stochastic_steady_state.jl @@ -81,13 +81,11 @@ function prepare_stochastic_steady_state_base_terms(parameters::Vector{M}, C) end - 𝐒₂ = (𝐒₂_raw * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} - 𝐒₁ = [𝐒₁[:,1:T.nPast_not_future_and_mixed] zeros(M, T.nVars) 𝐒₁[:,T.nPast_not_future_and_mixed+1:end]] - aug_state₁ = sparse([zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)]) + aug_state₁ = [zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)] tmp = collect(T.I_nPast - 𝐒₁[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed]) - rhs = collect((𝐒₂ * ℒ.kron(aug_state₁, aug_state₁) / 2)[T.past_not_future_and_mixed_idx]) + rhs = collect((𝐒₂_raw * compressed_kron²_power(aug_state₁) / 2)[T.past_not_future_and_mixed_idx]) if M === Float64 ensure_sss_tmp_lu_buffer!(𝓂.workspaces.second_order, tmp, rhs) @@ -159,7 +157,7 @@ function calculate_stochastic_steady_state(::Val{:second_order}, ∇₂ = sparse(𝓂.caches.hessian)::SparseMatrixCSC{M, Int} # was: dense_to_sparse 𝐒₁_raw = Matrix(𝓂.caches.first_order_solution_matrix)::Matrix{M} 𝐒₁ = [𝐒₁_raw[:,1:T.nPast_not_future_and_mixed] zeros(M, T.nVars) 𝐒₁_raw[:,T.nPast_not_future_and_mixed+1:end]] - 𝐒₂ = (sparse(𝓂.caches.second_order_solution) * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝓂.caches.second_order_solution)::SparseMatrixCSC{M, Int} return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, 𝐒₁, 𝐒₂ end end @@ -172,15 +170,19 @@ function calculate_stochastic_steady_state(::Val{:second_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0) end - # Expand compressed 𝐒₂_raw to full - 𝐒₂ = (𝐒₂_raw * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} + 𝐒₂ = 𝐒₂_raw - so = 𝓂.constants.second_order - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - A = 𝐒₁[:,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - B̂ = 𝐒₂[:,kron_s⁺_s⁺] + T = 𝓂.constants.post_model_macro + A = 𝐒₁[:,1:T.nPast_not_future_and_mixed] + n_state_aug = T.nPast_not_future_and_mixed + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + # Newton only uses past/mixed rows and the state/constant prefix of the + # compressed policy coefficients; shock columns are not unknowns here. + A_sss = 𝐒₁[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed] + B_sss = 𝐒₂[T.past_not_future_and_mixed_idx,1:n_state_pair] - SSSstates, converged = solve_stochastic_steady_state_newton(Val(:second_order), 𝐒₁, 𝐒₂, collect(SSSstates), 𝓂) + SSSstates, converged = solve_stochastic_steady_state_newton( + Val(:second_order), A_sss, B_sss, collect(SSSstates), 𝓂) if !converged if opts.verbose println("SSS not found") end @@ -188,7 +190,8 @@ function calculate_stochastic_steady_state(::Val{:second_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0) end - state = A * SSSstates + B̂ * ℒ.kron(vcat(SSSstates,1), vcat(SSSstates,1)) / 2 + aug_sss = [SSSstates; one(M); zeros(M, 𝓂.constants.post_model_macro.nExo)] + state = A * SSSstates + (𝐒₂ * compressed_kron²_power(aug_sss) / 2) result = all_SS + Vector{M}(state) if caching && M === Float64 @@ -216,7 +219,7 @@ function calculate_stochastic_steady_state(::Val{:pruned_second_order}, ∇₂ = sparse(𝓂.caches.hessian)::SparseMatrixCSC{M, Int} # was: dense_to_sparse 𝐒₁_raw = Matrix(𝓂.caches.first_order_solution_matrix)::Matrix{M} 𝐒₁ = [𝐒₁_raw[:,1:T.nPast_not_future_and_mixed] zeros(M, T.nVars) 𝐒₁_raw[:,T.nPast_not_future_and_mixed+1:end]] - 𝐒₂ = (sparse(𝓂.caches.second_order_solution) * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} # was: dense_to_sparse + 𝐒₂ = sparse(𝓂.caches.second_order_solution)::SparseMatrixCSC{M, Int} return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, 𝐒₁, 𝐒₂ end end @@ -229,13 +232,12 @@ function calculate_stochastic_steady_state(::Val{:pruned_second_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0) end - # Expand compressed 𝐒₂_raw to full - 𝐒₂ = (𝐒₂_raw * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} + 𝐒₂ = 𝐒₂_raw T = 𝓂.constants.post_model_macro - aug_state₁ = sparse([zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)]) + aug_state₁ = [zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)] state = 𝐒₁[:,1:T.nPast_not_future_and_mixed] * SSSstates + - 𝐒₂ * ℒ.kron(aug_state₁, aug_state₁) / 2 + 𝐒₂ * compressed_kron²_power(aug_state₁) / 2 result = all_SS + Vector{M}(state) @@ -259,25 +261,23 @@ function solve_stochastic_steady_state_newton(::Val{:second_order}, # Get cached computational constants constants = initialise_constants!(𝓂) - so = constants.second_order + so = ensure_computational_constants!(constants) T = constants.post_model_macro - s_in_s⁺ = so.s_in_s⁺ - s_in_s = so.s_in_s I_nPast = T.I_nPast - - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - - A = 𝐒₁[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed] - B = 𝐒₂[T.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂[T.past_not_future_and_mixed_idx,kron_s⁺_s⁺] + + # `𝐒₁`/`𝐒₂` arrive already restricted to the past/mixed rows and the + # state-and-constant prefix of the compressed columns; the shock columns are + # not unknowns of this Newton solve. See the call site above. + A = 𝐒₁ + B = 𝐒₂ + B̂ = B max_iters = 100 # SSS .= 𝐒₁ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + 𝐒₃ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 ℂ = 𝓂.workspaces.second_order nPast = length(x) + state_identity = @view so.I_state_vol[:, 1:nPast] ensure_sss_kron_buffers!(ℂ, nPast; third_order=false) x_aug = ℂ.x_aug_buf x_aug[end] = one(R) @@ -287,10 +287,10 @@ function solve_stochastic_steady_state_newton(::Val{:second_order}, for i in 1:max_iters copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I - I_nPast) - ℒ.kron!(kron_x_aug_xx, x_aug, x_aug) + compressed_kron²_power!(kron_x_aug_xx, x_aug) x̂ = A * x + B̂ * kron_x_aug_xx / 2 Δx = x̂ - x @@ -313,7 +313,7 @@ function solve_stochastic_steady_state_newton(::Val{:second_order}, # end # timeit_debug copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug_xx, x_aug, x_aug) + compressed_kron²_power!(kron_x_aug_xx, x_aug) return x, isapprox(A * x + B̂ * kron_x_aug_xx / 2, x, rtol = tol) end @@ -339,9 +339,9 @@ function calculate_stochastic_steady_state(::Val{:third_order}, ∇₃ = sparse(𝓂.caches.third_order_derivatives)::SparseMatrixCSC{M, Int} # was: dense_to_sparse 𝐒₁_raw = Matrix(𝓂.caches.first_order_solution_matrix)::Matrix{M} 𝐒₁ = [𝐒₁_raw[:,1:T.nPast_not_future_and_mixed] zeros(M, T.nVars) 𝐒₁_raw[:,T.nPast_not_future_and_mixed+1:end]] - 𝐒₂ = (sparse(𝓂.caches.second_order_solution) * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} # was: dense_to_sparse - 𝐒̂₃ = (sparse(𝓂.caches.third_order_solution) * 𝓂.constants.third_order.𝐔₃)::SparseMatrixCSC{M, Int} # was: dense_to_sparse - return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒̂₃ + 𝐒₂ = sparse(𝓂.caches.second_order_solution)::SparseMatrixCSC{M, Int} + 𝐒₃ = sparse(𝓂.caches.third_order_solution)::SparseMatrixCSC{M, Int} + return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃ end end @@ -353,8 +353,7 @@ function calculate_stochastic_steady_state(::Val{:third_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0) end - # Expand compressed 𝐒₂_raw to full - 𝐒₂ = (𝐒₂_raw * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} + 𝐒₂ = 𝐒₂_raw ∇₃ = calculate_third_order_derivatives(parameters, SS_and_pars, 𝓂.caches, 𝓂.functions.third_order_derivatives, 𝓂.workspaces, caching = caching) nPast = 𝓂.constants.post_model_macro.nPast_not_future_and_mixed @@ -377,24 +376,20 @@ function calculate_stochastic_steady_state(::Val{:third_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0) end - if length(𝓂.workspaces.third_order.Ŝ) == 0 || !(eltype(𝐒₃) == eltype(𝓂.workspaces.third_order.Ŝ)) - 𝓂.workspaces.third_order.Ŝ = 𝐒₃ * 𝓂.constants.third_order.𝐔₃ - else - ℒ.mul!(𝓂.workspaces.third_order.Ŝ, 𝐒₃, 𝓂.constants.third_order.𝐔₃) - end - - Ŝ = 𝓂.workspaces.third_order.Ŝ - 𝐒₃̂ = sparse_preallocated!(Ŝ, ℂ = 𝓂.workspaces.third_order)::SparseMatrixCSC{M, Int} - - so = 𝓂.constants.second_order - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - kron_s⁺_s⁺_s⁺ = so.kron_s⁺_s⁺_s⁺ - - A = 𝐒₁[:,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - B̂ = 𝐒₂[:,kron_s⁺_s⁺] - Ĉ = 𝐒₃̂[:,kron_s⁺_s⁺_s⁺] - - SSSstates, converged = solve_stochastic_steady_state_newton(Val(:third_order), 𝐒₁, 𝐒₂, 𝐒₃̂, collect(SSSstates), 𝓂) + T = 𝓂.constants.post_model_macro + A = 𝐒₁[:,1:T.nPast_not_future_and_mixed] + 𝐒₃ = sparse(𝐒₃)::SparseMatrixCSC{M, Int} + n_state_aug = T.nPast_not_future_and_mixed + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + n_state_triple = n_state_aug * (n_state_aug + 1) * (n_state_aug + 2) ÷ 6 + # Keep the Newton system restricted to the state/constant prefix of the + # compressed coefficients. The full compressed matrices remain available + # for the returned solution and final state evaluation below. + A_sss = 𝐒₁[T.past_not_future_and_mixed_idx,1:T.nPast_not_future_and_mixed] + B_sss = 𝐒₂[T.past_not_future_and_mixed_idx,1:n_state_pair] + C_sss = 𝐒₃[T.past_not_future_and_mixed_idx,1:n_state_triple] + SSSstates, converged = solve_stochastic_steady_state_newton( + Val(:third_order), A_sss, B_sss, C_sss, collect(SSSstates), 𝓂) if !converged if opts.verbose println("SSS not found") end @@ -402,7 +397,8 @@ function calculate_stochastic_steady_state(::Val{:third_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0) end - state = A * SSSstates + B̂ * ℒ.kron(vcat(SSSstates,1), vcat(SSSstates,1)) / 2 + Ĉ * ℒ.kron(vcat(SSSstates,1), ℒ.kron(vcat(SSSstates,1), vcat(SSSstates,1))) / 6 + aug_sss = [SSSstates; one(M); zeros(M, 𝓂.constants.post_model_macro.nExo)] + state = A * SSSstates + 𝐒₂ * compressed_kron²_power(aug_sss) / 2 + 𝐒₃ * compressed_kron³_power(aug_sss) / 6 result = all_SS + Vector{M}(state) @@ -412,7 +408,7 @@ function calculate_stochastic_steady_state(::Val{:third_order}, 𝓂.caches.valid_for.third_order_stochastic_steady_state = Float64.(parameters) end - return result, converged, SS_and_pars, solution_error, ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃̂ + return result, converged, SS_and_pars, solution_error, ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃ end function calculate_stochastic_steady_state(::Val{:pruned_third_order}, @@ -433,9 +429,9 @@ function calculate_stochastic_steady_state(::Val{:pruned_third_order}, ∇₃ = sparse(𝓂.caches.third_order_derivatives)::SparseMatrixCSC{M, Int} # was: dense_to_sparse 𝐒₁_raw = Matrix(𝓂.caches.first_order_solution_matrix)::Matrix{M} 𝐒₁ = [𝐒₁_raw[:,1:T.nPast_not_future_and_mixed] zeros(M, T.nVars) 𝐒₁_raw[:,T.nPast_not_future_and_mixed+1:end]] - 𝐒₂ = (sparse(𝓂.caches.second_order_solution) * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} # was: dense_to_sparse - 𝐒̂₃ = (sparse(𝓂.caches.third_order_solution) * 𝓂.constants.third_order.𝐔₃)::SparseMatrixCSC{M, Int} # was: dense_to_sparse - return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒̂₃ + 𝐒₂ = sparse(𝓂.caches.second_order_solution)::SparseMatrixCSC{M, Int} + 𝐒₃ = sparse(𝓂.caches.third_order_solution)::SparseMatrixCSC{M, Int} + return cached_sss, true, SS_and_pars, zero(M), ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃ end end @@ -447,8 +443,7 @@ function calculate_stochastic_steady_state(::Val{:pruned_third_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0) end - # Expand compressed 𝐒₂_raw to full - 𝐒₂ = (𝐒₂_raw * 𝓂.constants.second_order.𝐔₂)::SparseMatrixCSC{M, Int} + 𝐒₂ = 𝐒₂_raw ∇₃ = calculate_third_order_derivatives(parameters, SS_and_pars, 𝓂.caches, 𝓂.functions.third_order_derivatives, 𝓂.workspaces, caching = caching) nPast = 𝓂.constants.post_model_macro.nPast_not_future_and_mixed @@ -469,18 +464,9 @@ function calculate_stochastic_steady_state(::Val{:pruned_third_order}, return all_SS, false, SS_and_pars, solution_error, zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0), zeros(M,0,0), spzeros(M,0,0), spzeros(M,0,0) end - if length(𝓂.workspaces.third_order.Ŝ) == 0 || !(eltype(𝐒₃) == eltype(𝓂.workspaces.third_order.Ŝ)) - 𝓂.workspaces.third_order.Ŝ = 𝐒₃ * 𝓂.constants.third_order.𝐔₃ - else - ℒ.mul!(𝓂.workspaces.third_order.Ŝ, 𝐒₃, 𝓂.constants.third_order.𝐔₃) - end - - Ŝ = 𝓂.workspaces.third_order.Ŝ - 𝐒₃̂ = sparse_preallocated!(Ŝ, ℂ = 𝓂.workspaces.third_order)::SparseMatrixCSC{M, Int} - T = 𝓂.constants.post_model_macro - aug_state₁ = sparse([zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)]) - state = 𝐒₁[:,1:T.nPast_not_future_and_mixed] * SSSstates + 𝐒₂ * ℒ.kron(aug_state₁, aug_state₁) / 2 + aug_state₁ = [zeros(M, T.nPast_not_future_and_mixed); one(M); zeros(M, T.nExo)] + state = 𝐒₁[:,1:T.nPast_not_future_and_mixed] * SSSstates + 𝐒₂ * compressed_kron²_power(aug_state₁) / 2 result = all_SS + Vector{M}(state) @@ -489,43 +475,38 @@ function calculate_stochastic_steady_state(::Val{:pruned_third_order}, 𝓂.caches.valid_for.pruned_third_order_stochastic_steady_state = Float64.(parameters) end - return result, true, SS_and_pars, solution_error, ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃̂ + return result, true, SS_and_pars, solution_error, ∇₁, ∇₂, ∇₃, 𝐒₁, 𝐒₂, 𝐒₃ end function solve_stochastic_steady_state_newton(::Val{:third_order}, 𝐒₁::Matrix{Float64}, - 𝐒₂::AbstractSparseMatrix{Float64}, - 𝐒₃::AbstractSparseMatrix{Float64}, + 𝐒₂::AbstractMatrix{Float64}, + 𝐒₃::AbstractMatrix{Float64}, x::Vector{Float64}, 𝓂::ℳ; tol::AbstractFloat = 1e-14)::Tuple{Vector{Float64}, Bool} # Get cached computational constants - so = ensure_computational_constants!(𝓂.constants) T = 𝓂.constants.post_model_macro - s_in_s⁺ = so.s_in_s⁺ - s_in_s = so.s_in_s I_nPast = T.I_nPast + so = ensure_computational_constants!(𝓂.constants) - kron_s⁺_s⁺ = so.kron_s⁺_s⁺ - - kron_s⁺_s = so.kron_s⁺_s - - kron_s⁺_s⁺_s⁺ = so.kron_s⁺_s⁺_s⁺ - - kron_s_s⁺_s⁺ = so.kron_s_s⁺_s⁺ - - A = 𝐒₁[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,1:𝓂.constants.post_model_macro.nPast_not_future_and_mixed] - B = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s] - B̂ = 𝐒₂[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺] - C = 𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s_s⁺_s⁺] - Ĉ = 𝐒₃[𝓂.constants.post_model_macro.past_not_future_and_mixed_idx,kron_s⁺_s⁺_s⁺] + # Pre-sliced by the caller, as at second order. + A = 𝐒₁ + n_state_aug = T.nPast_not_future_and_mixed + 1 + n_state_pair = n_state_aug * (n_state_aug + 1) ÷ 2 + n_state_triple = n_state_aug * (n_state_aug + 1) * (n_state_aug + 2) ÷ 6 + B = 𝐒₂ + B̂ = B + C = 𝐒₃ + Ĉ = C max_iters = 100 # SSS .= 𝐒₁ * aug_state + 𝐒₂ * ℒ.kron(aug_state, aug_state) / 2 + 𝐒₃ * ℒ.kron(ℒ.kron(aug_state,aug_state),aug_state) / 6 ℂ = 𝓂.workspaces.third_order nPast = length(x) + state_identity = @view so.I_state_vol[:, 1:nPast] ensure_sss_kron_buffers!(ℂ, nPast; third_order=true) x_aug = ℂ.x_aug_buf x_aug[end] = 1.0 @@ -536,11 +517,11 @@ function solve_stochastic_steady_state_newton(::Val{:third_order}, for i in 1:max_iters copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) - ℒ.kron!(kron_x_kron, x_aug, kron_x_aug) + compressed_kron²_power!(kron_x_aug, x_aug) + compressed_kron³_power!(kron_x_kron, x_aug) - ℒ.kron!(kron_x_aug_I, x_aug, I_nPast) - ℒ.kron!(kron_x_kron_I, kron_x_aug, I_nPast) + compressed_kron²!(kron_x_aug_I, x_aug, state_identity) + compressed_kron³!(kron_x_kron_I, x_aug, x_aug, state_identity) ∂x = (A + B * kron_x_aug_I + C * kron_x_kron_I / 2 - I_nPast) Δx = (A * x + B̂ * kron_x_aug / 2 + Ĉ * kron_x_kron / 6 - x) @@ -561,8 +542,8 @@ function solve_stochastic_steady_state_newton(::Val{:third_order}, end copyto!(x_aug, 1, x, 1, nPast) - ℒ.kron!(kron_x_aug, x_aug, x_aug) - ℒ.kron!(kron_x_kron, x_aug, kron_x_aug) + compressed_kron²_power!(kron_x_aug, x_aug) + compressed_kron³_power!(kron_x_kron, x_aug) return x, isapprox(A * x + B̂ * kron_x_aug / 2 + Ĉ * kron_x_kron / 6, x, rtol = tol) end diff --git a/src/structures.jl b/src/structures.jl index 5a652d1a5..5b277e178 100644 --- a/src/structures.jl +++ b/src/structures.jl @@ -337,6 +337,10 @@ mutable struct second_order_indices I_exo::Matrix{Float64} # I(nExo) reused by inversion warmup helpers I_state_vol::Matrix{Float64} # I(nPast+1) reused by inversion warmup helpers I_aug::Matrix{Float64} # I(nPast+1+nExo) reused by inversion warmup helpers + compressed_pair_index_map::Matrix{Int} # Pair-coordinate map for hot pullback loops + shockvar_cols::Vector{Int} # Compressed columns for shockvar_idxs + shock²_cols::Vector{Int} # Compressed columns for shock²_idxs + var_vol²_cols::Vector{Int} # Compressed columns for var_vol²_idxs # ========================================================================= # CONDITIONAL FORECAST CONSTANTS @@ -346,6 +350,9 @@ mutable struct second_order_indices var²_idxs::Vector{Int} # Variable² indices (no-vol: kron(s_in_s, s_in_s)) shockvar²_idxs::Vector{Int} # Shock × variable² indices shockvar_no_vol_idxs::Vector{Int} # Shock-variable cross indices (no-vol: kron(e_in_s⁺, s_in_s)) + var²_cols::Vector{Int} # Compressed columns for var²_idxs + shockvar²_cols::Vector{Int} # Compressed columns for shockvar²_idxs + shockvar_no_vol_cols::Vector{Int} # Compressed columns for shockvar_no_vol_idxs # ========================================================================= # MOMENT COMPUTATION CONSTANTS (model-constant values for moments.jl) @@ -430,7 +437,14 @@ mutable struct third_order_indices shockvar3_idxs::Vector{Int} # Shock × var indices (position 3) shockvar³2_idxs::Vector{Int} # Shock × var³ indices (2nd variant) shockvar³_idxs::Vector{Int} # Shock × var³ indices - I_exo2::SparseMatrixCSC{Float64, Int} # I(nExo^2) reused by inversion warmup helpers + shock_state_state_idxs::Vector{Int} # Sorted shock × state² compressed indices + shock_state_state_rows::Vector{Int} # Loop-order rows into shock_state_state_idxs + shock_shock_state_idxs::Vector{Int} # Sorted shock² × state compressed indices + shock_shock_state_rows::Vector{Int} # Loop-order rows into shock_shock_state_idxs + var_vol³_cols::Vector{Int} # Compressed columns for var_vol³_idxs + shock³_cols::Vector{Int} # Compressed columns for shock³_idxs + shockvar³2_cols::Vector{Int} # Compressed columns for shockvar³2_idxs + shockvar³_cols::Vector{Int} # Compressed columns for shockvar³_idxs # ========================================================================= # MOMENT COMPUTATION CONSTANTS @@ -1148,13 +1162,13 @@ mutable struct inversion_workspace{T <: Real} aug_state₁̂::Vector{T} # n_past+1+n_exo - hat state (vol=0) state²⁻_vol::Vector{T} # n_past+1 - second-order state with volatility slot # Third-order state kron buffers - kronstate_vol³::Vector{T} # (n_past+1)^3 - triple kron of state_vol - kron_buffer2ss::Vector{T} # n_past^2 - ℒ.kron(state₁, state₂) for pruned 3rd order - kron_buffer3sv::Matrix{T} # (n_exo * (n_past+1)^2, n_exo) - ℒ.kron(kron(J, state_vol), state_vol) - kron_buffer4sv::Matrix{T} # (n_exo^2 * (n_past+1), n_exo^2) - x_kron_II! scratch - kron_shock_state2::Vector{T} # n_exo * (n_past+1)^2 - ℒ.kron(kron_shock_state, state_vol) - kron_shock2_state::Vector{T} # n_exo^2 * (n_past+1) - ℒ.kron(kron_shock_shock, state_vol) - kronaug_state_aux::Vector{T} # (n_past+1+n_exo)^2 - auxiliary augmented-state kron scratch + kronstate_vol³::Vector{T} # compressed triple of state_vol + kron_buffer2ss::Vector{T} # compressed pair of state₁ and state₂ + kron_buffer3sv::Matrix{T} # shock × compressed state pair × shock + kron_buffer4sv::Matrix{T} # compressed shock pair × state + kron_shock_state2::Vector{T} # shock × compressed state pair + kron_shock2_state::Vector{T} # compressed shock pair × state + kronaug_state_aux::Vector{T} # compressed augmented-state pair scratch # Pullback buffers (for reverse-mode AD in rrule) ∂_tmp1::Matrix{T} # (n_exo, n_past + n_exo) @@ -1196,10 +1210,13 @@ and lazily (re)allocated by `ensure_particle_workspace!` — inside a sampler th likelihood is evaluated thousands of times at the same dimensions, so these buffers are allocated once for the whole run rather than once per evaluation. -Six `nVars × n_particles` and three `nExo × n_particles` matrices cover the -simultaneous needs of every variant: the bootstrap filter uses the fewest, the -tempered filter the most (ancestors, states, Metropolis proposals, and the -swap partners for each). +A cloud is stored as `nVars × n_particles` matrices — one per pruned state +component, so one matrix at first, second and third order, two at pruned second +order and three at pruned third order — which is what lets the whole swarm be +propagated with a handful of BLAS `gemm` calls. `pools` is a flat vector of such +matrices handed out in groups of `n_components` by `ensure_particle_pools!`; the +bootstrap filter needs the fewest groups, the tempered filter the most +(ancestors, states, Metropolis proposals, and the swap partners for each). """ mutable struct particle_workspace{T <: Real} # Dimensions (for reallocation checks) @@ -1207,13 +1224,8 @@ mutable struct particle_workspace{T <: Real} nExo::Int n_particles::Int - # nVars × n_particles state clouds - X::Matrix{T} - X2::Matrix{T} - Anc::Matrix{T} - Anc2::Matrix{T} - St::Matrix{T} - St2::Matrix{T} + # nVars × n_particles state-component buffers, handed out in groups + pools::Vector{Matrix{T}} # nExo × n_particles shock clouds E::Matrix{T} @@ -1348,12 +1360,12 @@ mutable struct higher_order_workspace{F <: Real, G <: AbstractFloat, H <: Real} # Dedicated FastLapackInterface LU workspace for the SSS pullback transpose solve fast_lu_ws_sss_pullback::FastLapackInterface.LUWs fast_lu_dims_sss_pullback::NTuple{2, Int} - # SSS Newton iter kron! buffers (Float64 path; shared by primal, rrule forward loop, and ForwardDiffExt) + # SSS Newton compressed-kron buffers (Float64 path; shared by primal, rrule forward loop, and ForwardDiffExt) x_aug_buf::Vector{F} # length nPast+1, holds [x; 1] - kron_x_aug_xx::Vector{F} # length (nPast+1)^2, holds kron(x_aug, x_aug) - kron_x_aug_x_kron::Vector{F} # length (nPast+1)^3, holds kron(x_aug, kron_x_aug); 3rd order only - kron_x_aug_I::Matrix{F} # size (nPast+1)*nPast × nPast, holds kron(x_aug, I_nPast) - kron_x_kron_I::Matrix{F} # size (nPast+1)^2*nPast × nPast, holds kron(kron_x_aug, I_nPast); 3rd order only + kron_x_aug_xx::Vector{F} # compressed pair terms of x_aug with itself + kron_x_aug_x_kron::Vector{F} # compressed triple terms of x_aug with itself; 3rd order only + kron_x_aug_I::Matrix{F} # compressed pair terms of x_aug with I_nPast + kron_x_kron_I::Matrix{F} # compressed triple terms of x_aug with itself and I_nPast; 3rd order only # ForwardDiff partials buffers for stochastic steady state (accessed via model struct) ∂x_second_order::Matrix{H} # For second order SSS partials ∂x_third_order::Matrix{H} # For third order SSS partials diff --git a/test/runtests.jl b/test/runtests.jl index 0d58ed59c..fdacd18a2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -56,6 +56,7 @@ elseif test_set == "higher_order_2" elseif test_set == "higher_order_3" include("test_higher_order_3.jl") elseif test_set == "basic" + include("test_compressed_kron.jl") include("test_basic.jl") elseif test_set == "system_prior_estimation" include("test_system_prior_estimation.jl") diff --git a/test/test_compressed_kron.jl b/test/test_compressed_kron.jl new file mode 100644 index 000000000..3b7dbddad --- /dev/null +++ b/test/test_compressed_kron.jl @@ -0,0 +1,369 @@ +using Test +using LinearAlgebra +using SparseArrays +using Random +using MacroModelling + +Random.seed!(1234) + +function pair_extractor(n) + rows = Int[] + cols = Int[] + vals = Int[] + row = 0 + for i in 1:n + for j in 1:i + row += 1 + push!(rows, row) + push!(cols, (i - 1) * n + j) + push!(vals, 1) + if i != j + push!(rows, row) + push!(cols, (j - 1) * n + i) + push!(vals, 1) + end + end + end + return sparse(rows, cols, vals, n * (n + 1) ÷ 2, n^2) +end + +function triple_extractor(n) + rows = Int[] + cols = Int[] + vals = Int[] + row = 0 + for i in 1:n + for j in 1:i + for k in 1:j + row += 1 + indices = unique(((i, j, k), (i, k, j), (j, i, k), + (j, k, i), (k, i, j), (k, j, i))) + for (a, b, c) in indices + push!(rows, row) + push!(cols, ((a - 1) * n + b - 1) * n + c) + push!(vals, 1) + end + end + end + end + return sparse(rows, cols, vals, n * (n + 1) * (n + 2) ÷ 6, n^3) +end + +@testset "compressed Kronecker vectors" begin + for n in 0:5 + a = collect(1.0:n) + b = collect(2.0:2.0:2n) + c = [-Float64(i) for i in 1:n] + U₂ = pair_extractor(n) + U₃ = triple_extractor(n) + + pair = MacroModelling.compressed_kron²(a, b) + triple = MacroModelling.compressed_kron³(a, b, c) + @test pair ≈ U₂ * kron(a, b) + @test triple ≈ U₃ * kron(kron(a, b), c) + + pair_out = similar(pair) + triple_out = similar(triple) + @test MacroModelling.compressed_kron²!(pair_out, a, b) === pair_out + @test MacroModelling.compressed_kron³!(triple_out, a, b, c) === triple_out + @test pair_out ≈ pair + @test triple_out ≈ triple + + pair_power_out = similar(pair) + triple_power_out = similar(triple) + @test MacroModelling.compressed_kron²_power!(pair_power_out, a) === pair_power_out + @test MacroModelling.compressed_kron³_power!(triple_power_out, a) === triple_power_out + @test pair_power_out ≈ U₂ * kron(a, a) + @test triple_power_out ≈ U₃ * kron(kron(a, a), a) + @test MacroModelling.compressed_kron²_power(a) ≈ pair_power_out + @test MacroModelling.compressed_kron³_power(a) ≈ triple_power_out + + same_triple_out = similar(triple) + @test MacroModelling.compressed_kron³_same!(same_triple_out, a) === same_triple_out + @test same_triple_out ≈ U₃ * kron(kron(a, a), a) + end + + a = [1.0, -2.0, 3.0] + @test MacroModelling.compressed_kron²(a, a) ≈ pair_extractor(3) * kron(a, a) + @test MacroModelling.compressed_kron³(a, a, a) ≈ triple_extractor(3) * kron(kron(a, a), a) +end + +@testset "compressed Kronecker argument order" begin + # The pair and triple products are fully symmetric in their vector + # arguments, so call sites are free to pass them in whatever order the + # available method wants. `find_shocks` and the inversion filter rely on + # this when they write `compressed_kron²!(buf, x, J)`. + for n in (1, 3, 6) + a = randn(n) + b = randn(n) + c = randn(n) + @test MacroModelling.compressed_kron²(a, b) == MacroModelling.compressed_kron²(b, a) + for perm in ((a, c, b), (b, a, c), (b, c, a), (c, a, b), (c, b, a)) + @test MacroModelling.compressed_kron³(a, b, c) ≈ MacroModelling.compressed_kron³(perm...) + end + end + + # And the vector-against-matrix form is the column-wise vector form. + n = 4 + a = randn(n) + B = randn(n, 3) + out = MacroModelling.compressed_kron²(a, B) + for j in axes(B, 2) + @test out[:, j] ≈ MacroModelling.compressed_kron²(a, B[:, j]) + end +end + +@testset "compressed power derivative weights" begin + # d/dx compressed_kron²_power(x) = 2·compressed_kron²(x, dx) and + # d/dx compressed_kron³_power(x) = 3·compressed_kron³(x, x, dx). These are + # the factors that turn the forward Taylor weights 1/2 and 1/6 into 1 and + # 1/2 in every Jacobian and pullback built on the compressed basis. + for n in (3, 5, 8) + x = randn(n) + dx = randn(n) + h = 1e-6 + fd² = (MacroModelling.compressed_kron²_power(x .+ h .* dx) .- + MacroModelling.compressed_kron²_power(x .- h .* dx)) ./ (2h) + fd³ = (MacroModelling.compressed_kron³_power(x .+ h .* dx) .- + MacroModelling.compressed_kron³_power(x .- h .* dx)) ./ (2h) + @test 2 .* MacroModelling.compressed_kron²(x, dx) ≈ fd² rtol = 1e-6 + @test 3 .* MacroModelling.compressed_kron³(x, x, dx) ≈ fd³ rtol = 1e-6 + end +end + +@testset "column-wise compressed Kronecker" begin + # The particle filters carry one column per particle; the `_columns!` + # helpers must agree with the vector kernels column by column. + n, N = 5, 7 + A = randn(n, N) + B = randn(n, N) + pair² = zeros(n * (n + 1) ÷ 2, N) + pairAB = zeros(n * (n + 1) ÷ 2, N) + cube = zeros(n * (n + 1) * (n + 2) ÷ 6, N) + MacroModelling.compressed_kron²_power_columns!(pair², A) + MacroModelling.compressed_kron²_columns!(pairAB, A, B) + MacroModelling.compressed_kron³_power_columns!(cube, A) + for j in 1:N + @test pair²[:, j] ≈ MacroModelling.compressed_kron²_power(A[:, j]) + @test pairAB[:, j] ≈ MacroModelling.compressed_kron²(A[:, j], B[:, j]) + @test cube[:, j] ≈ MacroModelling.compressed_kron³_power(A[:, j]) + end +end + +@testset "compressed Kronecker power edge cases" begin + empty = Float64[] + @test isempty(MacroModelling.compressed_kron²_power(empty)) + @test isempty(MacroModelling.compressed_kron³_power(empty)) + @test MacroModelling.compressed_kron²_power!(Float64[], empty) isa Vector{Float64} + @test MacroModelling.compressed_kron³_power!(Float64[], empty) isa Vector{Float64} +end + +@testset "compressed Kronecker analytical VJPs" begin + n = 4 + a = [0.4, -1.1, 0.7, 1.3] + b = [-0.3, 0.8, 1.2, -0.5] + c = [0.9, -0.6, 0.2, 1.4] + pair_cotangent = randn(n * (n + 1) ÷ 2) + triple_cotangent = randn(n * (n + 1) * (n + 2) ÷ 6) + da = zeros(n); db = zeros(n) + MacroModelling.compressed_kron²_vjp!(da, db, pair_cotangent, a, b) + eps_fd = 1e-7 + @test dot(da, c) ≈ (dot(pair_cotangent, MacroModelling.compressed_kron²(a .+ eps_fd .* c, b)) - + dot(pair_cotangent, MacroModelling.compressed_kron²(a .- eps_fd .* c, b))) / (2eps_fd) + @test dot(db, c) ≈ (dot(pair_cotangent, MacroModelling.compressed_kron²(a, b .+ eps_fd .* c)) - + dot(pair_cotangent, MacroModelling.compressed_kron²(a, b .- eps_fd .* c))) / (2eps_fd) + + d3a = zeros(n); d3b = zeros(n); d3c = zeros(n) + MacroModelling.compressed_kron³_vjp!(d3a, d3b, d3c, triple_cotangent, a, b, c) + @test dot(d3a, c) ≈ (dot(triple_cotangent, MacroModelling.compressed_kron³(a .+ eps_fd .* c, b, c)) - + dot(triple_cotangent, MacroModelling.compressed_kron³(a .- eps_fd .* c, b, c))) / (2eps_fd) + + same_vjp = zeros(n) + MacroModelling.compressed_kron³_power_vjp!(same_vjp, triple_cotangent, a) + @test dot(same_vjp, c) ≈ (dot(triple_cotangent, MacroModelling.compressed_kron³(a .+ eps_fd .* c, a .+ eps_fd .* c, a .+ eps_fd .* c)) - + dot(triple_cotangent, MacroModelling.compressed_kron³(a .- eps_fd .* c, a .- eps_fd .* c, a .- eps_fd .* c))) / (2eps_fd) + + pair_power_vjp = zeros(n) + MacroModelling.compressed_kron²_power_vjp!(pair_power_vjp, pair_cotangent, a) + @test dot(pair_power_vjp, c) ≈ (dot(pair_cotangent, MacroModelling.compressed_kron²(a .+ eps_fd .* c, a .+ eps_fd .* c)) - + dot(pair_cotangent, MacroModelling.compressed_kron²(a .- eps_fd .* c, a .- eps_fd .* c))) / (2eps_fd) + + identity_cotangent = randn(n * (n + 1) * (n + 2) ÷ 6, n) + identity_vjp = zeros(n) + MacroModelling.compressed_kron³_identity_vjp!(identity_vjp, identity_cotangent, a) + @test dot(identity_vjp, c) ≈ (sum(identity_cotangent .* MacroModelling.compressed_kron³(a .+ eps_fd .* c, a .+ eps_fd .* c, Matrix{Float64}(I, n, n))) - + sum(identity_cotangent .* MacroModelling.compressed_kron³(a .- eps_fd .* c, a .- eps_fd .* c, Matrix{Float64}(I, n, n)))) / (2eps_fd) +end + +@testset "compressed state-update equivalence" begin + n = 5 + m = 3 + U₂ = pair_extractor(n) + U₃ = triple_extractor(n) + S₂ = randn(m, size(U₂, 1)) + S₃ = randn(m, size(U₃, 1)) + a = randn(n) + b = randn(n) + + @test S₂ * MacroModelling.compressed_kron²(a, a) / 2 ≈ + (S₂ * U₂) * kron(a, a) / 2 + @test S₂ * MacroModelling.compressed_kron²(a, b) ≈ + (S₂ * U₂) * kron(a, b) + @test S₃ * MacroModelling.compressed_kron³(a, a, a) / 6 ≈ + (S₃ * U₃) * kron(kron(a, a), a) / 6 +end + +@testset "compressed directional derivative multiplicities" begin + n = 5 + a = randn(n) + da = randn(n) + a₀ = randn(n) + a₂ = randn(n) + da₂ = randn(n) + U₂ = pair_extractor(n) + U₃ = triple_extractor(n) + + # The two symmetric pair permutations reduce to one compressed kernel. + full_pair_derivative = U₂ * (kron(da, a) + kron(a, da)) / 2 + @test MacroModelling.compressed_kron²(da, a) ≈ full_pair_derivative + + # The three symmetric cubic permutations reduce to one kernel with 1/2 + # after the Taylor coefficient 1/6 is applied. + full_triple_derivative = U₃ * ( + kron(kron(da, a), a) + kron(kron(a, da), a) + kron(kron(a, a), da)) / 6 + @test MacroModelling.compressed_kron³(da, a, a) / 2 ≈ full_triple_derivative + + # A mixed pair has no extra Taylor factor: both distinct directional + # terms remain present in compressed coordinates. + full_mixed_pair_derivative = U₂ * (kron(da, a₂) + kron(a₀, da₂)) + @test MacroModelling.compressed_kron²(da, a₂) + + MacroModelling.compressed_kron²(a₀, da₂) ≈ full_mixed_pair_derivative +end + +@testset "cached compressed cubic row maps" begin + n_state = 3 + n_exo = 2 + state = randn(n_state) + shock = randn(n_exo) + state_vol = [state; 1.0] + shock_offset = length(state_vol) + augmented = [state_vol; shock] + full_compressed = MacroModelling.compressed_kron³_power(augmented) + + shock_state_state_indices = sort!([MacroModelling.compressed_triple_index(shock_offset + q, i, j) + for q in 1:n_exo for i in 1:length(state_vol) for j in 1:i]) + shock_state_state_rows = MacroModelling.compressed_shock_state_state_rows( + shock_state_state_indices, shock_offset, length(state_vol), n_exo) + shock_state_state = MacroModelling.compressed_triple_shock_state_state( + shock, state_vol, shock_offset, shock_state_state_indices; + index_rows = shock_state_state_rows) + @test shock_state_state ≈ full_compressed[shock_state_state_indices] / 3 + + shock_shock_state_indices = sort!([MacroModelling.compressed_triple_index(shock_offset + i, + shock_offset + j, + k) + for i in 1:n_exo for j in 1:i for k in 1:length(state_vol)]) + shock_shock_state_rows = MacroModelling.compressed_shock_shock_state_rows( + shock_shock_state_indices, shock_offset, length(state_vol), n_exo) + shock_shock_state = MacroModelling.compressed_triple_shock_shock_state( + shock, state_vol, shock_offset, shock_shock_state_indices; + index_rows = shock_shock_state_rows) + @test shock_shock_state ≈ full_compressed[shock_shock_state_indices] / 3 + + state_to_pair = zeros(Float64, length(shock_shock_state_indices), n_exo * (n_exo + 1) ÷ 2) + @test MacroModelling.compressed_triple_state_to_pair!( + state_to_pair, state_vol, length(augmented), shock_offset, n_exo, + shock_shock_state_indices; index_rows = shock_shock_state_rows) === state_to_pair + @test state_to_pair ≈ MacroModelling.compressed_triple_state_to_pair( + state_vol, length(augmented), shock_offset, n_exo, shock_shock_state_indices; + index_rows = shock_shock_state_rows) + + state_pair_to_shock = zeros(Float64, length(shock_state_state_indices), n_exo) + @test MacroModelling.compressed_triple_state_pair_to_shock!( + state_pair_to_shock, MacroModelling.compressed_kron²_power(state_vol), + length(augmented), shock_offset, n_exo, shock_state_state_indices; + index_rows = shock_state_state_rows) === state_pair_to_shock + @test state_pair_to_shock ≈ MacroModelling.compressed_triple_state_pair_to_shock( + MacroModelling.compressed_kron²_power(state_vol), length(augmented), shock_offset, + n_exo, shock_state_state_indices; index_rows = shock_state_state_rows) +end + +# The model's `third_order_indices` caches these sets, and a handful of pullback +# entry points rebuild them when they are called without the model in scope. Both +# go through the same two builders, so what has to hold is that those builders +# agree with the sorted set plus the binary-search row map the callers index with. +@testset "compressed cubic index-map builders" begin + for (n_state, n_exo) in ((1, 1), (3, 2), (5, 4), (12, 7)) + shock_offset = n_state + + indices, rows = MacroModelling.compressed_shock_state_state_index_map(n_state, n_exo) + @test issorted(indices) + @test allunique(indices) + @test indices == sort!([MacroModelling.compressed_triple_index(shock_offset + q, i, j) + for q in 1:n_exo for i in 1:n_state for j in 1:i]) + @test rows == MacroModelling.compressed_shock_state_state_rows(indices, shock_offset, + n_state, n_exo) + + indices, rows = MacroModelling.compressed_shock_shock_state_index_map(n_state, n_exo) + @test issorted(indices) + @test allunique(indices) + @test indices == sort!([MacroModelling.compressed_triple_index(shock_offset + i, + shock_offset + j, k) + for i in 1:n_exo for j in 1:i for k in 1:n_state]) + @test rows == MacroModelling.compressed_shock_shock_state_rows(indices, shock_offset, + n_state, n_exo) + end +end + +@testset "compressed transition static audit" begin + transition_files = [ + "src/MacroModelling.jl", + "src/filter/find_shocks.jl", + "src/filter/inversion.jl", + "src/filter/particle.jl", + "src/get_functions.jl", + "src/occasionally_binding_constraints.jl", + "src/steady_state/stochastic_steady_state.jl", + ] + + strip_line_comments(source) = join((first(split(line, "#"; limit = 2)) for line in split(source, '\n')), '\n') + active_sources = Dict(path => strip_line_comments(read(joinpath(@__DIR__, "..", path), String)) + for path in transition_files) + + for (path, source) in active_sources + if path != "src/get_functions.jl" + @test !occursin(r"second_order_solution\s*\*\s*.*𝐔₂", source) + @test !occursin(r"third_order_solution\s*\*\s*.*𝐔₃", source) + end + @test !occursin("ℒ.kron(aug_state, aug_state)", source) + @test !occursin("ℒ.kron(aug_state₁, aug_state₁)", source) + @test !occursin("ℒ.kron(state_vol, state_vol)", source) + @test !occursin("ℒ.kron(state¹⁻_vol, state¹⁻_vol)", source) + @test !occursin("ℒ.kron(ℒ.kron(", source) + @test !occursin("compressed_pair_index(", source) + @test !occursin("compressed_pair_indices(", source) + @test !occursin("compressed_triple_indices(", source) + if path != "src/filter/particle.jl" + @test !occursin("searchsortedfirst(", source) + end + end + + # The only active U₂/U₃ conversions in the audited transition set are the + # documented full-coordinate public get_solution outputs. + public_solution = active_sources["src/get_functions.jl"] + @test count("𝐔₂", public_solution) == 2 + @test count("𝐔₃", public_solution) == 2 + + # Strip the two historical block-commented implementations before auditing + # reverse-mode code. Solver and moments rrules may still use full tensors, + # but active transition pullbacks must not construct same-state full k rons. + rrule_source = replace(read(joinpath(@__DIR__, "..", "src/rrules.jl"), String), r"(?s)#=.*?=#" => "") + rrule_source = strip_line_comments(rrule_source) + @test !occursin("ℒ.kron(aug_state, aug_state)", rrule_source) + @test !occursin("ℒ.kron(state_vol, state_vol)", rrule_source) + @test !occursin("ℒ.kron(ℒ.kron(", rrule_source) + @test !occursin("compressed_pair_index(", rrule_source) + @test !occursin("compressed_pair_indices(", rrule_source) + @test !occursin("compressed_triple_indices(", rrule_source) + @test !occursin("searchsortedfirst(", rrule_source) +end diff --git a/test/test_particle_filter.jl b/test/test_particle_filter.jl index ca93eef72..681381375 100644 --- a/test/test_particle_filter.jl +++ b/test/test_particle_filter.jl @@ -205,8 +205,8 @@ threw(f) = try; f(); false; catch; true; end tp_coarse = collect(get_estimated_variables(RBC_pf, data; filter = :tempered_particle, algorithm = :first_order, smooth = true, measurement_error = me^2, n_particles = 20_000, particle_rng = Random.Xoshiro(1), - tempering_target_ratio = 50.0, tempering_mh_steps = 3, - tempering_mh_scale = 0.9)) + particle_target_ratio = 50.0, particle_mh_steps = 3, + particle_mh_scale = 0.9)) @test tp_coarse != tp_sm @test all(isfinite, tp_coarse) end @@ -407,10 +407,10 @@ threw(f) = try; f(); false; catch; true; end end @testset "Filter selection and automatic measurement error" begin - # `:particle` is an alias for the bootstrap filter: same RNG ⇒ same value + # `:particle` is an alias for the guided filter: same RNG ⇒ same value @test get_loglikelihood(RBC_pf, data, p; filter = :particle, algorithm = :first_order, measurement_error = me^2, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) == - get_loglikelihood(RBC_pf, data, p; filter = :bootstrap_particle, algorithm = :first_order, + get_loglikelihood(RBC_pf, data, p; filter = :guided_particle, algorithm = :first_order, measurement_error = me^2, n_particles = 2_000, particle_rng = Random.Xoshiro(5)) # `:auto` leaves the Kalman filter without measurement error @test get_loglikelihood(RBC_pf, data, p; filter = :kalman) ==