Skip to content

Add balanced growth path support for first-order models - #304

Open
ylevch wants to merge 14 commits into
thorek1:mainfrom
ylevch:feature/balanced-growth-path
Open

Add balanced growth path support for first-order models#304
ylevch wants to merge 14 commits into
thorek1:mainfrom
ylevch:feature/balanced-growth-path

Conversation

@ylevch

@ylevch ylevch commented Jul 3, 2026

Copy link
Copy Markdown

Hi Thore,

I wanted to be able to work with models with levels and BGP, so I took a stab at it.
This PR adds first-order support for such models with auto-detection in the equation parser.

I am a relative novice to open source development/contribution process and learn many things as I go (with Claude's help). Feedback would be appreciated. As a reference, I used IRIS-Toolbox implementation, so the main idea is that every variable gets a level and an additive growth unknown x^G. Since we get 2N unknowns, each steady-state equation is evaluated at two time origins (t, t+1) to double the equation count.

This PR includes:

  • Auto-detection of non-stationary levels in the parser; build the 2N augmented (level + growth) steady-state system; anchor the free level of each independent cointegrated trend to 0 via the steady-state Jacobian's rank.
  • ∂SS/∂param inverts a singular augmented Jacobian (free trend levels give zero columns; the two-time-point construction gives duplicate rows), fixed with the minimum-norm least-squares solution (pinv).
  • get_solution/get_irf work end-to-end for levels models.
  • Deterministic BGP drift x_t = anchor + xᴳ·t is added to trending variables in get_irf/simulate when levels = true. Unconditional variances of trending levels are NaN.
  • x[ss] = expr lets one to pin a trend's level explicitly (otherwise automatic anchoring is to 0).
  • I also included toy tests and compared steady state outputs of a simple QPM using this implementation vs IRIS-Toolbox: levels, growth rates and cointegration relationships matched.

Best,
Lazar

ylevch and others added 6 commits July 3, 2026 19:52
Allow models to be written in levels with non-stationary I(1) variables on a
common balanced growth path (IRIS / semistructural QPM style), instead of
hand-stationarizing into gaps and growth rates.

Steady state uses the IRIS two-time-point augmentation: each variable gets a
level and an additive per-period growth unknown `xᴳ`, and every steady-state
equation is evaluated at two time origins so the 2N residuals pin the 2N
(level, growth) unknowns. Growth-rate and cointegration identities then fall
out automatically. This is auto-detected at parse time, so fully stationary
models take an unchanged path.

- parser: detect unit-root/non-stationary levels (cancelling level or varless
  collapsed law); when detected, build the 2N augmented (level+growth) SS
  system and classify growth symbols as SS unknowns (model_setup.jl,
  equation_processing.jl).
- nsss solver: append growth unknowns to the solved SS vector; pass growths
  across solver blocks; anchor the free cointegrated trend directions to 0 via
  the SS Jacobian rank (zero incidence rows + dropzeros so the existing
  indeterminate->default path pins them); feed the residual cross-check the
  growth-inclusive solution.
- options_and_caches: keep growth symbols out of the variable-reporting index.

Verified on a random-walk-with-drift toy, a two-trend cointegration toy, and a
full cointegrated reference QPM (steady levels and growths match the IRIS
reference); stationary models are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The implicit-function derivative of the non-stochastic steady state inverts the
SS Jacobian ∂equations/∂(SS, pars). For levels/BGP models this augmented
(level+growth) Jacobian is singular — free trend levels give zero columns and
the two-time-point construction gives duplicate rows — so the LU solve failed
and all parameter derivatives were silently returned as zero (e.g. ∂dx/∂g = 0
for the random-walk-with-drift toy where dx = g).

Use the minimum-norm least-squares solution (pinv) for growth models in both
the reverse-mode rrule (src/rrules.jl) and the forward-mode Dual path
(ext/ForwardDiffExt.jl). It pins free-level sensitivities to 0 and resolves the
consistent duplicate rows exactly, giving correct ∂SS/∂param through the growth
unknowns. Gated on growth detection, so stationary models keep the fast LU path
unchanged.

Adds derivative assertions to the BGP test (∂dx/∂g = 1, ∂x/∂g = 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a get_irf finiteness check to the random-walk-with-drift test, covering the
first-order dynamic pipeline (policy function + IRFs) for a model with a unit
root, mirroring the end-to-end validation on the full reference QPM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first-order solution works in deviations from the constant anchored steady
state, so levels output (get_irf/simulate with levels=true) left trending
variables flat at their anchor instead of following the balanced growth path.

Add the deterministic drift x_t = anchor + xᴳ·t to trending variables when
returning levels: read each variable's solved growth xᴳ from the steady-state
solution (bgp_growth_by_name; lead/lag auxiliaries inherit the base variable's
growth) and add xᴳ·t per period in get_irf. Deviations (levels=false) are
unchanged, and stationary models are unaffected (no growth symbols → no-op).
Only the drift slope xᴳ is meaningful — the anchor is an arbitrary particular
solution of the cointegrated level system.

Verified: toy level drifts by g while its growth-rate variable stays at g and
deviations carry no drift; reference QPM trending levels (l_cpi_c, l_gdp,
l_rer, …) drift at exactly the IRIS reference steady_change rates while
stationary levels (dl_*, ir_*) stay flat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a "Balanced Growth Path (Models in Levels)" section to the steady-state
docs explaining auto-detection, the level+growth steady state, trend anchoring,
and drift in level paths, with a runnable random-walk-with-drift example. Flip
the MacroModelling.jl cell in the README feature-comparison matrix to
"yes (first order)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Let users pin a trending variable's steady-state level with an in-block
`x[ss] = expr` equation (an IRIS `!!`-style steady-state override), instead of
relying on the automatic anchor-to-0 of each free cointegrated trend. The anchor
constrains the level only; the growth stays pinned by the variable's dynamic law.

- parser: extract `x[ss] = expr` equations before dynamic processing into
  `equations.ss_anchors` (RHS collapsed to steady-state form); register
  parameters referenced only in anchors so they enter the parameter vector.
- nsss solver: force-anchor user-specified trend levels (then the rank pass
  covers any remaining free directions) and set them to the anchor expression in
  the indeterminate-variable step (parameters/constants resolved at solve time).

Anchors are level-only and target the model's independent trends; for a linear
model the dynamics are invariant to the anchor. Verified: `x[ss] = xbar` pins the
level to the parameter while the growth still equals the drift, levels drift from
the anchor, and stationary models / unanchored trends are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thorek1

thorek1 commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Hi Lazar,

This is really cool!
Thank you for spending time on this. Implementing BGP handling has been on my todo list since a long time and I was thinking of going down this route.

I had a brief look at your PR and my only comment at this stage would be to include a more complex model, ideally a benchmark test case for this kind of problem.

I will go through the codes in more detail in the coming weeks.

Best,

Thore

@thorek1
thorek1 marked this pull request as ready for review July 4, 2026 12:04
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 32.18%. Comparing base (f2274a0) to head (70bd494).

Files with missing lines Patch % Lines
ext/ForwardDiffExt.jl 0.00% 8 Missing ⚠️
src/rrules.jl 50.00% 3 Missing ⚠️
src/parser/equation_processing.jl 96.42% 1 Missing ⚠️
src/parser/model_setup.jl 97.61% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (f2274a0) and HEAD (70bd494). Click for more details.

HEAD has 31 uploads less than BASE
Flag BASE (f2274a0) HEAD (70bd494)
32 1
Additional details and impacted files
@@             Coverage Diff             @@
##             main     #304       +/-   ##
===========================================
- Coverage   88.05%   32.18%   -55.87%     
===========================================
  Files          35       35               
  Lines       30243    29372      -871     
===========================================
- Hits        26629     9453    -17176     
- Misses       3614    19919    +16305     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@thorek1

thorek1 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

I will collect some more thoughts here as I go along looking at the PR.

Suggestions:

  • covariance of BGP models refers to the covariance along the path (finite values instead of the currently infinite ones)
  • Show the growth rates in a separate column after the steady state values in SS calls
  • make it work with models with expectations. e.g.
using MacroModelling

@model RBC_baseline begin
	c[0] ^ (-σ) = β * c[1] ^ (-σ) ** z[1] * (k[0] / l[1]) ^- 1) + 1 - δ)

	ψ * c[0] ^ σ / (1 - l[0]) = w[0]

	k[0] = (1 - δ) * k[-1] + i[0]

	y[0] = c[0] + i[0] + g[0]

	y[0] = z[0] * k[-1] ^ α * l[0] ^ (1 - α)

	w[0] = y[0] * (1 - α) / l[0]

	r[0] = y[0] * α * 4 / k[-1]

	z[0] = (1 - ρᶻ) + ρᶻ * z[-1] + σᶻ * ϵᶻ[x]

	g[0] = (1 - ρᵍ) *+ ρᵍ * g[-1] + σᵍ * ϵᵍ[x]

end


@parameters RBC_baseline begin
	σᶻ = 0.066

	σᵍ = .104

	σ = 1

	α = 1/3

	i_y = 0.25

	k_y = 10.4

	ρᶻ = 0.97

	ρᵍ = 1.01 # unit root

	g_y = 0.2038|= g_y * y[ss]

    δ = i_y / k_y

    β = 1 // k_y + (1 - δ))

	ψ | l[ss] = 1/3
end

get_solution(RBC_baseline)

thorek1 and others added 8 commits July 14, 2026 10:26
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants