Skip to content

N-NB-EGM: nested endogenous-grid solver for two-margin durable-choice models - #403

Merged
hmgaudecker merged 51 commits into
feat/nb-egmfrom
feat/nested-nbegm-ez
Jul 21, 2026
Merged

hmgaudecker merged 51 commits into
feat/nb-egmfrom
feat/nested-nbegm-ez

Conversation

@hmgaudecker

@hmgaudecker hmgaudecker commented Jul 12, 2026 •

Copy link
Copy Markdown
Member

Stacked on #400 (NB-EGM). Adds N-NB-EGM — an outer durable-margin grid search wrapping an inner 1-D NB-EGM (or DCEGM) solve — the pylcm solver for models with one endogenous liquid Euler margin and a second, non-convexly-adjusted durable/illiquid margin.

What this adds

  • NNBEGMInnerSpec — one vocabulary for a nested 1-D EGM inner configuration (inner Euler margin, post-decision function, savings grid), normalized from a DCEGM/NBEGM inner.
  • NNBEGM — the solver. Each period the outer margin d' is enumerated on a grid (a keeper branch holding d', an adjuster branch choosing it and paying an adjustment cost); conditional on d' the inner solver returns the liquid-margin policy by Euler inversion, and the outer envelope takes the max over d'. The inner cores are the NB-EGM: breakpoint-aware EGM solver for non-convex, discrete-continuous budgets #400 NB-EGM kernels, so any inner capability is inherited.

Applicability

N-NB-EGM is a general two-margin solver — durable/illiquid consumption-and-adjustment models, portfolio-with-transaction-cost problems, and the (S)Cars-style car-choice DP all fit its shape. Two capabilities exercised here as concrete tests:

  • Recursive (Epstein-Zin) preferences — inherited from the inner NB-EGM's certainty-equivalent support (NB-EGM: breakpoint-aware EGM solver for non-convex, discrete-continuous budgets #400), so a regime with a nonlinear certainty equivalent solves on N-NB-EGM.
  • A Kaplan-Violante-style acceptance test (test_n_nbegm_epstein_zin.py) — liquid margin + illiquid durable with a credited adjust move, Cobb-Douglas composite flow, stochastic survival (mortality), Epstein-Zin. The N-NB-EGM value weakly dominates and tracks (< 1% mean) a dense two-action GridSearch oracle — the established off-grid-EGM criterion, since grid-quantized brute is a lower bound for the durable choice.

Notes


Audit repair set merged from #400

The external adversarial audit of #400's Epstein-Zin implementation
(findings F1–F10, see the audit section in #400's description) is merged
down: anchored log-domain certainty-equivalent partials, per-node lottery
transforms (stochastic pre-fold gated off under EZ), the paired
value-and-gradient marginal read, build-time recursion gates, the
Cobb-Douglas unit-EIS limit, and the below-support-only carry-read
extrapolation. The N-NB-EGM keeper branch reads its continuation through
the same repaired seams. Full suite on this branch after the merge:
green (zero failures).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u

hmgaudecker and others added 4 commits July 12, 2026 11:34
The NEGM keeper/adjuster architecture generalized to an NB-EGM inner:

- the adjuster strips the outer post-decision transition and binds the
  outer value as a flat param per outer-grid node; the keeper injects the
  no-adjustment map, making the durable a passive ride-along state;
- inner adapters can expose several traced cores (the ride-along NB-EGM
  kernel splits into continuation and envelope cores), so the nested
  adapter republishes them under keeper:/adjuster: prefixes;
- the outer axis collapses by an fmax fold (an infeasible candidate's
  NaN-dead cells never poison a cell another candidate solves) and the
  published continuation is the pointwise (bridged) upper envelope of the
  candidates' carry rows on the shared liquid state grid — inner configs
  publishing jump-topology rows are rejected until the
  topology-preserving publication lands.

The smooth two-asset toy pins the wrapper: two-period agreement with
NEGM(inner=DCEGM) (tight away from the borrowing-constrained corner),
dense-brute consistency with matched choice sets, three-period tracking
through published carries, and exact outer-batch invariance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
…stein-Zin

Admit NestedNBEGM to the nonlinear-certainty-equivalent gate: its inner NB-EGM
solve already threads the Epstein-Zin context, the composite-flow Euler
inversion reads the fixed-durable flow's power structure, the regime-level
survival split takes the joint certainty equivalent, and the outer durable
choice is a plain max. On a two-asset EZ problem (liquid Euler margin, illiquid
durable with a credited outer move, Cobb-Douglas composite flow, stochastic
survival with a bequest) the nested value weakly dominates the dense grid-search
value everywhere and tracks it to under a percent on average — the endogenous
grid dissolving the durable/consumption grid quantization is the point of the
method.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
…dget

The nested toy exercises an inner NB-EGM whose budget declares no breakpoints,
matching the zero-breakpoint support the inner solver carries, so the nested
solve is validated against the empty-partition path as well as the kinked one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
@read-the-docs-community

read-the-docs-community Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

…ed out)

