Skip to content

Commit 2c4b8cf

Browse files
Copilotthorek1
andcommitted
Refactor to use NonlinearSolve.jl with per-equation residuals
Changed from optimization to nonlinear solving: - Use NonlinearSolve.jl instead of Optimization.jl - Return per-equation average residuals (not scalar sum) - Use Newton-Raphson solver with autodiff - Transform Sobol draws to standard normal distribution - Updated README with new approach and requirements Co-authored-by: thorek1 <13523097+thorek1@users.noreply.github.com>
1 parent 9fa2158 commit 2c4b8cf

2 files changed

Lines changed: 163 additions & 128 deletions

File tree

test_dynamic_equations_optimization.jl

Lines changed: 138 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,184 @@
1-
using Revise
1+
"""
2+
Test script for solving variables across stochastic shocks using the dynamic equations function.
3+
4+
This script demonstrates:
5+
1. Loading a model and computing steady states
6+
2. Generating Sobol quasi-random sequences for shocks
7+
3. Evaluating dynamic equations across multiple shock realizations
8+
4. Solving for variables (past, present, future) using NonlinearSolve.jl
9+
"""
10+
211
using MacroModelling
312
using QuasiMonteCarlo
4-
using Optimization
5-
# using OptimizationOptimJL
6-
using OptimizationNLopt
7-
import SpecialFunctions: erfinv
13+
using NonlinearSolve
14+
using SpecialFunctions
815
using Statistics
9-
# using Zygote
1016

1117
# Define a simple RBC model
12-
# @model RBC begin
13-
# 1 / c[0] = (β / c[1]) * (α * exp(z[1]) * k[0]^(α - 1) + (1 - δ))
14-
# c[0] + k[0] = (1 - δ) * k[-1] + q[0]
15-
# q[0] = exp(z[0]) * k[-1]^α
16-
# z[0] = ρ * z[-1] + std_z * eps_z[x]
17-
# end
18-
19-
# @parameters RBC begin
20-
# std_z = 0.01
21-
# ρ = 0.2
22-
# δ = 0.02
23-
# α = 0.5
24-
# β = 0.95
25-
# end
26-
27-
include("./models/Gali_2015_chapter_3_nonlinear.jl")
28-
29-
m = Gali_2015_chapter_3_nonlinear
30-
# m.dyn_equations[8]
18+
@model RBC begin
19+
1 / c[0] =/ c[1]) ** exp(z[1]) * k[0]^- 1) + (1 - δ))
20+
c[0] + k[0] = (1 - δ) * k[-1] + q[0]
21+
q[0] = exp(z[0]) * k[-1]^α
22+
z[0] = ρ * z[-1] + std_z * eps_z[x]
23+
end
24+
25+
@parameters RBC begin
26+
std_z = 0.01
27+
ρ = 0.2
28+
δ = 0.02
29+
α = 0.5
30+
β = 0.95
31+
end
32+
33+
println("Solving model...")
34+
solve!(RBC)
35+
3136
# Get steady states
32-
SS_non_stochastic = SS(m, derivatives = false) |> collect
33-
SS_stochastic = SSS(m, derivatives = false) |> collect
37+
println("Computing steady states...")
38+
SS_non_stochastic = get_steady_state(RBC, stochastic = false)
39+
SS_stochastic = get_steady_state(RBC, stochastic = true)
40+
41+
println("Non-stochastic steady state:")
42+
println(SS_non_stochastic)
43+
println("\nStochastic steady state:")
44+
println(SS_stochastic)
45+
46+
# Get auxiliary indices for the steady state input
47+
aux_idx = RBC.solution.perturbation.auxiliary_indices
48+
SS_for_func = SS_non_stochastic[aux_idx.dyn_ss_idx]
49+
50+
# Number of variables and shocks
51+
n_vars = length(RBC.var)
52+
n_shocks = length(RBC.exo)
53+
n_dyn_eqs = length(RBC.dyn_equations)
54+
n_calib_eqs = length(RBC.calibration_equations)
55+
n_total_eqs = n_dyn_eqs + n_calib_eqs
56+
57+
println("\nModel dimensions:")
58+
println(" Variables: ", n_vars)
59+
println(" Shocks: ", n_shocks)
60+
println(" Dynamic equations: ", n_dyn_eqs)
61+
println(" Calibration equations: ", n_calib_eqs)
62+
println(" Total equations: ", n_total_eqs)
3463

35-
# SS_for_func = SS_non_stochastic #
64+
# Generate Sobol sequence for shocks
65+
n_draws = 100
66+
println("\nGenerating ", n_draws, " Sobol draws for shocks...")
3667

