MacroModelling.jl

repository·main·Indexed 40 days ago

https://github.com/thorek1/macromodelling.jl

A high-performance Julia package for developing, solving, and estimating Dynamic Stochastic General Equilibrium (DSGE) models. It supports first-, second-, and third-order perturbation solutions, occasionally binding constraints (OBC), and model estimation via gradient-based samplers or inversion filters. Designed for iterative research, it offers significant speed advantages over Dynare for higher-order perturbations and derivative construction.

Tokens
60.7K
Snippets
194
Records
224
Agent score
97%

What's inside MacroModelling.jl

  1. Overview of MacroModelling.jl capabilities

    main

    What is MacroModelling.jl?

    MacroModelling.jl is a Julia package designed for developing and solving Dynamic Stochastic General Equilibrium (DSGE) models. It is intended for use by central banks, regulators, graduate students, and researchers.

    Key Features

    • Model Parsing: Uses a time-indexed syntax (e.g., [0], [-1], [1]) to define variables and shocks.
    • Model Solving: Automatically solves models from equations and parameters.
    • Perturbation Solutions: Computes first-, second-, and third-order (pruned) solutions.
    • Constraints: Handles occasionally binding constraints (OBC).
    • Analysis: Computes Impulse Response Functions (IRFs), simulations, and conditional forecasts.
    • Estimation: Supports model estimation via gradient-based samplers (NUTS/HMC) or inversion filters.
    • Sensitivity Analysis: Differentiates solutions and moments with respect to parameters.

    Note on Timing: The package uses an end-of-period timing convention.

  2. Overview of MacroModelling.jl

    main

    What is MacroModelling.jl?

    MacroModelling.jl is a Julia package designed for developing and solving Dynamic Stochastic General Equilibrium (DSGE) models. It is optimized for macroeconomists, central bankers, and researchers who need to perform counterfactual analysis, explore economic mechanisms, and handle complex model features like forward-looking expectations, nonlinearity, and high dimensionality.

    Key Capabilities

    • Model Parsing: Uses a user-friendly syntax where variables are followed by time indices (e.g., [2], [1], [0], [-1], [-2]) and shocks are denoted by [x].
    • Solving: Calculates first, second, and third-order (pruned) perturbation solutions using symbolic derivatives. It can also handle occasionally binding constraints.
    • Analysis: Supports impulse response functions (IRFs), model simulation, conditional forecasting, and variance decompositions (including Shapley value allocations for higher-order terms).
    • Estimation: Supports estimation via the Kalman filter (first order) using gradient-based samplers (e.g., NUTS, HMC), or nonlinear estimation using the inversion filter or filter-free joint likelihood.
    • Differentiation: Fully compatible with Automatic Differentiation (AD) via ForwardDiff.jl (forward-mode) and Mooncake.jl or Zygote.jl (reverse-mode) for differentiating solutions, log-likelihoods, and moments with respect to parameters.
    • Dynamic Modification: Allows users to modify model and calibration equations in place (adding, removing, or updating) without re-running the initial @model or @parameters macros, maintaining a revision history similar to the TROLL workflow.
  3. Understand MacroModelling.jl speed benchmarks

    main

    MacroModelling.jl is benchmarked against Dynare (the standard DSGE toolbox) to compare runtime performance across different operating systems (Ubuntu, macOS, Windows) and CPU architectures.

    Key Performance Indicators

    • Model Size: Represented as states / variables. Larger models shift runtime costs toward linear-algebra (BLAS/LAPACK) operations.
    • Components: Benchmarks are split into specific steps:
      • Jacobian: First-order derivatives of model equations.
      • Hessian: Second-order derivatives.
      • First-order solve: Perturbation solution at the first order.
      • Second-order solve: Perturbation solution at the second order.
      • Third-order bundled: A combined timing for the full third-order pipeline (includes first-order solve, Hessian, second-order solve, third-order derivatives, and third-order solve).
    • Speedup: Expressed as Nx. 4.8x means MacroModelling.jl is 4.8 times faster than Dynare. 0.1x means Dynare is faster.

    Performance Summary

    • Derivative Construction: MacroModelling.jl shows massive speedups in building Jacobians and Hessians, often ranging from hundreds to thousands of times faster than Dynare on Linux and macOS.
    • Scaling: While absolute solve times increase with model size, MacroModelling.jl generally scales better than Dynare.
    • Windows Performance: On Windows, MacroModelling.jl is compared against Dynare running on MATLAB with Intel MKL (its fastest configuration). Even in this conservative comparison, MacroModelling.jl typically remains faster, especially in third-order bundled tasks.
  4. Understand the MacroModelling.jl repository structure

    main

    The repository is organized as follows:

    • src/: Core package code, including the main module, macros (@model, @parameters), and the user-facing API (get_functions.jl).
    • test/: The test suite.
    • models/: Example DSGE models.
    • docs/: Documentation generated via Documenter.jl.
    • benchmark/: Scripts for performance benchmarking.
    • ext/: Package extensions.
  5. Compare MacroModelling.jl performance with Dynare

    main

    When to use MacroModelling.jl

    • Iterative Work: If you are changing parameters interactively, running estimations, or calling solvers repeatedly, MacroModelling.jl is superior due to Julia's JIT compilation and the package's object-oriented design.
    • Higher-Order Perturbations: MacroModelling.jl is significantly faster for second and third-order solutions (often 40x to 115x faster than Dynare).
    • Gradient-Based Estimation: It integrates deeply with Julia's ecosystem of gradient-based samplers (NUTS, HMC).

    When to use Dynare

    • Single Runs: If you only need to run a model once and exit, Dynare (built on MATLAB) will likely deliver the first output sooner because it avoids Julia's initial JIT compilation overhead.

    Performance Note

    While the 'time-to-first-plot' might be longer in MacroModelling.jl due to compilation, subsequent runs are extremely fast. For large models, performance in first-order solves is largely determined by the underlying BLAS/LAPACK libraries.

  6. Calculate Generalized Impulse Response Functions (GIRFs)

    main

    Standard IRFs can be state-dependent in non-linear models or models with occasionally binding constraints (OBC). Generalized IRFs (GIRFs) mitigate this by simulating the model with and without the shock, taking the difference, and averaging over multiple draws to provide an average IRF over the model's ergodic set.

    To use GIRFs, set generalised_irf = true in plot_irf. Alternatively, you can use the plot_girf function, which is a convenient wrapper for plot_irf(..., generalised_irf = true).

    # Using plot_irf with the generalised_irf flag
    plot_irf(Gali_2015_chapter_3_obc,
        generalised_irf = true,
        shocks = :eps_z,
        variables = [:Y, :R, :Pi, :C],
        shock_size = 3)
    
    # Or using the dedicated wrapper
    plot_girf(Gali_2015_chapter_3_obc, shocks = :eps_z)
  7. How Shapley decomposition is implemented via path integrals

    main

    To avoid the $2^{n^e}$ computational cost of direct Shapley value evaluation, MacroModelling.jl uses the Aumann–Shapley path integral on the multilinear extension of the coalition value $V$.

    Instead of enumerating all subsets of shocks, the implementation evaluates a path integral along the diagonal of the unit cube $[0,1]^{n^e}$ using Gauss–Legendre quadrature.

    Computational Complexity

    The cost is reduced to approximately $\lceil k/2 \rceil \cdot n^e$ evaluations, where $k$ is the perturbation order:

    OrderAumann–Shapley path integral cost
    2$2 \cdot n^e$ Lyapunov solves
    3$3 \cdot n^e$ Lyapunov solves

    Convergence Safeguard

    For variance decomposition, the routine starts with a low-order Gauss–Legendre rule. If the relative Shapley-efficiency closure error (the maximum relative deviation of row sums from one) exceeds 1e-3, the routine automatically reruns with one additional node, up to a maximum of seven nodes.

  8. Configure IRF visualization with `plot_type`

    main

    The plot_type argument (default: :compare) determines how multiple scenarios or shocks are visualized when using plot_irf or plot_irf!.

    • :compare: Displays IRFs as separate lines. Use this to compare how different parameter values or model specifications affect the response.
    • :stack: Stacks IRFs on top of each other to show cumulative effects. This is useful for analyzing scenarios composed of multiple shock series, showing how each shock contributes to the total response (represented by a solid black line).
    # Using :stack to show cumulative impact of multiple shocks
    plot_irf(model, shocks = :eps_a)
    plot_irf!(model, shocks = :eps_nu, plot_type = :stack)
    
    # Using :compare to see effects of parameter changes
    plot_irf(model, parameters = (:β => 0.99,), shocks = :eps_a)
    plot_irf!(model, parameters = (:β => 0.95,), shocks = :eps_a, plot_type = :compare)
  9. Define a Bayesian model for estimation

    main

    To perform Bayesian estimation, you must combine your model with prior distributions using the Turing.jl framework.

    Workflow:

    1. Define Priors: Create an array of distributions (from the Distributions.jl package). For non-normal distributions like Gamma or Beta, use the μσ = true keyword to pass moments as parameters.
    2. Create Likelihood Function: Use a Turing.@model function. Inside this function:
      • Use Turing.product_distribution(prior_distributions) to assign priors to parameters.
      • Use the @addlogprob! macro combined with get_loglikelihood(model, data, parameters) to add the model's log-likelihood to the posterior.

    Note: Using product_distribution inside the macro is significantly faster for reverse-mode automatic differentiation than defining priors directly within the macro body.

    import Turing
    
    # 1. Define priors
    prior_distributions = [
        Beta(0.356, 0.02, μσ = true),           # alp
        Normal(0.0085, 0.003),                  # gam
        # ...
    ]
    
    # 2. Define the Turing model
    Turing.@model function FS2000_loglikelihood_function(prior_distributions, data, m; verbose = false)
        parameters ~ Turing.product_distribution(prior_distributions)
        Turing.@addlogprob! get_loglikelihood(m, data, parameters)
    end
  10. Modify model equations in place

    main

    You can edit a model after it has been defined using @model and @parameters without re-declaring the entire model. MacroModelling.jl re-runs the equation-processing pipeline and recomputes the non-stochastic steady state automatically after modifications. Each change is recorded in a chronological revision log.

    Target Selection

    Modifying functions accept the target equation as:

    • A 1-based index (based on the order returned by get_equations(𝓂)).
    • An Expr (e.g., :(lhs = rhs)).
    • A String (matching is canonical, so whitespace and parenthesisation are ignored).

    Batch Operations

    To avoid multiple re-solves, you can pass a Vector or Tuple of (target, new) pairs to update_equations!, or a vector of new equations to add_equation!.

    # Example of batch updates
    update_equations!(RBC, [
        (4, :(z[0] = 0.95 * z[-1] + std_z * eps_z[x])),
        (:(c[0] + k[0] = (1 - δ) * k[-1] + q[0]),
         :(c[0] + k[0] + g[0] = (1 - δ) * k[-1] + q[0])),
    ])
  11. How auxiliary variables are handled for leads and lags

    main

    The MacroModelling parser automatically creates auxiliary variables when the model contains leads or lags of a variable that span more than one period. This allows you to define complex moving averages or smoothed variables using the standard DSL syntax.

    For example, if you define a variable as an average of several periods, the parser handles the underlying mechanics of those lagged/lead terms.

    @model RBC_lead_lag begin
        # ... other equations ...
        z[0] = ρ * z[-1] + std_z * (eps_z[x-8] + eps_z[x-4] + eps_z[x+4] + eps_z_s[x])
        c̄⁻[0] = (c[0] + c[-1] + c[-2] + c[-3]) / 4
        c̄⁺[0] = (c[0] + c[1] + c[2] + c[3]) / 4
    end
  12. Orchestration heuristics for complex tasks

    main

    When performing non-trivial tasks (those involving 3+ steps or architectural choices), follow these heuristics:

    • Plan Mode: Always use plan mode for complex tasks. Include explicit verification steps in your plan, not just implementation steps. Re-plan immediately if initial assumptions fail.
    • Subagent Usage: For complex tasks, offload exploration or research to subagents. Ensure each subagent has exactly one focused goal.
    • Elegance Check: Before finalizing non-trivial changes, reassess if a cleaner root-cause solution exists. Avoid over-engineering simple fixes.
    • Autonomous Bug-Fixing: Aim to drive issue resolution end-to-end. Use logs, errors, and tests to iterate quickly toward a verified result without requiring manual intervention.