The solver, its inner-spec, period kernel, guards, and tests use the NNBEGM /
n-nb-egm name, parallel to NBEGM. Descriptive nesting language and the separate
NEGM (nested-EGM) solver are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
@hmgaudecker
hmgaudecker deleted the feat/nested-nbegm-ez branch July 12, 2026 15:00
@hmgaudecker hmgaudecker changed the title NestedNBEGM: two-asset durable-choice EGM under Epstein-Zin (Kaplan-Violante) N-NB-EGM: nested endogenous-grid solver for two-margin durable-choice models Jul 12, 2026
hmgaudecker and others added 6 commits July 12, 2026 17:59
The keeper's no-adjustment durable law may depend on more than the stock: a
permanent-income deflator keep(car, growth) = (1-vs*delta)*car/growth reads the
durable and a growth node. _durable_keeper_transition now introspects the map's
signature and threads all its declared arguments (copying each annotation from
the map), so the engine resolves them from the regime's DAG. A map reading only
the durable is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
…n seam too

The keeper wires its no-adjustment map into the model at two seams: the durable
state transition (`_durable_keeper_transition`) and the econ-function DAG's outer
post-decision (`_with_no_adjustment_outer_function`). The transition already
threads every argument the map declares; the DAG seam still hard-coded a
durable-only call, so a keeper that reads a further node — e.g. a permanent-income
growth factor `keep(car, growth)` — dropped that argument and failed to resolve.

Generalize the DAG seam to introspect the map's signature and thread every
declared argument (forcing the durable to `ContinuousState`), mirroring the
transition seam. The `no_adjustment_func is None` identity branch is unchanged:
`next_<durable>` still declares only the durable stock and holds it.

Also greens the branch's type-check: the keeper tests cast their toy functions to
the `EconFunction`/`EconFunctionsMapping` protocols, and the two-asset EZ model
test annotates its constraints mapping so `|=` type-checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
# Conflicts:
#	src/_lcm/user_regime_validation.py
@hmgaudecker hmgaudecker reopened this Jul 13, 2026
hmgaudecker and others added 3 commits July 13, 2026 19:58
- The certainty-equivalent build gates (PowerMean-only, H_epstein_zin
  required) apply to NNBEGM regimes as well as NBEGM ones: the nested
  solver runs the same EZ inner kernel per outer candidate, so a
  linear-in-probabilities CE or a custom H is exactly as wrong there.
  Rejection messages name the concrete solver class.
- The nested-vs-dense-brute tests assert unsigned relative gaps (tight
  mean, loose per-cell cap) instead of one-sided dominance: the two
  solvers optimize over different outer candidate sets by construction
  (post-decision grid with off-grid inner policy vs state-dependent
  action grid with grid-quantized consumption), so neither value
  weakly dominates the other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
@hmgaudecker

Copy link
Copy Markdown
Member Author

Round-4 external audit repairs (ledger: pr400-round4/REPAIRS.md in the audit archive):

  • F2 (serious) — the PowerMean-only / H_epstein_zin-required build gates now bind for NNBEGM regimes too (the nested solver runs the same EZ inner kernels per outer candidate); two failing-first rejection tests (5bfabd7e).
  • F3 (serious) — the nested-vs-dense-brute tests assert unsigned relative gaps instead of one-sided dominance: solve optimizes over the post-decision grid while simulate re-optimizes on the action grid, so the candidate sets are non-nested and no ordering exists. The deeper repair — storing the solved outer argmax (+ winning inner sim_policy) in KernelResult so simulate replays the solve's selection — is deferred as an explicit design decision.
  • F4 (moderate) — paper now states the bridged-only publication restriction for the nested outer carry (paper 447f2b3).
  • Full suite at dc9590d6 (merged with feat/nb-egm round-4 state): 2052 passed, 56 skipped, 2 xfailed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u