68+
# Generate Sobol sequence in [0,1]
69+
shock_draws_uniform = QuasiMonteCarlo.sample(n_draws, n_shocks, SobolSample())
3770

71+
# Convert to normal distribution using inverse error function
72+
normal_draws = @. sqrt(2) * erfinv(2 * shock_draws_uniform - 1)
3873

39-
# histogram(vec(shock_draws), bins=30, title="Histogram of Uniform [0,1] Sobol Draws", xlabel="Value", ylabel="Frequency")
74+
# Standardize to zero mean and unit variance
75+
normal_draws .-= mean(normal_draws, dims=2)
76+
normal_draws ./= Statistics.std(normal_draws, dims=2)
4077

41-
# histogram(vec(@. sqrt(2) * erfinv(2 * shock_draws - 1)), bins=30, title="Histogram of Normal Sobol Draws", xlabel="Value", ylabel="Frequency")
78+
println("Shock draws shape: ", size(normal_draws))
79+
println("Shock draws mean: ", mean(normal_draws, dims=2))
80+
println("Shock draws std: ", Statistics.std(normal_draws, dims=2))
4281

4382
# Calibration parameters
44-
calib_params = zeros(length(m.calibration_equations_parameters))
83+
calib_params = zeros(length(RBC.calibration_equations_parameters))
4584

46-
# Define objective function: sum of squared residuals across all shock draws
47-
function objective(vars_flat, shock_draws, model, SS_non_stochastic, calib_params)
48-
# n_vars = length(model.var)
85+
# Define residual function that returns per-equation average residuals
86+
# This is compatible with NonlinearSolve.jl
87+
function residual_function!(residual_avg, vars_flat, p)
88+
shock_draws, model, SS_for_func, calib_params = p
89+
4990
n_draws = size(shock_draws, 2)
50-
91+
n_vars = length(model.var)
92+
5193
# Unpack variables: [past; present; future]
52-
past = vars_flat#[1:n_vars]
53-
present = vars_flat#[n_vars+1:2*n_vars]
54-
future = vars_flat#[2*n_vars+1:3*n_vars]
94+
past = vars_flat[1:n_vars]
95+
present = vars_flat[n_vars+1:2*n_vars]
96+
future = vars_flat[2*n_vars+1:3*n_vars]
5597

5698
n_eqs = length(model.dyn_equations) + length(model.calibration_equations)
57-
58-
residual = zeros(eltype(vars_flat), n_eqs)
59-
60-
# Sum squared residuals across all shock draws
61-
total_loss = 0.0
99+
residual_temp = zeros(n_eqs)
100+
101+
# Initialize average residuals to zero
102+
fill!(residual_avg, 0.0)
103+
104+
# Sum residuals across all shock draws
62105
for i in 1:n_draws
63106
shocks = shock_draws[:, i]
64107

65108
# Evaluate dynamic equations
66-
get_dynamic_residuals(residual, model.parameter_values, calib_params,
67-
past, present, future, SS_non_stochastic, shocks, model)
109+
get_dynamic_residuals(residual_temp, model.parameter_values, calib_params,
110+
past, present, future, SS_for_func, shocks, model)
68111

69-
# Add squared residuals
70-
total_loss += sum(residual.^2)
112+
# Accumulate residuals
113+
residual_avg .+= residual_temp
71114
end
72115

73-
return total_loss / n_draws # Average loss
116+
# Average over draws
117+
residual_avg ./= n_draws
118+
119+
return nothing
74120
end
75121

76-
n_eqs = length(m.dyn_equations) + length(m.calibration_equations)
77-
residual = zeros(n_eqs)
78-
79-
initial_vars = SS_stochastic # vcat(SS_stochastic, SS_stochastic, SS_stochastic)
80-
81-
82-
83-
n_shocks = length(m.exo)
84-
85-
# Generate Sobol sequence for shocks
86-
n_draws = 1000
87-
88-
89-
# Generate Sobol sequence
90-
shock_draws = QuasiMonteCarlo.sample(n_draws, n_shocks, SobolSample())
91-
92-
normal_draws = @. sqrt(2) * erfinv(2 * shock_draws - 1)
93-
94-
normal_draws .-= mean(normal_draws, dims=2)
95-
normal_draws ./= Statistics.std(normal_draws, dims=2)
96-
97-
mean(normal_draws, dims=2)
98-
Statistics.std(normal_draws, dims=2)
99-
100-
101-
objective(initial_vars, normal_draws, m, SS_non_stochastic, calib_params)
102-
103-
# Set up optimization problem
104-
optf = OptimizationFunction((x, p) -> objective(x, normal_draws, m, SS_non_stochastic, calib_params), AutoForwardDiff())
105-
prob = OptimizationProblem(optf, SS_stochastic)
106-
107-
# Solve using BFGS
108-
sol = solve(prob, NLopt.LD_LBFGS())
109-
# solPR = solve(prob, NLopt.LN_PRAXIS())
110-
solNM = solve(prob, NLopt.LN_NELDERMEAD())
111-
# solBO = solve(prob, NLopt.LN_BOBYQA())
112-
# solCO = solve(prob, NLopt.LN_COBYLA())
122+
# Initial guess: use stochastic steady state for all time periods
123+
println("\nSetting up nonlinear problem...")
124+
initial_vars = vcat(SS_stochastic, SS_stochastic, SS_stochastic)
113125

