Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
143 changes: 81 additions & 62 deletions src/MacroModelling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ Reexport.@reexport import SparseArrays: sparse, spzeros, droptol!, sparsevec, sp


# Type definitions
const Symbol_input = Union{Symbol,Vector{Symbol},Matrix{Symbol},Tuple{Symbol,Vararg{Symbol}}}
const String_input = Union{String,Vector{String},Matrix{String},Tuple{String,Vararg{String}}}
const Symbol_input = Union{Symbol,Vector{Symbol},Matrix{Symbol},Tuple{Symbol,Vararg{Symbol}},Vector{Vector{Symbol}}}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot also allow for vector of tuples or tuples of tuples

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added support for Vector{Tuple{Symbol,Vararg{Symbol}}} and Tuple{Tuple{Symbol,Vararg{Symbol}},Vararg{Tuple{Symbol,Vararg{Symbol}}}} (and their String equivalents). Updated Symbol_input and String_input type definitions, and modified is_grouped_covariance_input, parse_covariance_groups, and parse_variables_input_to_index to handle these formats. Commit: a13fd2d

const String_input = Union{String,Vector{String},Matrix{String},Tuple{String,Vararg{String}},Vector{Vector{String}}}
const ParameterType = Union{Nothing,
Pair{Symbol, Float64},
Pair{String, Float64},
Expand Down Expand Up @@ -2212,16 +2212,13 @@ function determine_efficient_order(𝐒₁::Matrix{<: Real},
# their product can affect states
if nnz(𝐒₂) > 0
𝐒₂_states = 𝐒₂[state_idx_in_var, kron_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j]
# Check which states are affected by this product
affected = vec(sum(abs, 𝐒₂_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
# Generate selector vector for columns where both states are dependencies
# Kronecker structure: column index = (i-1)*nˢ + j for states i,j
selector = vec(ℒ.kron(prev_dependencies, prev_dependencies))
if any(selector)
# Check which states are affected by the selected products
affected = vec(sum(abs, 𝐒₂_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

Expand Down Expand Up @@ -2275,15 +2272,11 @@ function determine_efficient_order(𝐒₁::Matrix{<: Real},
# Second order propagation
if nnz(𝐒₂) > 0
𝐒₂_states = 𝐒₂[state_idx_in_var, kron_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j]
affected = vec(sum(abs, 𝐒₂_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
# Generate selector vector for columns where both states are dependencies
selector = vec(ℒ.kron(prev_dependencies, prev_dependencies))
if any(selector)
affected = vec(sum(abs, 𝐒₂_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

Expand Down Expand Up @@ -2403,32 +2396,22 @@ function determine_efficient_order(𝐒₁::Matrix{<: Real},
# Second order propagation
if nnz(𝐒₂) > 0
𝐒₂_states = 𝐒₂[state_idx_in_var, kron_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j]
affected = vec(sum(abs, 𝐒₂_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
# Generate selector vector for columns where both states are dependencies
selector = vec(ℒ.kron(prev_dependencies, prev_dependencies))
if any(selector)
affected = vec(sum(abs, 𝐒₂_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

# Third order propagation
if nnz(𝐒₃) > 0
𝐒₃_states = 𝐒₃[state_idx_in_var, kron_s_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
for k in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j] && prev_dependencies[k]
affected = vec(sum(abs, 𝐒₃_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
end
# Generate selector vector for columns where all three states are dependencies
selector = vec(ℒ.kron(ℒ.kron(prev_dependencies, prev_dependencies), prev_dependencies))
if any(selector)
affected = vec(sum(abs, 𝐒₃_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

Expand Down Expand Up @@ -2501,32 +2484,22 @@ function determine_efficient_order(𝐒₁::Matrix{<: Real},
# Second order propagation
if nnz(𝐒₂) > 0
𝐒₂_states = 𝐒₂[state_idx_in_var, kron_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j]
affected = vec(sum(abs, 𝐒₂_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
# Generate selector vector for columns where both states are dependencies
selector = vec(ℒ.kron(prev_dependencies, prev_dependencies))
if any(selector)
affected = vec(sum(abs, 𝐒₂_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

# Third order propagation
if nnz(𝐒₃) > 0
𝐒₃_states = 𝐒₃[state_idx_in_var, kron_s_s_s]
col_idx = 1
for i in 1:nˢ
for j in 1:nˢ
for k in 1:nˢ
if prev_dependencies[i] && prev_dependencies[j] && prev_dependencies[k]
affected = vec(sum(abs, 𝐒₃_states[:, col_idx:col_idx], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
col_idx += 1
end
end
# Generate selector vector for columns where all three states are dependencies
selector = vec(ℒ.kron(ℒ.kron(prev_dependencies, prev_dependencies), prev_dependencies))
if any(selector)
affected = vec(sum(abs, 𝐒₃_states[:, selector], dims=2) .> tol)
dependencies_in_states = dependencies_in_states .| affected
end
end

Expand Down Expand Up @@ -9060,7 +9033,12 @@ end


function parse_variables_input_to_index(variables::Union{Symbol_input,String_input}, T::timings)::Union{UnitRange{Int}, Vector{Int}}
variables = variables isa String_input ? variables .|> Meta.parse .|> replace_indices : variables
# Handle nested vector conversion separately
if variables isa Vector{Vector{String}}
variables = [group .|> Meta.parse .|> replace_indices for group in variables]
elseif variables isa String_input
variables = variables .|> Meta.parse .|> replace_indices
end

if variables == :all_excluding_auxiliary_and_obc
return Int.(indexin(setdiff(T.var[.!contains.(string.(T.var),"ᵒᵇᶜ")],union(T.aux, T.exo_present)),sort(union(T.var,T.aux,T.exo_present))))
Expand All @@ -9075,6 +9053,14 @@ function parse_variables_input_to_index(variables::Union{Symbol_input,String_inp
return Int[]
end
return getindex(1:length(T.var),convert(Vector{Bool},vec(sum(variables .== T.var,dims= 2))))
elseif variables isa Vector{Vector{Symbol}}
# For grouped inputs, return union of all variables
all_vars = reduce(vcat, variables)
if length(setdiff(all_vars,T.var)) > 0
@warn "The following variables are not part of the model: " * join(string.(setdiff(all_vars,T.var)),", ") * ". Use `get_variables(𝓂)` to list valid names."
return Int[]
end
return Int.(indexin(unique(all_vars), T.var))
elseif variables isa Vector{Symbol}
if length(setdiff(variables,T.var)) > 0
@warn "The following variables are not part of the model: " * join(string.(setdiff(variables,T.var)),", ") * ". Use `get_variables(𝓂)` to list valid names."
Expand All @@ -9099,6 +9085,39 @@ function parse_variables_input_to_index(variables::Union{Symbol_input,String_inp
end
end

# Helper function to check if input is grouped covariance format
function is_grouped_covariance_input(variables::Union{Symbol_input,String_input})::Bool
# Check if it's a nested vector (either String or Symbol)
return variables isa Vector{Vector{Symbol}} || variables isa Vector{Vector{String}}
end

# Function to parse grouped covariance input into groups of indices
function parse_covariance_groups(variables::Union{Symbol_input,String_input}, T::timings)::Vector{Vector{Int}}
# Convert String_input to Symbol_input for nested vectors
if variables isa Vector{Vector{String}}
variables = [group .|> Meta.parse .|> replace_indices for group in variables]
end

if !is_grouped_covariance_input(variables)
# Not grouped, return single group
idx = parse_variables_input_to_index(variables, T)
return [collect(idx)]
end

# Parse each group (variables is now Vector{Vector{Symbol}})
groups = Vector{Vector{Int}}()
for group in variables
if length(setdiff(group, T.var)) > 0
@warn "The following variables are not part of the model: " * join(string.(setdiff(group,T.var)),", ") * ". Use `get_variables(𝓂)` to list valid names."
push!(groups, Int[])
else
push!(groups, Int.(indexin(group, T.var)))
end
end

return groups
end


function parse_shocks_input_to_index(shocks::Union{Symbol_input, String_input}, T::timings)#::Union{UnitRange{Int64}, Int64, Vector{Int64}}
shocks = shocks isa String_input ? shocks .|> Meta.parse .|> replace_indices : shocks
Expand Down
46 changes: 43 additions & 3 deletions src/get_functions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3207,7 +3207,7 @@ If occasionally binding constraints are present in the model, they are not taken
- `mean` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the mean of selected variables (the mean for the linearised solution is the NSSS). Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all shocks less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all shocks less those related to auxiliary variables. `:all` will contain all variables.
- `standard_deviation` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the standard deviation of selected variables. Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all shocks less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all shocks less those related to auxiliary variables. `:all` will contain all variables.
- `variance` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the variance of selected variables. Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all shocks less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all shocks less those related to auxiliary variables. `:all` will contain all variables.
- `covariance` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the covariance of selected variables. Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all shocks less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all shocks less those related to auxiliary variables. `:all` will contain all variables.
- `covariance` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the covariance of selected variables. Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. For grouped covariance computation, pass a `Vector` of `Vector`s (e.g. `[[:y, :c], [:k, :i]]`) to compute covariances only within each group, returning a single covariance matrix where cross-group covariances are set to zero. This allows more granular control over which covariances to compute. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all variables less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all variables less those related to occasionally binding constraints. `:all` will contain all variables.
- `autocorrelation` [Default: `Symbol[]`, Type: `Union{Symbol_input,String_input}`]: variables for which to show the autocorrelation of selected variables. Inputs can be a variable name passed on as either a `Symbol` or `String` (e.g. `:y` or \"y\"), or `Tuple`, `Matrix` or `Vector` of `String` or `Symbol`. Any variables not part of the model will trigger a warning. `:all_excluding_auxiliary_and_obc` contains all shocks less those related to auxiliary variables and related to occasionally binding constraints (obc). `:all_excluding_obc` contains all shocks less those related to auxiliary variables. `:all` will contain all variables.
- `autocorrelation_periods` [Default: `1:5`, Type = `UnitRange{Int}`]: periods for which to return the autocorrelation of selected variables
- $ALGORITHM®
Expand Down Expand Up @@ -3243,6 +3243,12 @@ get_statistics(RBC, RBC.parameter_values, parameters = RBC.parameters, standard_
# output
Dict{Symbol, AbstractArray{Float64}} with 1 entry:
:standard_deviation => [0.0266642, 0.264677, 0.0739325, 0.0102062]

# For grouped covariance (computing covariances only within specified groups):
get_statistics(RBC, RBC.parameter_values, covariance = [[:c, :k], [:q]])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot make the second group also have two variables instead of only :q

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the documentation example to use balanced groups with two variables each: [[:c, :k], [:y, :i]] instead of [[:c, :k], [:q]]. The output description now reflects a 4×4 matrix instead of 3×3. Commit: 093b24d

# output
Dict{Symbol, AbstractArray{Float64}} with 1 entry:
:covariance => [...3x3 matrix with c-k covariances filled, q variance filled, and cross-group elements set to zero...]
```
"""
function get_statistics(𝓂,
Expand Down Expand Up @@ -3283,6 +3289,10 @@ function get_statistics(𝓂,
var_var_idx = @ignore_derivatives parse_variables_input_to_index(variance, 𝓂.timings)

covar_var_idx = @ignore_derivatives parse_variables_input_to_index(covariance, 𝓂.timings)

# Check if covariance is grouped and parse groups
is_grouped_covar = @ignore_derivatives is_grouped_covariance_input(covariance)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why do you need this indicator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indicator was used to check whether to extract grouped covariances or use standard extraction. Simplified this by directly checking !isnothing(covar_groups) instead of maintaining a separate is_grouped_covar variable. Commit: a13fd2d

covar_groups = @ignore_derivatives is_grouped_covar ? parse_covariance_groups(covariance, 𝓂.timings) : nothing

autocorr_var_idx = @ignore_derivatives parse_variables_input_to_index(autocorrelation, 𝓂.timings)

Expand Down Expand Up @@ -3405,8 +3415,38 @@ function get_statistics(𝓂,

# droptol!(covar_dcmp_sp,eps(Float64))

# push!(ret,covar_dcmp_sp[covar_var_idx,covar_var_idx])
ret[:covariance] = solved ? covar_dcmp_sp[covar_var_idx,covar_var_idx] : fill(Inf * sum(abs2,parameter_values),isnothing(covar_var_idx) ? 0 : length(covar_var_idx), isnothing(covar_var_idx) ? 0 : length(covar_var_idx))
if is_grouped_covar
# Extract only the specified covariance groups (block diagonal structure)
# Return a single matrix with zeros for non-computed covariances
if solved
# Initialize matrix with zeros
covar_result = zeros(T, length(covar_var_idx), length(covar_var_idx))

# Fill in only the specified groups
for group in covar_groups
for (i_idx, i) in enumerate(group)
for (j_idx, j) in enumerate(group)
# Find position in covar_var_idx
i_pos = findfirst(==(i), covar_var_idx)
j_pos = findfirst(==(j), covar_var_idx)
Comment on lines +3424 to +3430
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The findfirst calls inside nested loops are inefficient. Consider precomputing a lookup dictionary mapping indices to positions in covar_var_idx before the loop, similar to the optimization pattern used in lines 2144-2149 of MacroModelling.jl.

Suggested change
# Fill in only the specified groups
for group in covar_groups
for (i_idx, i) in enumerate(group)
for (j_idx, j) in enumerate(group)
# Find position in covar_var_idx
i_pos = findfirst(==(i), covar_var_idx)
j_pos = findfirst(==(j), covar_var_idx)
# Precompute lookup dictionary for covar_var_idx
covar_var_idx_lookup = Dict{eltype(covar_var_idx), Int}()
for (idx, val) in enumerate(covar_var_idx)
covar_var_idx_lookup[val] = idx
end
# Fill in only the specified groups
for group in covar_groups
for (i_idx, i) in enumerate(group)
for (j_idx, j) in enumerate(group)
# Find position in covar_var_idx using lookup
i_pos = get(covar_var_idx_lookup, i, nothing)
j_pos = get(covar_var_idx_lookup, j, nothing)

Copilot uses AI. Check for mistakes.
if !isnothing(i_pos) && !isnothing(j_pos)
covar_result[i_pos, j_pos] = covar_dcmp_sp[i, j]
end
end
end
end

ret[:covariance] = covar_result
else
# Return matrix with Inf-filled diagonal and zeros elsewhere
covar_result = fill(Inf * sum(abs2,parameter_values), length(covar_var_idx), length(covar_var_idx))
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the model is not solved, the unsolved case fills the entire matrix with Inf values, but the comment suggests it should have 'zeros elsewhere'. This is inconsistent with the solved case which has zeros for cross-group covariances. The unsolved matrix should match the structure of the solved case with zeros for non-diagonal elements or cross-group elements.

Suggested change
covar_result = fill(Inf * sum(abs2,parameter_values), length(covar_var_idx), length(covar_var_idx))
covar_result = zeros(T, length(covar_var_idx), length(covar_var_idx))
for i in 1:length(covar_var_idx)
covar_result[i, i] = Inf * sum(abs2, parameter_values)
end

Copilot uses AI. Check for mistakes.
ret[:covariance] = covar_result
end
else
# Original behavior for non-grouped input
# push!(ret,covar_dcmp_sp[covar_var_idx,covar_var_idx])
ret[:covariance] = solved ? covar_dcmp_sp[covar_var_idx,covar_var_idx] : fill(Inf * sum(abs2,parameter_values),isnothing(covar_var_idx) ? 0 : length(covar_var_idx), isnothing(covar_var_idx) ? 0 : length(covar_var_idx))
end
end
if !(autocorrelation == Symbol[])
# push!(ret,autocorr[autocorr_var_idx,:] )
Expand Down
Loading