@github-actions

github-actions Bot commented Jul 14, 2026 •

Copy link
Copy Markdown

Benchmark comparison (main → HEAD)

Comparing 28d7d93a (main) → a0cddf44 (HEAD)

Benchmark Statistic before after Ratio Alert
aca-baseline execution time 13.527 s 15.176 s 1.12 ❌
peak GPU mem 588 MB 588 MB 1.00
compilation time 374.68 s 317.71 s 0.85
peak CPU mem 7.03 GB 7.47 GB 1.06
aca-baseline-debug execution time 58.332 s 58.912 s 1.01
peak GPU mem 587 MB 587 MB 1.00
compilation time 440.03 s 369.62 s 0.84
peak CPU mem 7.76 GB 7.44 GB 0.96
Mahler-Yum execution time 4.697 s 4.445 s 0.95
peak GPU mem 520 MB 520 MB 1.00
compilation time 11.33 s 10.96 s 0.97
peak CPU mem 1.58 GB 1.48 GB 0.93
Precautionary Savings - Solve execution time 24.2 ms 28.9 ms 1.19 ❌
peak GPU mem 8 MB 8 MB 1.00
compilation time 1.57 s 1.62 s 1.03
peak CPU mem 1.15 GB 1.07 GB 0.92
Precautionary Savings - Simulate execution time 61.9 ms 61.4 ms 0.99
peak GPU mem 157 MB 157 MB 1.00
compilation time 3.58 s 3.66 s 1.02
peak CPU mem 1.33 GB 1.22 GB 0.92
Precautionary Savings - Solve & Simulate execution time 94.9 ms 93.5 ms 0.99
peak GPU mem 566 MB 566 MB 1.00
compilation time 4.75 s 4.94 s 1.04
peak CPU mem 1.31 GB 1.20 GB 0.92
Precautionary Savings - Solve & Simulate (irreg) execution time 202.7 ms 204.0 ms 1.01
peak GPU mem 2.18 GB 2.18 GB 1.00
compilation time 5.11 s 5.24 s 1.03
peak CPU mem 1.37 GB 1.27 GB 0.93
IskhakovEtAl2017DCEGMSimulate execution time 238.9 ms
compilation time 4.49 s
peak CPU mem 1.61 GB
IskhakovEtAl2017DCEGMSolve execution time 1.722 s
compilation time 7.48 s
peak CPU mem 1.50 GB
IskhakovEtAl2017Simulate execution time 200.6 ms 220.4 ms 1.10
compilation time 4.18 s 4.34 s 1.04
peak CPU mem 1.29 GB 1.20 GB 0.93
IskhakovEtAl2017Solve execution time 44.5 ms 53.0 ms 1.19 ❌
compilation time 0.70 s 0.69 s 0.98
peak CPU mem 1.15 GB 1.05 GB 0.92
IskhakovEtAl2017DCEGMSimulateGpuPeakMem peak GPU mem 282 MB
IskhakovEtAl2017DCEGMSolveGpuPeakMem peak GPU mem 0 MB
IskhakovEtAl2017SimulateGpuPeakMem peak GPU mem 281 MB 281 MB 1.00
IskhakovEtAl2017SolveGpuPeakMem peak GPU mem 67 MB 67 MB 1.00

hmgaudecker and others added 29 commits July 14, 2026 14:12
The NEGM outer-cost contract requires an inner resources function that
does not read the outer post-decision directly: the toy's NEGM variant now
declares `outer_cost="credited"` with the cost-free base
`resources_before_outer_cost`, and pylcm composes the identical resources
quantity at model build. The keeper argument-threading checks compare
recomputed arithmetic, so their tolerance scales with the active float
dtype's roundoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
The NNBEGM machinery moves out of the deleted _lcm/solution/solvers.py
into its own _lcm/solution/nnbegm.py in the upstream per-solver layout;
the multi-argument keeper threading lands in negm.py. The NNBEGM kernel
adopts the KernelResult vocabulary (continuation / simulation_policy)
and the renamed continuation mapping.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
…ithful crossing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNQkLG5QzXcgGdpgH6sN4u
@hmgaudecker
hmgaudecker merged commit 6fe6012 into feat/nb-egm Jul 21, 2026
7 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant