Skip to content

Commit 6adc9dd

Browse files
Default use_sparse_FOC_jac to True; pin dense path in test_inner_loop
1 parent 066c297 commit 6adc9dd

4 files changed

Lines changed: 8 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Adds an optional `use_sparse_FOC_jac` `Specifications` parameter (default off, so default runs are unchanged) that accelerates the time path iteration (TPI) household solve. When True, `scipy.optimize.root` is given a sparse (banded) finite-difference Jacobian for the stacked household Euler and labor first order conditions: the sparsity pattern is auto-detected once per problem size and the solver then needs far fewer function evaluations per Jacobian build (about 20x fewer on the default S=80 cohort solve), with an automatic fallback to dense finite differences if the Jacobian is not sparse enough to benefit or if a solve fails. The result matches the dense-finite-difference solution to within the model's resource-constraint accuracy floor on every calibration tested (OG-Core standard example, OG-ETH, OG-ZAF, OG-PHL, OG-IDN), giving roughly a 1.9-2.4x TPI speedup at no accuracy cost.
12+
- Adds a `use_sparse_FOC_jac` `Specifications` parameter (default `True`) that accelerates the time path iteration (TPI) household solve. With it on, `scipy.optimize.root` is given a sparse (banded) finite-difference Jacobian for the stacked household Euler and labor first order conditions: the sparsity pattern is auto-detected once per problem size and the solver then needs far fewer function evaluations per Jacobian build (about 20x fewer on the default S=80 cohort solve), with an automatic fallback to dense finite differences if the Jacobian is not sparse enough to benefit or if a solve fails. The result matches the legacy dense-finite-difference solution to within the model's resource-constraint accuracy floor on every calibration tested (OG-Core standard example, OG-ETH, OG-ZAF, OG-PHL, OG-IDN), giving roughly a 1.9-2.4x TPI speedup at no accuracy cost. Set `use_sparse_FOC_jac=False` to recover bit-identical agreement with v0.16.0 and earlier.
1313

1414
## [0.16.0] - 2026-06-02 12:00:00
1515

docs/book/content/theory/equilibrium.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ In all of the specifications of `OG-Core`, we use a two-stage fixed point algori
2525

2626
Our approach is to choose the minimum number of macroeconomic variables in an outer loop in order to be able to solve the household's $2JS$ Euler equations in terms of only the $\bar{n}_{j,s}$ and $\bar{b}_{j,s+1}$ variables directly, holding all other variables constant. The household system of Euler equations has a provable root solution and is orders of magnitude more tractable (less nonlinear) to solve holding these outer loop variables constant.
2727

28-
Moreover, with the outer-loop variables held fixed, each cohort's system of $2S$ Euler equations is not only less nonlinear but structurally sparse: every equation involves at most five of the $2S$ unknowns---a household's own age and its immediate neighbors. The root finder normally probes each unknown separately when building each step ($2S = 160$ evaluations of the system when $S = 80$), but with most equations depending on only a handful of unknowns, those affecting no common equation can be probed together, cutting the count to about seven at $S = 80$---a number set by how many neighbors couple, not by $S$. The parameter `use_sparse_FOC_jac` (default `False`) turns this on; the solver falls back to the standard calculation otherwise. The structure is derived in Appendix {ref}`SecAppDerivHHjac`.
28+
Moreover, with the outer-loop variables held fixed, each cohort's system of $2S$ Euler equations is not only less nonlinear but structurally sparse: every equation involves at most five of the $2S$ unknowns---a household's own age and its immediate neighbors. The root finder normally probes each unknown separately when building each step ($2S = 160$ evaluations of the system when $S = 80$), but with most equations depending on only a handful of unknowns, those affecting no common equation can be probed together, cutting the count to about seven at $S = 80$---a number set by how many neighbors couple, not by $S$. The parameter `use_sparse_FOC_jac` (default `True`) controls this; set it to `False` to use the legacy dense-finite-difference Jacobian on every call. The structure is derived in Appendix {ref}`SecAppDerivHHjac`.
2929

3030
```{figure} ./images/HH_jac_sparsity.png
3131
---

ogcore/default_parameters.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4394,15 +4394,15 @@
43944394
},
43954395
"use_sparse_FOC_jac": {
43964396
"title": "Flag to use a sparse (banded) finite-difference Jacobian in the household first order condition root finder",
4397-
"description": "Flag to use a sparse (banded) finite-difference Jacobian in the household first order condition root finder. When True, the sparsity pattern of the stacked Euler/labor first order conditions is auto-detected once per problem size and supplied to scipy.optimize.root, which then needs far fewer function evaluations per Jacobian build than the default dense finite differences. The solver falls back to the dense finite-difference Jacobian automatically if the Jacobian is not sparse enough to benefit or if a solve fails, so the result is unchanged.",
4397+
"description": "Flag to use a sparse (banded) finite-difference Jacobian in the household first order condition root finder. When True (the default), the sparsity pattern of the stacked Euler/labor first order conditions is auto-detected once per problem size and supplied to scipy.optimize.root, which then needs far fewer function evaluations per Jacobian build than the default dense finite differences. The solver falls back to the dense finite-difference Jacobian automatically if the Jacobian is not sparse enough to benefit or if a solve fails. Set to False to use the legacy dense Jacobian on every call.",
43984398
"short_description": "Use a banded finite-difference Jacobian in the household solver",
43994399
"param_notation": "$\\texttt{use_sparse_FOC_jac}$",
44004400
"section_1": "Model Solution Parameters",
44014401
"notes": "",
44024402
"type": "bool",
44034403
"value": [
44044404
{
4405-
"value": false
4405+
"value": true
44064406
}
44074407
],
44084408
"validators": {

tests/test_TPI.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,11 +388,15 @@ def test_params_to_array(tax_func_type):
388388
def test_inner_loop():
389389
# Test TPI.inner_loop function. Provide inputs to function and
390390
# ensure that output returned matches what it has been before.
391+
# Explicitly disable use_sparse_FOC_jac so this regression test
392+
# continues to exercise the legacy dense-finite-difference path
393+
# (the sparse path is covered by test_inner_loop_sparse_FOC_jac).
391394
input_tuple = utils.safe_read_pickle(
392395
os.path.join(CUR_PATH, "test_io_data", "tpi_inner_loop_inputs.pkl")
393396
)
394397
guesses, outer_loop_vars_old, initial_values, ubi, j, ind = input_tuple
395398
p = Specifications()
399+
p.update_specifications({"use_sparse_FOC_jac": False})
396400
r = outer_loop_vars_old[0]
397401
r_p = outer_loop_vars_old[2]
398402
w = outer_loop_vars_old[1]

0 commit comments

Comments
 (0)