Skip to content

[ifp_advanced] Align K with the (c, a) convention used across the EGM lectures - #1016

Merged
mmcky merged 1 commit into
mainfrom
fix-ifp-advanced-solve-model-naming
Jul 29, 2026
Merged

[ifp_advanced] Align K with the (c, a) convention used across the EGM lectures#1016
mmcky merged 1 commit into
mainfrom
fix-ifp-advanced-solve-model-naming

Conversation

@mmcky

@mmcky mmcky commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #762, which is now closed. That PR read the symptom correctly but fixed the wrong end of it; this one fixes the actual defect and is verified by execution.

What is wrong

K in ifp_advanced.md is written as K(a_in, c_in, ifp) -> (a_out, c_out), but solve_model — copied verbatim from ifp_egm.md — calls it as c_out, a_out = K(c_in, a_in, ifp). That is a swap on both the arguments and the unpacking, so the two cancel and the code is numerically correct. What it is not is readable: every name in solve_model ends up meaning the opposite of what it says. Its first parameter is documented as c_init: Initial guess of σ but is really the asset grid, and its first return value is the asset grid too. A student comparing the signature at line 441 against the call site at line 490 sees a flat contradiction, in a lecture whose entire point is that the source is read.

ifp_advanced.md is the only lecture in the family with this inversion. Both ifp_egm.md and ifp_egm_transient_shocks.md define K(c_in, a_in, ifp) -> (c_out, a_out), and their solve_model is otherwise byte-identical to this one. This lecture's own simulate_household and compute_asset_stationary also already take (c_vec, a_vec) in that order.

The fix

Swap K's first two parameters and its return order so it matches its siblings, leaving solve_model untouched, then update the five call sites to the (c, a) ordering everything else already uses. Ten lines, no change to any computation.

Verification

Executed the lecture end to end on jax 0.11.0 and diffed against main:

quantity main this branch
Gini coefficient 0.918535 0.918532
top 1% wealth share 0.8982 0.8982
median / p99 / min identical
policy arrays agree to 2.3e-05

Both display as 0.9185 in the lecture output, so nothing visible moves.

The 2.3e-05 residual is a side benefit rather than drift, and it is worth recording. solve_model measures convergence as max|c_out - c_in|. On main that expression landed on the asset grid, so it evaluated (s + c_out) - (s + c_in) in float32 — a cancellation whose granularity is one ulp of the grid maximum, which at s = 100 is exactly 7.629395e-06 against a tolerance of 1e-05. The stopping rule was measuring round-off, and main's reported final error is precisely that ulp. Measuring the consumption residual directly is well conditioned; it converges in 158 iterations instead of 160, and the two solutions differ well inside the solver's own tolerance.

Why #762 could not be merged

It applied the same five call-site edits without touching K, which reverses the x and y arguments of the jnp.interp in simulate_household. It raises nothing — consumption is monotone, so the interpolation is perfectly happy — it just returns nonsense. Executed, it gives a Gini of -0.9995 (outside the possible range of the statistic entirely), a mean wealth of -3389 and a minimum of -4.1e+07. Worth flagging as a reminder that a silently-passing notebook is not evidence of a correct one here.

… lectures

`K` in this lecture was written as `K(a_in, c_in, ifp) -> (a_out, c_out)`,
while `solve_model` — copied verbatim from `ifp_egm.md` — calls it as
`c_out, a_out = K(c_in, a_in, ifp)`. That double swap on both the arguments
and the unpacking left the code numerically correct but made every name in
`solve_model` mean the opposite of what it says: its first parameter was
really the asset grid and its first return value the asset grid too, despite
the signature and docstring claiming consumption.

`ifp_advanced.md` was the only lecture in the family with this inversion.
Both `ifp_egm.md` and `ifp_egm_transient_shocks.md` define
`K(c_in, a_in, ifp) -> (c_out, a_out)`, and their `solve_model` is otherwise
identical to this one. So the fix is to `K`, not to `solve_model`: swap its
first two parameters and its return order, then update the five call sites
to the `(c, a)` ordering the rest of the family — and this lecture's own
`simulate_household` and `compute_asset_stationary` — already use.

Verified by executing the lecture on jax 0.11.0 and comparing against main:

  Gini            0.918535 -> 0.918532   (0.9185 as displayed, unchanged)
  top 1% share    0.8982   -> 0.8982
  median / p99    identical
  policy arrays   agree to 2.3e-05, inside the solver's own 1e-05 tol

The residual difference is a side benefit rather than drift. `solve_model`
measures convergence as `max|c_out - c_in|`, which previously landed on the
*asset grid* and so computed `(s + c_out) - (s + c_in)` in float32 — a
cancellation whose granularity is one ulp of the grid maximum, exactly
7.629395e-06 at s = 100, against a tolerance of 1e-05. The criterion was
dominated by round-off. Measuring the consumption residual directly is
well conditioned and converges in 158 iterations rather than 160.

Supersedes #762, which changed the five call sites to this ordering without
touching `K`. That combination runs without raising but reverses the x and y
arguments of the `jnp.interp` in `simulate_household`; executed, it returns a
Gini of -0.9995 and a minimum wealth of -4.1e+07.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 03:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a long-standing readability defect in ifp_advanced.md by aligning the Coleman–Reffett operator K with the (c, a) argument/return convention used across the EGM lecture family, so variable names in solve_model match what they actually represent.

Changes:

  • Reordered K’s first two parameters to (c_in, a_in, ifp) and changed its return order to (c_out, a_out).
  • Updated the five solve_model call sites to pass (c_init, a_init) and unpack (c_out, a_out) consistently (including the timed .block_until_ready() target).

@mmcky
mmcky requested a review from jstac July 29, 2026 03:57
@mmcky

mmcky commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

@jstac a third set of eyes on this would be great if you have time. I am pretty sure this is correct now.

@jstac

jstac commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Looks good @mmcky , many thanks! pls merge when ready.

@github-actions

Copy link
Copy Markdown

📖 Netlify Preview Ready!

Preview URL: https://pr-1016--sunny-cactus-210e3e.netlify.app

Commit: 2fea917

📚 Changed Lectures


Build Info

@mmcky
mmcky merged commit 60af480 into main Jul 29, 2026
2 checks passed
@mmcky
mmcky deleted the fix-ifp-advanced-solve-model-naming branch July 29, 2026 04:08
@mmcky

mmcky commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

✅ Translation sync completed (zh-cn)

Target repo: QuantEcon/lecture-python.zh-cn
Translation PR: QuantEcon/lecture-python.zh-cn#212
Files synced (1):

  • lectures/ifp_advanced.md

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.

3 participants