114-
objective!(residual, sol.u, shock_draws, m, SS_non_stochastic, calib_params)
115-
objective!(residual, initial_vars, shock_draws, m, SS_non_stochastic, calib_params)
126+
# Package parameters for the residual function
127+
params = (normal_draws, RBC, SS_for_func, calib_params)
116128

129+
# Test initial residuals
130+
residual_test = zeros(n_total_eqs)
131+
residual_function!(residual_test, initial_vars, params)
132+
println("Initial residual norm: ", norm(residual_test))
133+
println("Initial max|residual|: ", maximum(abs.(residual_test)))
117134

118-
ststst = SSS(m, derivatives = false, algorithm = :third_order)
135+
# Set up nonlinear problem
136+
println("\nSolving nonlinear system...")
137+
println("This may take a moment...")
119138

120-
ststst .= sol.u
139+
prob = NonlinearProblem(residual_function!, initial_vars, params)
121140

141+
# Solve using Newton-Raphson with automatic differentiation
142+
sol = solve(prob, NewtonRaphson(; autodiff = AutoFiniteDiff()))
122143

123-
ststst_orig = SSS(m, derivatives = false, algorithm = :third_order)
144+
println("\nSolution complete!")
145+
println("Solution return code: ", sol.retcode)
146+
println("Final residual norm: ", norm(sol.resid))
147+
println("Final max|residual|: ", maximum(abs.(sol.resid)))
124148

149+
# Extract solved variables
150+
solved_past = sol.u[1:n_vars]
151+
solved_present = sol.u[n_vars+1:2*n_vars]
152+
solved_future = sol.u[2*n_vars+1:3*n_vars]
125153

126-
println("\nOptimization complete!")
127-
println("Final objective value: ", sol.objective)
128-
println("Initial objective value: ", objective(initial_vars, shock_draws, RBC, SS_for_func, calib_params))
129-
println("Improvement: ", (1 - sol.objective / objective(initial_vars, shock_draws, RBC, SS_for_func, calib_params)) * 100, "%")
130-
131-
# Extract optimized variables
132-
opt_past = sol.u[1:n_vars]
133-
opt_present = sol.u[n_vars+1:2*n_vars]
134-
opt_future = sol.u[2*n_vars+1:3*n_vars]
135-
136-
println("\nOptimized variables:")
137-
println("Past: ", opt_past)
138-
println("Present: ", opt_present)
139-
println("Future: ", opt_future)
154+
println("\nSolved variables:")
155+
println("Past: ", solved_past)
156+
println("Present: ", solved_present)
157+
println("Future: ", solved_future)
140158

141159
println("\nComparison with steady states:")
142160
for (i, var) in enumerate(RBC.var)
143161
println(" ", var, ":")
144162
println(" Non-stochastic SS: ", SS_non_stochastic[i])
145163
println(" Stochastic SS: ", SS_stochastic[i])
146-
println(" Optimized (past): ", opt_past[i])
147-
println(" Optimized (pres): ", opt_present[i])
148-
println(" Optimized (fut): ", opt_future[i])
164+
println(" Solved (past): ", solved_past[i])
165+
println(" Solved (present): ", solved_present[i])
166+
println(" Solved (future): ", solved_future[i])
149167
end
150168

169+
# Evaluate residuals at the solution for a few shock draws
170+
println("\nPer-draw residuals at solution for first 5 shock draws:")
171+
residual = zeros(n_total_eqs)
172+
for i in 1:min(5, n_draws)
173+
shocks = normal_draws[:, i]
174+
get_dynamic_residuals(residual, RBC.parameter_values, calib_params,
175+
solved_past, solved_present, solved_future, SS_for_func, shocks, RBC)
176+
println("Draw ", i, ": max|residual| = ", maximum(abs.(residual)),
177+
", mean|residual| = ", sum(abs.(residual))/length(residual))
178+
end
179+
180+
println("\nScript completed successfully!")
181+
151182
# Evaluate residuals at the optimal point for a few shock draws
152183
println("\nResiduals at optimal point for first 5 shock draws:")
153184
residual = zeros(n_total_eqs)
Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,31 @@
1-
# Dynamic Equations Optimization Test Script
1+
# Dynamic Equations Nonlinear Solver Test Script
22

