Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a4b3207
Add particle filter (bootstrap, auxiliary, tempered) with measurement…
thorek1 Jul 22, 2026
fa9ba41
Optimise first-order particle filters: batched BLAS, allocation-free
thorek1 Jul 22, 2026
461c10b
Optimise higher-order particle filters: allocation-free transitions
thorek1 Jul 22, 2026
3768eb0
Add LowLevelParticleFilters.jl comparison benchmark
thorek1 Jul 22, 2026
2ce7d14
Make each particle filter its own `filter` value; smarter defaults
thorek1 Jul 26, 2026
173a88e
Support a full measurement-error covariance matrix
thorek1 Jul 26, 2026
066b578
Add a Filters documentation page; cite primary sources
thorek1 Jul 26, 2026
cf56f77
Extend particle filtering to the model-estimate and plotting entry po…
thorek1 Jul 26, 2026
db085d2
Add particle smoothing; finish the filter-argument cleanup
thorek1 Jul 26, 2026
2e51d65
Add particle-filter shock decomposition; fix broken plotting signature
thorek1 Jul 26, 2026
e8ed6c2
Support both pruned decomposition attributions for the particle filters
thorek1 Jul 26, 2026
95e7f37
Rename measurement error to `measurement_error` with covariance seman…
thorek1 Jul 26, 2026
f1bf908
Move particle-filter buffers into the model workspace
thorek1 Jul 26, 2026
7717ebf
Make the tempering controls act on the estimates path too
thorek1 Jul 26, 2026
a491d6e
Correct the filter comparison table for smoothing and correlated meas…
thorek1 Jul 26, 2026
b32b2d0
Make the correlated-measurement-error path allocation-free
thorek1 Jul 26, 2026
df31ddd
Fix the inversion filter's Jacobian term, which entered at half weight
thorek1 Jul 26, 2026
0622332
Add filter-equivalence tests anchored on the initial state covariance
thorek1 Jul 26, 2026
c937998
Pin the particle filter's initial-covariance timing convention
thorek1 Jul 27, 2026
446a575
Document measurement error, the initial covariance, and the filter-fr…
thorek1 Jul 27, 2026
1453901
Expose initial_covariance on the estimate functions; test state equiv…
thorek1 Jul 27, 2026
6344ea7
Add higher-order equivalence checks for the particle filters
thorek1 Jul 27, 2026
20ba81d
add nsss function to sw07
Jul 29, 2026
5758b2a
Merge branch 'particle-filter' of https://github.com/thorek1/MacroMod…
Jul 29, 2026
8ef6618
Keep higher-order state updates compressed (#312)
thorek1 Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]
104 changes: 104 additions & 0 deletions benchmark/particle_filter_llpf_comparison.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 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,
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
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]
Σ, _ = 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)
params = m.parameter_values
kal = get_loglikelihood(m, data(observables), params; filter = :kalman,
presample_periods = 0, initial_covariance = :theoretical,
measurement_error = me .^ 2)
println("\n==== $name (N=$N) ====")
println("Kalman+ME = ", round(kal, digits = 3))

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 = pf_filter,
algorithm = :first_order, presample_periods = 0, initial_covariance = :theoretical,
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")
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)
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading