I’m building a DSGE modeling language (lol what else is new) where I want to use something like ComponentArrays.jl for the construction of first order perturbations using DifferentiationInterface.jl. The point here is that I divide a model into blocks with unknowns and targets ala sequence_jacobian.py.
The problem here is that the component array loses its structure when performing DI.jacobian(), but not with DI.gradient(). I can produce a (not so) minimum working example with the following.
using ComponentArrays
using DifferentiationInterface
import ForwardDiff
lag(x::AbstractVector) = x[1]
lag(x::Real) = x
contemp(x::AbstractVector) = x[2]
contemp(x::Real) = x
lead(x::AbstractVector) = x[3]
lead(x::Real) = x
function production(x::ComponentArray)
(; K, L, Z, α, δ) = x
r = contemp(α) * contemp(Z) * (lag(K) / contemp(L)) ^ (contemp(α) - 1) - contemp(δ)
w = (1 - contemp(α)) * contemp(Z) * (lag(K) / contemp(L)) ^ contemp(α)
Y = contemp(Z) * lag(K) ^ contemp(α) * contemp(L) ^ (contemp(α) - 1)
return ComponentArray(; r, w, Y)
end
Suppose we already solved for the steady state using NonlinearSolve.jl using x -> production(x) as the objective, which gave us
ss = ComponentArray(
L = 1.0, δ = 0.025, α = 0.11, K = 3.142857142857058, Z = 0.881646097521458
)
Finding the first order perturbation then reduces to the following calculus
stacked_ss = ComponentArray(NamedTuple{keys(ss)}([fill(i, 3) for i in ss]))
J = DifferentiationInterface.jacobian(
x -> production(x), AutoForwardDiff(), stacked_ss
)
which yields the following illegible mess:
3×15 Matrix{Float64}:
-0.00991 0.0 0.0 0.0 0.03115 0.0 0.0 0.03969 0.0 0.0 0.35826 0.0 0.0 -1.0 0.0
0.03115 0.0 0.0 0.0 -0.09790 0.0 0.0 1.00948 0.0 0.0 0.01917 0.0 0.0 0.0 0.0
0.03500 0.0 0.0 0.0 -0.89000 0.0 0.0 1.13424 0.0 0.0 1.14513 0.0 0.0 0.0 0.0
However, when I individually take the gradient per target…
DifferentiationInterface.gradient(
x -> production(x)[:r], AutoForwardDiff(), stacked_ss
)
the component array structure is preserved!
ComponentVector{Float64}(
L = [0.0, 0.031150000000000788, 0.0],
δ = [0.0, -1.0, 0.0],
α = [0.0, 0.35826144883243144, 0.0],
K = [-0.009911363636364154, 0.0, 0.0],
Z = [0.0, 0.0396984686921376, 0.0]
)
As much as I enjoy the type stability and composability of ComponentArrays.jl, I feel like I have to do more work to step away from the intuitiveness of named tuples. And if I have to reshape and manipulate every output from the function call, then I may as well create my own struct specific to this problem.
There are a couple hills I think are worth dying on here:
DI.jacobianis the clear winner here since we can prep the gradient upon block definition and store sparsity patterns.- I need to preserve the names of rows/columns of the Jacobian for computing chain rules when faced with multiple blocks. Whether this be ComponentArrays.jl, NamedTuples, or some custom object.
For those who are curious, I copied the design philosophy of sequence_jacobian.py and create the above model block with a macro.
@simple function production(K, L, Z, α, δ)
r = α * Z * (lag(K) / L) ^ (α - 1) - δ
w = (1 - α) * Z * (lag(K) / L) ^ α
Y = Z * lag(K) ^ α * L ^ (α - 1)
return r, w, Y
end