3-
This script demonstrates how to use the `get_dynamic_residuals` function to optimize variables across stochastic shock realizations.
3+
This script demonstrates how to use the `get_dynamic_residuals` function to solve for variables across stochastic shock realizations using NonlinearSolve.jl.
44

55
## Purpose
66

77
The script shows an advanced use case where:
88
1. A model is loaded and solved
99
2. Stochastic and non-stochastic steady states are computed
1010
3. Shock draws are generated using Sobol quasi-random sequences
11-
4. Variables (past, present, future) are optimized to minimize average residuals across all shock realizations
11+
4. Variables (past, present, future) are solved to zero out average residuals across all shock realizations
1212

1313
## Requirements
1414

1515
```julia
1616
using MacroModelling
1717
using QuasiMonteCarlo
18-
using Optimization
19-
using OptimizationOptimJL
18+
using NonlinearSolve
19+
using SpecialFunctions
20+
using Statistics
2021
```
2122

2223
Install missing packages with:
2324
```julia
2425
using Pkg
2526
Pkg.add("QuasiMonteCarlo")
26-
Pkg.add("Optimization")
27-
Pkg.add("OptimizationOptimJL")
27+
Pkg.add("NonlinearSolve")
28+
Pkg.add("SpecialFunctions")
2829
```
2930

3031
## Usage
@@ -37,32 +38,35 @@ julia test_dynamic_equations_optimization.jl
3738

3839
1. **Model Setup**: Defines and solves a simple RBC model
3940
2. **Steady States**: Computes both stochastic and non-stochastic steady states
40-
3. **Shock Generation**: Uses Sobol sequences from QuasiMonteCarlo.jl to generate quasi-random shock draws
41-
4. **Optimization**: Minimizes the sum of squared residuals across all shock draws by choosing optimal values for past, present, and future variables
42-
5. **Results**: Displays optimized variables and compares them with steady states
41+
3. **Shock Generation**: Uses Sobol sequences from QuasiMonteCarlo.jl to generate quasi-random shock draws, then transforms them to standard normal distribution
42+
4. **Nonlinear Solving**: Uses NonlinearSolve.jl to find values for past, present, and future variables that zero out average residuals across all shock draws
43+
5. **Results**: Displays solved variables and compares them with steady states
4344

4445
## Key Features
4546

4647
- Uses **Sobol sequences** for better coverage of the shock space compared to random sampling
47-
- Optimizes over **all time dimensions** (past, present, future) simultaneously
48-
- Uses **non-stochastic steady state** for the steady_state input (as requested)
48+
- Transforms uniform Sobol draws to **standard normal** distribution using inverse error function
49+
- Solves for **all time dimensions** (past, present, future) simultaneously
50+
- Uses **non-stochastic steady state** for the steady_state input
51+
- Returns **per-equation average residuals** compatible with NonlinearSolve.jl
4952
- Evaluates dynamic equations efficiently using the pre-compiled function
50-
- Shows how to integrate the dynamic equations function with optimization workflows
53+
- Uses **Newton-Raphson** method with automatic differentiation
5154

5255
## Expected Output
5356

5457
The script will:
5558
- Display model dimensions
56-
- Show initial and final objective values
57-
- Print optimized variables
58-
- Compare optimized values with steady states
59-
- Display residuals at the optimal point
59+
- Show initial and final residual norms
60+
- Print solved variables
61+
- Compare solved values with steady states
62+
- Display per-draw residuals at the solution
6063

6164
## Notes
6265

63-
- The optimization finds variables that best satisfy the dynamic equations across many shock realizations
66+
- The nonlinear solver finds variables that satisfy the dynamic equations (on average) across many shock realizations
6467
- This approach could be used for:
65-
- Finding robust policies
66-
- Analyzing stochastic equilibria
68+
- Computing stochastic steady states numerically
69+
- Analyzing equilibria under uncertainty
6770
- Implementing custom solution algorithms
68-
- Estimating models with moment matching
71+
- Model estimation with moment matching
72+
- The residual function returns per-equation averages (not a scalar), making it compatible with nonlinear solvers

0 commit comments

Comments
 (0)