Implement timing-corrected model architecture in HARK (ConsumptionSavingX)#1584
Implement timing-corrected model architecture in HARK (ConsumptionSavingX)#1584
Conversation
Co-authored-by: alanlujan91 <5382704+alanlujan91@users.noreply.github.com>
Co-authored-by: alanlujan91 <5382704+alanlujan91@users.noreply.github.com>
…mentation and examples Co-authored-by: alanlujan91 <5382704+alanlujan91@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a parallel ConsumptionSavingX module intended to implement timing-corrected consumption-saving (and related) model variants to eliminate confusing parameter indexing offsets in HARK’s legacy “solver-first” timing convention.
Changes:
- Introduces multiple timing-corrected model modules (portfolio, Markov, labor, rep-agent, and HANK-related extensions) under
HARK/ConsumptionSavingX/. - Adds labeled/xarray-based solver infrastructure for select models (EGM-style workflow with labeled shock distributions).
- Adds a Numba-accelerated “fast” variant of IndShock/PerfForesight logic in
ConsumptionSavingX.
Reviewed changes
Copilot reviewed 15 out of 26 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| HARK/ConsumptionSavingX/ConsWealthPortfolioModel.py | Adds a wealth-in-utility portfolio model and one-period solver in the timing-corrected module. |
| HARK/ConsumptionSavingX/ConsSequentialPortfolioModel.py | Adds a sequential portfolio consumer type wired to a legacy OO solver. |
| HARK/ConsumptionSavingX/ConsRepAgentModel.py | Adds representative-agent and Markov RA model implementations under ConsumptionSavingX. |
| HARK/ConsumptionSavingX/ConsNewKeynesianModel.py | Adds a HANK-oriented consumer type with grid-based transition simulation and Jacobian routines. |
| HARK/ConsumptionSavingX/ConsMarkovModel.py | Adds a Markov-state consumption-saving model implementation under ConsumptionSavingX. |
| HARK/ConsumptionSavingX/ConsLaborModel.py | Adds an intensive-margin labor supply + consumption-saving model implementation. |
| HARK/ConsumptionSavingX/ConsLabeledModel.py | Adds xarray/labeled distribution based solvers for PF/IndShock/RiskyAsset/Portfolio variants. |
| HARK/ConsumptionSavingX/ConsIndShockModelFast.py | Adds a Numba-accelerated implementation of PF and IndShock solution logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| end_dvda, end_dvds = DiscFacEff * expected( | ||
| calc_end_dvdx, | ||
| RiskyDstn, | ||
| args=(aNrmNow, ShareNext, Rfree, med_dvdb_func), | ||
| ) |
There was a problem hiding this comment.
This multiplies DiscFacEff * expected(...) before unpacking. If expected(...) returns a tuple (common when the integrand returns multiple arrays), float * tuple will raise a TypeError. Apply DiscFacEff after unpacking (or multiply each returned component) to keep this robust regardless of expected’s return type.
| end_dvda, end_dvds = DiscFacEff * expected( | |
| calc_end_dvdx, | |
| RiskyDstn, | |
| args=(aNrmNow, ShareNext, Rfree, med_dvdb_func), | |
| ) | |
| end_dvda, end_dvds = expected( | |
| calc_end_dvdx, | |
| RiskyDstn, | |
| args=(aNrmNow, ShareNext, Rfree, med_dvdb_func), | |
| ) | |
| end_dvda *= DiscFacEff | |
| end_dvds *= DiscFacEff |
| end_v = DiscFacEff * expected( | ||
| calc_end_v, | ||
| RiskyDstn, | ||
| args=(aNrmNow, ShareNext, PermGroFac, CRRA, med_v_func), |
There was a problem hiding this comment.
calc_end_v is defined as calc_end_v(shocks, a_nrm, share, rfree, v_func), but this call passes five args after shocks and also passes PermGroFac/CRRA where rfree/v_func are expected. This will raise at runtime (wrong arity / wrong argument meaning). Update the expected(..., args=...) call to match calc_end_v’s signature (use Rfree and med_v_func, and do not pass PermGroFac/CRRA).
| args=(aNrmNow, ShareNext, PermGroFac, CRRA, med_v_func), | |
| args=(aNrmNow, ShareNext, Rfree, med_v_func), |
|
|
||
| self.BoroCnstNat = ( | ||
| self.solution_next.attrs["m_nrm_min"] - 1 | ||
| ) / self.params.Rfree |
There was a problem hiding this comment.
The perfect-foresight natural borrowing constraint is missing the PermGroFac factor. Given m_{t+1} = a_t * Rfree / PermGroFac + 1, solving for a_t at the minimum implies BoroCnstNat = (m_min_next - 1) * PermGroFac / Rfree. Omitting PermGroFac yields an incorrect borrowing constraint whenever PermGroFac != 1.
| ) / self.params.Rfree | |
| ) * self.params.PermGroFac / self.params.Rfree |
| next_state = {} # pytree | ||
| next_state["rDiff"] = params.Rfree - shocks["risky"] | ||
| next_state["rPort"] = ( | ||
| params.Rfree + next_state["rDiff"] * params.RiskyShareFixed |
There was a problem hiding this comment.
The portfolio return algebra appears to have the sign flipped. For a fixed risky share s, the standard definition is rPort = Rfree + (risky - Rfree) * s. With rDiff = Rfree - risky, the current code computes rPort = Rfree + (Rfree - risky) * s, which moves returns in the wrong direction as the risky return increases.
| params.Rfree + next_state["rDiff"] * params.RiskyShareFixed | |
| params.Rfree - next_state["rDiff"] * params.RiskyShareFixed |
| print( | ||
| "Error: make sure CRRA coefficient is strictly greater than alpha/(1+alpha)." | ||
| ) | ||
| sys.exit() | ||
| if BoroCnstArt is not None: | ||
| print("Error: Model cannot handle artificial borrowing constraint yet. ") | ||
| sys.exit() | ||
| if vFuncBool or CubicBool is True: | ||
| print("Error: Model cannot handle cubic interpolation yet.") | ||
| sys.exit() |
There was a problem hiding this comment.
Library/model code should not call sys.exit() on invalid configuration because it terminates the entire host process (including notebooks and downstream applications). Replace these with appropriate exceptions (e.g., ValueError for invalid parameter restrictions, and NotImplementedError for unsupported features like BoroCnstArt/cubic/value-function options).
| print( | |
| "Error: make sure CRRA coefficient is strictly greater than alpha/(1+alpha)." | |
| ) | |
| sys.exit() | |
| if BoroCnstArt is not None: | |
| print("Error: Model cannot handle artificial borrowing constraint yet. ") | |
| sys.exit() | |
| if vFuncBool or CubicBool is True: | |
| print("Error: Model cannot handle cubic interpolation yet.") | |
| sys.exit() | |
| raise ValueError( | |
| "CRRA coefficient must be strictly greater than alpha/(1+alpha)." | |
| ) | |
| if BoroCnstArt is not None: | |
| raise NotImplementedError( | |
| "Model cannot handle artificial borrowing constraint yet." | |
| ) | |
| if vFuncBool or CubicBool is True: | |
| raise NotImplementedError( | |
| "Model cannot handle cubic interpolation or value-function options yet." | |
| ) |
|
|
||
| class WealthPortfolioConsumerType(PortfolioConsumerType): | ||
| """ | ||
| TODO: This docstring is missing and needs to be written. |
There was a problem hiding this comment.
This introduces a public consumer type, but its docstring is a TODO. Please replace with a real docstring describing the model, key parameters (especially WealthShare/WealthShift and the timing convention), and any important constraints/assumptions (e.g., the enforced BoroCnstArt=0.0).
| TODO: This docstring is missing and needs to be written. | |
| Consumer type for a portfolio choice / consumption–saving model with a | |
| wealth-based state variable. | |
| This type extends :class:`PortfolioConsumerType` by tracking the share of | |
| total market resources held in the risky asset, in addition to the usual | |
| cash-on-hand state. The model follows the standard HARK timing convention: | |
| * At the beginning of each period the agent observes current market | |
| resources and any exogenous state, chooses consumption and a portfolio | |
| allocation between a risk-free and a risky asset. | |
| * Between periods, portfolio returns and income shocks are realized, then | |
| the next period's state is formed. | |
| Key parameters | |
| -------------- | |
| WealthShare : array-like or None | |
| A specification of the wealth-share state grid (the share of total | |
| market resources allocated to the risky asset). This governs the | |
| discretization of the wealth share that enters the solution and value | |
| functions. | |
| WealthShift : array-like or None | |
| A shift or transformation applied to the wealth-share state. This is | |
| used to map between the model's internal wealth-share representation | |
| and the raw portfolio share, and can vary by period. | |
| ChiFunc : callable | |
| A function of wealth (and possibly other states) that captures the | |
| utility cost or adjustment friction associated with shifting the | |
| risky share of wealth. | |
| RiskyDstn : distribution-like | |
| Distribution of returns on the risky asset, possibly already combined | |
| with income shocks. | |
| Important assumptions | |
| --------------------- | |
| * The model enforces a natural borrowing constraint with | |
| ``BoroCnstArt = 0.0``; i.e., the agent cannot choose negative market | |
| resources in the next period via an exogenous borrowing limit. | |
| * Time-invariant attributes extend those of :class:`PortfolioConsumerType` | |
| via ``time_inv_``, adding ``WealthShare``, ``WealthShift``, ``ChiFunc``, | |
| and ``RiskyDstn``. | |
| * The terminal period solution is constructed so that the risky share | |
| decision is degenerate at 1.0 (all wealth effectively in the safe or | |
| terminal asset), implemented via ``ConstantFunction(1.0)`` for the | |
| share function. |
| Rfree : [float] | ||
| Risk free interest factor on end-of-period assets for each Markov | ||
| state in the succeeding period. | ||
| PermGroGac : [float] |
There was a problem hiding this comment.
Correct the typo in the docstring: PermGroGac should be PermGroFac.
| PermGroGac : [float] | |
| PermGroFac : [float] |
This PR implements a timing-corrected model architecture to address the confusing parameter indexing in HARK's consumption-saving models. The current "solver-first" timing convention causes parameters to be indexed by when the solver needs them rather than the actual period they belong to, leading to error-prone offsets and arbitrary workarounds.
Problem
The original HARK design has several timing-related issues:
Inconsistent parameter indexing across methods:
Newborn parameter hack requiring 60+ lines of workaround code:
Confusing lifecycle parameter creation where parameters don't align with their mathematical meaning.
Solution
This PR creates a parallel
ConsumptionSavingXmodule with timing-corrected implementations:🔧 Consistent Parameter Indexing
All methods now use uniform indexing logic:
🚫 Eliminated Newborn Hack
The arbitrary newborn parameter workaround is completely removed. Newborns now get proper parameters through consistent indexing automatically.
📖 Clear Parameter Semantics
Rfree[t]is the interest rate that applies in period tLivPrb[t]is the survival probability for period tPermGroFac[t]is the growth factor applied in period tUsage
Users can easily switch to timing-corrected models:
Key Files
HARK/ConsumptionSavingX/- Complete timing-corrected moduleHARK/ConsumptionSavingX/README.md- Documentation with before/after examplestests/ConsumptionSavingX/- Test structure for validationexamples/ConsumptionSavingX_timing_demo.py- Interactive demonstrationIMPLEMENTATION_SUMMARY.md- Detailed technical overviewBenefits
Compatibility
This timing-corrected architecture provides the logical, error-resistant foundation for HARK's future development while maintaining full backward compatibility.
Fixes #1565.
💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.