diff --git a/.agents/prompts_local/20260715-0900h_prompt_notebook-and-binding-gate.md b/.agents/prompts_local/20260715-0900h_prompt_notebook-and-binding-gate.md new file mode 100644 index 000000000..d9394d963 --- /dev/null +++ b/.agents/prompts_local/20260715-0900h_prompt_notebook-and-binding-gate.md @@ -0,0 +1,241 @@ +# PROMPT (HARK repo): the powerlaw-extrapolation illustrative notebook + the binding-constraint gate refinement + +**Status:** PENDING owner release. **Vehicle (owner ruling, 2026-07-15): fold into the open +PR [econ-ark/HARK#1782](https://github.com/econ-ark/HARK/pull/1782)** — commits go on its +head branch `fix-aggshock-pf-decay-extrap`; push only on the owner's word. +**Audience:** an AI with NO prior knowledge of this program. Everything you need is stated +here or linked. Work in this checkout (`/home/shared/github/econ-ark/HARK-1782-powerlaw-decay`) +if you are on the owner's machine; otherwise clone econ-ark/HARK, check out +`fix-aggshock-pf-decay-extrap`, and use the PUBLIC links below wherever a local path is +marked (local). + +--- + +## 0. Background in one page (read this even if you read nothing else) + +Buffer-stock consumption theory proves two laws about the consumption function `c(m)` +(normalized market resources `m`) beyond any finite solution grid: + +- **Above** (high wealth): the "gap" below the perfect-foresight rule, + `g = c̄(m) − c(m)` with `c̄(m) = κ̲·(m + hNrm)`, decays as a power law + `g ∝ w̄^{−min(1, q↑)}` in total wealth `w̄ = m + hNrm`, where `q↑` is an eigenvalue + computed from primitives. Read: + (the + synthesis paper) — esp. §0–§2. +- **Below** (the borrowing-constraint end): consumption approaches the maximal-MPC line at + rate `q↓ = ρ` (the CRRA): `c = κ̄·m^e − K·(m^e)^{1+ρ}`, `m^e = m − mNrmMin`, + `κ̄ = 1 − ℘_eff^{1/ρ}Þ_R` (Theorem CE; extended to permanent shocks as Theorem CE-ψ with + a regime criterion). Read: the statement page §5 and §5b — + + (anchors: `#st-thm-ce`, `#st-thm-ce-psi`, `#st-prop-c1-psi`, `#st-rem-ce-regime`, + `#st-cor-c4`; fragments must be lowercase). +- The practical payoff — a small grid finished with these tails reproduces a vastly larger + grid's solution, and the tails matter INSIDE the solver (the Euler expectations), not + just on the returned policy — is demonstrated with figures at + . + **Your notebook reproduces the spirit of that page's experiment inside HARK.** + +**The h-convention trap (memorize):** HARK's `hNrm` EXCLUDES the current period's income; +the theory pages' `h` INCLUDES it (`h = hNrm + 1`). The PF rule is +`c̄(m) = MPCmin·(m + hNrm)`. When this prompt says "human wealth" for grid sizing, it means +**`hNrm`** unless it says otherwise; state the convention once in the notebook. + +## 1. What ALREADY EXISTS on this branch (do not re-implement) + +PR #1782's head (`1e3ae928` at authoring time) already contains, tested (full +`tests/ConsumptionSaving/` suite: 404 passed): + +- The **option surface** on `IndShockConsumerType`: `decay_extrap_form ∈ {None,'powerlaw'}` + (top tail; exponent auto-computed as `min(1, q_star)` via + `pf_decay.powerlaw_decay_params_from_agent`, overridable via `decay_extrap_Q`) and + `decay_extrap_form_lower ∈ {None,'kappabar'}` (bottom tail). Defaults `None` = + byte-identical stock behavior. Wiring: `pre_solve` swaps in + `solve_one_period_ConsIndShock_with_tails`; the tails act in BOTH roles (in-solve + expectations + returned policy). +- `HARK.interpolation.KappaBarTailInterp` (the Theorem-CE bottom tail; guards, `try_make`). +- `pf_decay.ce_psi_regime` (the Theorem CE-ψ regime gate; `ConstraintEndRegimeWarning`), + `pf_decay.aXtraMin_from_tail_tol`, `pf_decay.aXtraMax_from_tail_tol` (grid-design rules), + `pf_decay.powerlaw_tail_diagnostic` / `rel_gap_at` (anchor-quality diagnostics). +- The test port: `tests/ConsumptionSaving/test_powerlaw_extrap.py` (40 tests: nested-grid + fidelity, two-roles, regime gate, byte-zero pins platform-gated) — REUSE its patterns. +- `docs/CHANGELOG.md` bullets under 0.17.3(dev) describing the above. + +Your work is a **delta**: Tasks A–C below. + +## 2. TASK A — the binding-constraint gate (code change + tests) + +**Owner requirement (verbatim intent):** the bottom tail must be built whenever the +NATURAL borrowing constraint is the binding one — including when an artificial constraint +exists but is slack — and must NOT be built when the artificial constraint strictly binds. + +**Current behavior to change:** the landed code activates the bottom tail only when +`BoroCnstArt is None` and refuses whenever an artificial constraint is present (see the +refusal path in `solve_one_period_ConsIndShock_with_tails` and its test). That is more +conservative than the theory requires. + +**The precise gate:** per period, + + bottom tail active ⟺ BoroCnstArt is None OR BoroCnstNat ≥ BoroCnstArt + +- Equality counts as natural (the binding object is then the natural constraint and + Theorem CE applies). +- When `BoroCnstArt > BoroCnstNat` (artificial strictly binds): refuse, with a clear + warning — and the docstring must state the THEORY reason: the constraint end is then a + kink with MPC = 1 on the constrained segment, so Theorem CE's `MPC → κ̄` mechanism does + not operate there. +- **Per-period for lifecycle agents**: `BoroCnstNat` varies with age; the gate is evaluated + each backward step, so the tail may be active at some ages and not others. +- The tail's `m^e = m − mNrmMin` coordinate uses the natural constraint's value in the + allowed cases (it equals `mNrmMin` there by the max). + +**Tests (extend `test_powerlaw_extrap.py`):** three cases — no artificial constraint +(tail built; existing), artificial-but-slack `BoroCnstArt < BoroCnstNat` (tail BUILT — +the new behavior; assert fidelity on a small nested check), artificial-binding (clean +refusal + warning; solution equals the tails-off solution on that period). Plus one +lifecycle case where the gate flips across ages (assert per-period wrap types). Keep the +byte-zero default regression untouched and passing. + +## 3. TASK B — the analytic validity threshold `w̄₀` (small code + it feeds the notebook) + +The theorems are asymptotic: the one-step gap machinery is PROVEN for `w̄ ≥ w̄₀`, and `w̄₀` +has a fully explicit primitive formula in the proofs (written `x₀` there). From +`theory/powerlaw-decay/stage_A_proof.md` in the BufferStockTheory-Latest repo +(local: /home/shared/github/llorracc/BufferStockTheory-Latest; public: +), +display (5.0a)/(5.0) around line 251: + + x₀⁰ := max{ h + m̄, (8(ρ+1)ḡ/κ̲ + C₀)/Þ_Γ, 8(ρ+1)C₀/Þ_Γ, (h+1+C₀)/Þ_Γ, 2ζ } + x₀ := max{ x₀⁰, 2K̂ } + +Take EVERY constant's definition (`ḡ, C₀, ζ, m̄, K_R, K̂`) verbatim from that document's §5 +(the `K_R` display sits near line 308) — do not improvise. Note the document's own honesty +(statement.md Remark 7): these constants are **deliberately crude** — `x₀` certifies +validity with explicit constants; it is NOT where extrapolation first becomes accurate in +practice (empirically that happens much earlier). + +**Implement** `pf_decay.powerlaw_validity_threshold(...)` returning at least the K̂-free +`x₀⁰` (all five terms are trivial primitives) and, if you implement `K̂` too, the full +`x₀`; if you omit `K̂`, return `x₀⁰` with a documented note that the full threshold adds +`2K̂` (formula cited). THEOREM-REF pin to the stage_A display. Unit-test against a +hand-computed case. **Do NOT make it a refusal gate** — it is a diagnostic: the layered +criteria are (i) `w̄₀` = the guaranteed-validity floor (crude), (ii) +`aXtraMax_from_tail_tol` = the operative quality rule, (iii) "top knot above human wealth" +(`m ≳ h`) = the pedagogical intuition. The notebook presents all three in that order. + +## 4. TASK C — the illustrative Jupyter notebook + +**Location:** `examples/ConsumptionSaving/PowerlawExtrapolation.ipynb` (committed executed, +per the sibling examples' convention; keep total runtime ≤ ~3 minutes — HARK CI executes +example notebooks; every cell deterministic, no RNG without a seed). + +**Agent configuration (PINNED — the tolerance table below was computed on exactly this):** +preferences matching HAFiscal's **College-TOP** type with a **zero-income unemployment +atom** (owner ruling: preferences-only match, clean Theorem-CE bottom): `CRRA = 2.0`, +`DiscFac = 0.995714`, `Rfree = [1.01]`, `PermGroFac = [1 + 0.01958/4]` (quarterly), +`LivPrb = [1 − 1/160]`, **`PermShkStd = [0.003**0.5]`, `TranShkStd = [0.12**0.5]`** +(HAFiscal's quarterly volatilities — do NOT use HARK's 0.1/0.1 defaults, which drop +`q_star` to 0.43 and change every number below), `PermShkCount = TranShkCount = 7`, +`UnempPrb = 0.027`, `IncUnemp = 0.0` (⇒ `mNrmMin = 0`, `℘_eff = UnempPrb`), infinite +horizon (`cycles = 0`). Print the derived theory quantities up front via `pf_decay`: +`q_star` (measured on this config: **0.6727** < 1, so the realized top exponent is +`q_star` itself — say so), `MPCmin` (0.01021), `MPCmax = κ̄`, `hNrm` (**196.8**; BST's +h = hNrm + 1 = 197.8 — state the convention), `ce_psi_regime` (expect regime I), and +`powerlaw_validity_threshold`. EXPECT and explain a `NearResonanceWarning`: this +calibration's `λ_B = E[ψ²]/(ℛÞ_Γ)` sits within 1% of the `q* = 1` knife-edge, so the +`B_ψ` closed-form amplitude route is degraded — knot-matched amplitudes (the default) +are the right choice here; one sentence in the notebook. + +**The measured tolerance ↔ grid-top table (computed 2026-07-15 on the pinned config; +`tail_tol` = the RELATIVE consumption gap `(c̄−c)/c` at the knot = the certified error of +handing off to the PF rule there — i.e. what the powerlaw tail SAVES):** + +| top knot | aXtraMax ≈ | tail_tol (rel gap) | +|---|---|---| +| m = 1.5·h | 292 | 1.26e-1 | +| m = 2·h | 390 | **9.4e-2** | +| m = 3·h | 586 | 6.0e-2 | +| m = 4·h | 782 | **4.2e-2** | +| m = 24·h (≈ x₀⁰ scale) | 4,698 | 3.7e-3 | + +(h = h_BST = 197.8; inverse check: `aXtraMax_from_tail_tol(tol=9.4e-2)` returns the 2·h +knot to ~5%.) The notebook should present this table and its reading: at the owner's +4·h grid the PF rail's handoff error is ~4% of consumption — exactly the error the +powerlaw tail eliminates (the tail is value-matched at the knot, so ITS handoff error is +zero at the knot and grows only through amplitude mis-anchoring, which the fidelity +panels measure directly). + +**Narrative sections (with the MyST site links inline — lowercase fragments):** + +1. *What the theory says* — 3 short paragraphs + the links (§0's three bullets above), + including the owner's point stated correctly: *the theorem is asymptotic; the proofs' + explicit validity threshold `w̄₀` is computed below; in practice the extrapolation + anchor (the top gridpoint) should sit where market wealth exceeds human wealth — the + plots draw the `m = hNrm + 1` line. The hard-wired ex-ante recipe is + `aXtraMax = 2·h_BST` (primitive-computable); the quantitative check is EX POST: + report `tail_tol` after solving, and `aXtraMax_from_tail_tol` supports a + solve-measure-re-grid refinement when a target tolerance is demanded.* +2. *Three grid configurations, one truth:* + - **Truth**: one big-grid solve (e.g. `aXtraMax = 1e6`, `aXtraCount` a few thousand, + `aXtraNestFac` per the test port's pattern) — solved once, reused. + - **(G1) Failure case** (owner-specified): `aXtraMax = 4`, small `aXtraCount` (e.g. 16). + Show: the top knot sits at `m ≈ 5 ≪ hNrm ≈ 197` (pre-asymptotic anchor), the + extrapolated tail visibly departs the truth above the knot, AND — the teaching moment — + **HARK's own tooling predicts this**: `powerlaw_tail_diagnostic` flags the anchor, + `aXtraMax_from_tail_tol` reports the `aXtraMax` actually needed for a target + tolerance, and on the bottom side too few points violate the `#st-cor-c4` knot rule + with `aXtraMin_from_tail_tol` as the fix. + - **(G2) Owner's prescription**: grid extending to **4× human wealth** + (`aXtraMax ≈ 782 ≈ 4·h_BST` — state the h-convention), sensible `aXtraCount` + (e.g. 48). Per the table above the PF-rail handoff error there is ~4.2%; expect the + powerlaw tail to beat it by orders of magnitude — report the measured sup relative + errors above and below vs truth, and the improvement factor vs rails. + - **(G3) Guaranteed regime**: a grid whose top knot exceeds the computed `w̄₀ − hNrm` + (report the number; for these parameters expect `x₀⁰` of order tens of `h` — + dominated by the `8(ρ+1)ḡ/κ̲` term). Contrast: G2 already performs excellently + *empirically* though it sits below the *guaranteed* threshold — guarantee vs + practice, honestly displayed. +3. *The fidelity experiment* (the analog of the last experiment on the + extrapolators-in-practice page): for G2, solve with tails ON and OFF + (`decay_extrap_form(:_lower)` set vs `None`), evaluate both against the truth at truth + gridpoints **above the top knot and below the bottom knot**, and plot: (a) the gap + `g(w̄)` log–log above (truth line, small-grid knots, tail dashed, the `m = hNrm+1` and + `w̄₀` verticals); (b) `c/m^e → κ̄` and the `γ = κ̄m^e − c` slope-`(1+ρ)` law below; + (c) relative-error panels tails-vs-rails; (d) **MPC panels**: MPC → `MPCmin` from above + at the top, MPC → `κ̄` at the bottom (the most economically legible display). Reuse the + plotting/measurement patterns of `tests/ConsumptionSaving/test_powerlaw_extrap.py` + and, for style, the figure code in BufferStockTheory-Latest + `theory/powerlaw-decay/make_extrap_fidelity_figures.py` (local) — adapt, don't import. +4. *The binding-constraint gate demo* (Task A, user-facing): three cells — + no artificial constraint (tail on), `BoroCnstArt = −0.5` with a looser natural + constraint (tail STILL on — the new behavior), `BoroCnstArt = 0.2 > BoroCnstNat` + (warning + clean refusal; solution matches tails-off). One sentence each on why. +5. *Two-roles coda*: one small cell demonstrating in-solve vs evaluation-only attachment + (reuse the test port's helper) with the measured factor, and the takeaway sentence: + the tail must live inside the solver loop. + +## 5. The links package (curated; public first, local in parentheses) + +| What | Link | +|---|---| +| The synthesis paper (findings, intuition, translation tables) | | +| Theorem statements incl. CE / CE-ψ, anchors `#st-thm-ce`, `#st-thm-ce-psi`, `#st-cor-c4` | | +| The fidelity experiment page (fig7/fig8, two-roles) | | +| Mathematical derivations: top ladder / constants incl. `x₀` (5.0a) | BufferStockTheory-Latest repo `theory/powerlaw-decay/stage_A_proof.md` (local: /home/shared/github/llorracc/BufferStockTheory-Latest/...) | +| Constraint-end proofs (ψ≡1 and ψ-general) | `theory/powerlaw-decay/constraint_end_proof.md`, `constraint_end_proof_psi.md` (same repo) | +| Figure-generating code (styles + measurement patterns) | `theory/powerlaw-decay/make_extrap_fidelity_figures.py`, `make_ce_psi_fig9.py` (same repo) | +| Reference implementation + parameter map + HARK appendix | `theory/powerlaw-decay/powerlaw_extrap_lib.py`, `extrap_fidelity_notes.md` (same repo) | +| Pre-registered batteries + committed outputs | `theory/powerlaw-decay/verify_extrap_fidelity_checks.py`, `verify_ce_psi_checks.py` (+ `_out.txt`) | +| The PR this folds into | | + +## 6. Process, gates, and the report + +- Branch: commits on `fix-aggshock-pf-decay-extrap`, HARK-conventional messages; + CHANGELOG bullet(s) linking `#1782`; `ruff check` clean; new/changed files + ruff-formatted (existing dirty files keep their in-file style). +- Gates before you report: full `tests/ConsumptionSaving/` green (the 404 baseline + your + additions); the notebook executes top-to-bottom in a fresh kernel deterministically + within the runtime budget; the byte-zero default regression untouched and passing; + every link in the notebook resolves (fetch each once). +- **Push only on the owner's explicit word** — report first with: commits, test tallies, + the notebook's measured fidelity numbers (G1 vs G2 vs G3, above and below), the computed + `w̄₀` value, and any deviations with reasons. diff --git a/HARK/ConsumptionSaving/ConsAggShockModel.py b/HARK/ConsumptionSaving/ConsAggShockModel.py index 575fe9358..71b08e924 100644 --- a/HARK/ConsumptionSaving/ConsAggShockModel.py +++ b/HARK/ConsumptionSaving/ConsAggShockModel.py @@ -6,6 +6,8 @@ """ from copy import deepcopy +import logging +import warnings import numpy as np import scipy.stats as stats @@ -48,6 +50,7 @@ UpperEnvelope, VariableLowerBoundFunc2D, ) +from HARK.ConsumptionSaving.pf_decay import PFDecayGridWarning from HARK.metric import MetricObject from HARK.rewards import ( CRRAutility, @@ -82,6 +85,445 @@ utility_inv = CRRAutility_inv +# --------------------------------------------------------------------------- +# Perfect-foresight asymptote bounds for decay extrapolation (opt-in) +# --------------------------------------------------------------------------- +# The 2D aggregate-shock consumption solve builds each per-Markov-state cFunc +# slice as a piecewise-linear interpolation that, above its top gridpoint, +# extrapolates with the last segment's slope (naive linear). The theoretically +# correct behavior is to decay toward the affine perfect-foresight (PF) +# asymptote c(m) -> MPCmin * (m + hNrm). These helpers compute that asymptote's +# slope (MPCmin) and human-wealth intercept (hNrm) at a *reference* capital +# ratio, because in this general-equilibrium model the gross return R = Rfunc(k) +# and wage w = wFunc(k) are endogenous -- there is no scalar Rfree in the solver +# (unlike the 1D ConsIndShock/ConsMarkov solvers, which already decay). The +# bounds are used only when the consumer type's ``MPCmin`` and ``hNrm`` +# attributes are set (both default to None); otherwise the legacy +# bare-LinearInterp path is preserved byte-for-byte. +# +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §2. Model, conditions, and the imported foundations :: L3 (level convergence) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] +# The decay target is the theorem's gap g(x) = kappa*(m + h) - c(m), x = m + h: +# g is sandwiched 0 <= g <= kappa*h and g(x) -> 0 (L3, level convergence), so a +# correct extrapolation must rejoin the PF line from below — which naive-linear +# (last-segment-slope) extrapolation never does. + + +def pf_mpc_min(Rfree, DiscFac, CRRA, LivPrb=1.0): + """Limiting (minimum) MPC of the perfect-foresight consumer. + + ``MPCmin = 1 - (Rfree * DiscFac * LivPrb) ** (1/CRRA) / Rfree`` + + This is HARK's standard return-impatience MPC bound (cf. the ``MPCminNow`` + recursion in ConsMarkovModel), specialized to a return ``Rfree`` that is + uniform across Markov states -- the relevant case here, since the bound is + evaluated at a single reference capital ratio. Survival enters through the + effective discount factor ``DiscFac * LivPrb`` (NOT through human wealth). + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/statement.md :: 4. Remarks :: Mortality :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Mortality-as-impatience (Remark 9, perpetual-youth): survival L replaces + # beta by beta*L inside the patience factors only — here MPCmin via the + # effective discount factor — while human wealth h and Rcal = R/Gamma stay + # mortality-free, exactly this helper pair's split. + + Warns and returns a value ``<= 0`` if return impatience fails, which the + caller treats as "no valid PF bound" (legacy extrapolation). + """ + patience = (Rfree * DiscFac * LivPrb) ** (1.0 / CRRA) / Rfree + if patience >= 1.0: + warnings.warn( + f"pf_mpc_min: patience factor {patience:.6f} >= 1 (return impatience " + "violated); MPCmin <= 0, PF decay bound is undefined." + ) + return 1.0 - patience + + +def pf_human_wealth_markov(MrkvArray, Rfree, ExpIncNext, PermGroFac): + """Per-Markov-state normalized human wealth at a reference return. + + Solves the joint fixed point (the Markov generalization of the + perfect-foresight recursion ``h = (G/R)(E[income] + h')``):: + + h_i = sum_j MrkvArray[i, j] * (G_j / R) * (ExpIncNext_j + h_j) + + i.e. ``(I - D) h = D @ ExpIncNext`` with ``D[i, j] = MrkvArray[i, j] * G_j / R``. + Under a return ``R`` that is uniform across states, this is exactly HARK's + risk-adjusted Markov human-wealth recursion (ConsMarkovModel.py ~554-559): + its ``R**(1-CRRA) / R_adj`` factors collapse to ``G_j / R`` when ``R`` is + state-invariant. The "remain-in-this-state-forever" limit would be + degenerate for zero-income deep-unemployment states; this joint solve is the + correct PF human-wealth limit. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/ADVERSARIAL_TESTING_GUIDE.md :: 5. LANDMINES — documented evaluation traps and silent-pass hazards :: The `h` human-wealth convention] + # This fixed point EXCLUDES current income — the theorem's h-convention + # (h = h_BST - 1, matching HARK's solver-side calc_human_wealth). Do NOT + # swap in BST's h_BST = R/(R-Gamma), which INCLUDES current income: it + # would shift hNrm up by one period's expected income (the guide's + # documented spurious-refutation trap). + + Existence requires the spectral radius of ``D`` to be below 1 -- the + finite-human-wealth condition (the Markov FHWC). If it fails (e.g. a state + with ``G_j >= R``), human wealth is infinite/undefined and an array of NaNs + is returned, which the caller treats as "no valid PF bound" (legacy + extrapolation). + + Parameters + ---------- + MrkvArray : np.array, shape (S, S) + Macro-state transition matrix (rows sum to 1). Use ``[[1.0]]`` for the + single-aggregate-state case. + Rfree : float + Gross return at the reference capital ratio (uniform across states). + ExpIncNext : np.array, shape (S,) + Expected next-period normalized labor income by *arrival* state, i.e. + ``wRef * E[PermShk * TranShk | state]`` where ``wRef`` is the + general-equilibrium wage at the reference capital ratio. + PermGroFac : np.array, shape (S,) + Total deterministic permanent growth by state (the individual + ``PermGroFac`` times the macro-state aggregate growth ``PermGroFacAgg``). + + Returns + ------- + hNrm : np.array, shape (S,) + Per-state normalized human wealth, or all-NaN if the FHWC fails. + """ + M = np.asarray(MrkvArray, dtype=float) + S = M.shape[0] + G = np.asarray(PermGroFac, dtype=float).reshape(S) + E = np.asarray(ExpIncNext, dtype=float).reshape(S) + D = M * (G / Rfree)[None, :] + spec_rad = float(np.max(np.abs(np.linalg.eigvals(D)))) + if spec_rad >= 1.0: + warnings.warn( + f"pf_human_wealth_markov: spectral radius of the growth-discounted " + f"transition is {spec_rad:.6f} >= 1 (finite-human-wealth condition " + "violated); human wealth is infinite, PF decay bound is undefined." + ) + return np.full(S, np.nan) + return np.linalg.solve(np.eye(S) - D, D @ E) + + +_pf_decay_logger = logging.getLogger(__name__) + +# One amplitude-ratio log line per decay_theory params object (id-keyed; a +# recycled id after gc at worst re-logs -- this is a log-dedup, not logic). +_AMPLITUDE_RATIO_LOGGED = set() + +# NOTE: the former ('amplitude', B) decay_Q mode (closed-form-amplitude tail +# with a guarded level jump at the top knot) was REMOVED 2026-07-11 by design +# ruling: LEVEL CONTINUITY AT THE TOP KNOT IS AN INVARIANT of the decay +# machinery -- a forced level discontinuity is never acceptable (the measured +# jump at a pre-asymptotic top knot was order +138% of the local gap). The +# boundary value B_psi keeps its diagnostic role (the amplitude-ratio log +# below); an exponent-1 level-matched tail is decay_Q=1.0. + + +def make_cFunc_slice(m_temp, c_temp, MPCmin=None, hNrm=None, decay_form="powerlaw", + decay_theory=None, decay_Q="theory", decay_terms=2): + """Build one per-Mgrid consumption slice, optionally with PF decay extrapolation. + + When ``MPCmin`` and ``hNrm`` are both supplied (not None), the returned + ``LinearInterp`` decays above its top gridpoint toward the affine + perfect-foresight asymptote ``c(m) -> MPCmin * (m + hNrm)`` + (``slope_limit=MPCmin``, ``intercept_limit=MPCmin*hNrm``). Otherwise it is + the legacy bare ``LinearInterp(m_temp, c_temp)`` with naive constant-slope + extrapolation -- byte-for-byte the prior behavior. + + The decay uses ``decay_form`` (default ``'powerlaw'``): the gap below the + PF asymptote of a buffer-stock consumption function decays as a POWER LAW + in ``(m + hNrm)``, not exponentially, so ``LinearInterp``'s legacy ``'exp'`` + form (available here by passing ``decay_form='exp'``) vanishes far too fast + above the grid -- it hands back the PF line itself where the true function + is still measurably below it. The power-law tail matches the level and the + slope of the solved slice at its top knot, exactly as the exponential does, + so the switch needs no extra parameters. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The power-law gap tail g ~ C*(m+h)**(-q), q = min(1, q*), is the + # theorem's extrapolation form of record; the exponential heuristic it + # replaces is not merely inaccurate but impossible as an asymptotic form + # (Prop A0). + + Theory-informed tails (``decay_theory``, ``decay_Q``) + ----------------------------------------------------- + ``decay_theory`` is an optional ``pf_decay.PowerLawDecayParams`` for this + agent's primitives (one object covers every Markov state and M-slice that + shares (R, Gamma, beta_eff, rho, psi) -- the theory quantities do not + depend on income levels or the aggregate law of motion). ``decay_Q`` + selects the tail exponent policy: + + * ``decay_theory=None`` (default): byte-for-byte the prior PR behavior + (the fitted power-law tail; ``decay_Q`` is inert without theory). + * ``decay_theory`` supplied, ``decay_Q='theory'`` (the DEFAULT): attach the + power-law tail with the THEORY exponent ``q = min(1, q_star)``, + level-matched at the top knot (explicit-Q semantics of ``LinearInterp``). + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §0 "What is q*? (and why min(1, q*))" :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # min(1, q*) is the realized asymptotic decay exponent of the gap below + # the PF asymptote: the here-and-now 1/x precautionary component vs the + # discounted near-target x^(-q*) component, whichever fades slower. + + Measured tradeoff (phase-1, pre-registered): on reachable grid windows at + the near-resonance calibrations, theory-exponent tails were measured + 1.3-3.8x LESS accurate than the fitted tangent (the local exponent + migrates toward min(1, q*) only over hundreds of descent shells), while + both beat the exponential form by 2.6-8x. The fitted exponent, however, + is a noisy 2-knot estimator: it is grid-non-monotone (measured + 0.51/0.46/0.53 across truncation depths vs q = 0.59), it badly + understates the q* > 1 closed-form amplitude as an asymptotic statement + (42% of B_psi at the HAFiscal grid top -- final_proof Fig 2a), and + nothing intrinsic stops a pathological top knot from producing an + asymptotically impossible exponent > 1. The theory exponent is the + principled, grid-independent default; flipping back to the guarded fit + is the one-line ``decay_Q=None``. + * ``decay_Q=None`` WITH ``decay_theory``: 'theory-guarded fit' -- the + fitted exponent is used but CLAMPED to the ceiling ``min(1, q_star)`` + (with a ``PFDecayGridWarning`` when the clamp bites; inert on healthy + solves, where every measured fitted exponent sat below the ceiling). + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/statement.md :: Proposition A0 (no exponential decay — GIC-free) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Prop A0 impossibility floor: the true gap can never decay faster than + # 1/x (any o(1/x) decay, exponential included, is impossible); with the + # realized exponent min(1, q*) (Theorem A1/B1), a fitted exponent above + # that ceiling is theory-infeasible and signals a coarse or + # non-converged grid top; the clamp is the hard cap. + * ``decay_Q=``: explicit exponent, passed through to + ``LinearInterp(decay_extrap_Q=...)`` (level-matched; under the default + ``decay_terms=2`` also slope-matched/C1 — the one-term variant's + documented C1 kink ``(Q_fit - Q)*A/pivot`` exists only at + ``decay_terms=1``). + * ``decay_Q=('amplitude', B)``: REMOVED (raises ``ValueError``). This mode + attached the closed-form-amplitude tail ``gap = B/(x+h)`` with a + guarded level JUMP at the top knot; by design ruling (2026-07-11), + level continuity at the top knot is an INVARIANT of the decay + machinery, so a forced discontinuity is never attachable -- at a + pre-asymptotic top knot it was measured at order +138% of the local + gap. Use ``decay_Q=1.0`` for a level-matched exponent-1 tail; the + boundary value retains its diagnostic role via the amplitude-ratio + log line (below). + + ``decay_terms`` (default 2) selects the explicit-exponent attachment: + ``2`` = the C1 TWO-TERM tail (level- AND slope-matched with the theory + exponent leading; ``LinearInterp(decay_extrap_terms=2)``), ``1`` = the + one-term level-matched tail with the C1 kink. The two-term DEFAULT + exists to guard against Jacobian problems in SSJ-type (sequence-space + Jacobian) approaches: policy derivatives are primitive inputs to SSJ + Jacobian/fake-news construction and to differentiation through the + solution, and a C1 kink at the attachment point makes them + discontinuous for queries crossing it. Inert without an explicit + exponent (the fitted forms are inherently slope-matched). + + Rescue: with an explicit exponent available (theory default, guarded fit's + ceiling, or a float), the branches where the FITTED form must disable decay + (top slope at or below ``MPCmin`` with the knot still below the line -- + previously naive-linear extrapolation that never rejoins the PF line) + instead attach the level-matched explicit-Q tail. + + When ``decay_theory`` is supplied the returned interpolant carries a + ``decay_theory`` metadata dict (``q_star``, ``q``, ``Q_used``, ``Q_fit``, + ``B_psi``, ``lambda_B``, ``near_resonance``), and at ``q* > 1`` the + fitted-vs-closed-form amplitude ratio ``A*(x_top+h)/B_psi`` is logged once + per params object (the Fig-2a pre-asymptotic understatement, visible in + production logs instead of silently absorbed). NOTE: ``B_psi`` is in the + theorem's E[income] = 1 units; the ratio is unit-consistent only when the + slice's income scale is ~1 (wage-scaled slices shift it by the square of + the income unit). + + Concavity guard (Carroll & Kimball, 1996). The true consumption function is + strictly concave and lies strictly *below* the PF line ``MPCmin*(m+hNrm)`` at + every finite m, approaching it from below with slope falling to ``MPCmin`` + from above. So in a converged solve the top knot sits below the line + (``level_diff > 0``) with ``slope_top > MPCmin``; decay is engaged only then, + which also guarantees the decay rate ``B = (slope_top - MPCmin)/level_diff`` + is strictly positive (a genuine decay, never an exponential blow-up). A top + knot *meaningfully above* the line (``level_diff < -tol``) whose top slope has + already fallen to ``MPCmin`` is theoretically impossible in a converged + solution -- it signals a broken asset grid, an incorrect reference + ``MPCmin``/``hNrm``, or a non-converged solve -- so we raise rather than + silently extrapolate. An above-line knot whose slope is still well above + ``MPCmin`` is an ordinary pre-asymptotic backward-induction transient (HARK's + aggregate-shock solve starts from ``c = m``, above the line) and is left to + the legacy extrapolation for that slice. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §2. Model, conditions, and the imported foundations :: Carroll–Kimball 1996 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Imported foundations L0–L2′: c is strictly increasing and strictly + # concave (Carroll–Kimball 1996), the gap obeys 0 <= g <= kappa*h with c + # approaching the PF line from below and c' falling to kappa from above — + # exactly the top-knot configuration this guard enforces. + """ + if isinstance(decay_terms, bool) or decay_terms not in (1, 2): + raise ValueError( + "decay_terms must be 1 or 2, got " + repr(decay_terms) + ) + if MPCmin is None or hNrm is None: + return LinearInterp(m_temp, c_temp) + + intercept_limit = MPCmin * hNrm + m_top = m_temp[-1] + c_top = c_temp[-1] + # PF line minus the solved consumption at the top knot; > 0 means c is below + # the line (the theoretically required configuration). + level_diff = intercept_limit + MPCmin * m_top - c_top + tol = 1e-8 * max(1.0, abs(c_top)) + slope_top = (c_temp[-1] - c_temp[-2]) / (m_temp[-1] - m_temp[-2]) + + if level_diff < -tol and slope_top <= MPCmin + 1e-12: + raise ValueError( + "ConsAggShockModel PF decay: the top consumption knot at " + f"m={m_top:.6g} (c={c_top:.6g}) lies above the perfect-foresight line " + f"MPCmin*(m+hNrm)={intercept_limit + MPCmin * m_top:.6g} while its top " + f"slope {slope_top:.6g} has already fallen to MPCmin={MPCmin:.6g}. By " + "the Carroll-Kimball (1996) concavity of the consumption function this " + "is impossible in a converged solution; it signals a broken asset " + "grid, an incorrect reference MPCmin/hNrm, or a non-converged solve. " + "Review the algorithm/parameters rather than extrapolating (HARK's " + "decay term would otherwise grow without bound here)." + ) + + # ----- resolve the tail policy (see the docstring's mode table) ----- + Q_explicit = None + if isinstance(decay_Q, tuple): + raise ValueError( + "decay_Q=('amplitude', B) was removed: level continuity at the " + "top knot is an invariant of the decay machinery (design ruling " + "2026-07-11) -- an externally imposed amplitude forces a level " + "jump there. Use decay_Q=1.0 for the level-matched exponent-1 " + f"tail. Got {decay_Q!r}" + ) + elif decay_Q is None: + mode = "guarded_fit" if decay_theory is not None else "legacy" + elif isinstance(decay_Q, str) and decay_Q == "theory": + if decay_theory is None: + mode = "legacy" # inert default: byte-for-byte prior behavior + elif np.isfinite(getattr(decay_theory, "q", float("nan"))): + mode = "explicit" + Q_explicit = float(decay_theory.q) + else: + # theory refused (FHWC/GIC): no finite exponent to attach; the + # Prop-A0 ceiling of 1.0 still guards the fit + mode = "guarded_fit" + elif isinstance(decay_Q, (int, float)) and not isinstance(decay_Q, bool): + mode = "explicit" + Q_explicit = float(decay_Q) + else: + raise ValueError( + "decay_Q must be 'theory', None, a positive float, or " + f"('amplitude', B); got {decay_Q!r}" + ) + if mode != "legacy" and decay_form != "powerlaw": + raise ValueError( + "decay_theory / non-default decay_Q require decay_form='powerlaw'" + ) + + # theory ceiling min(1, q*); q* nan (theory refused) leaves the GIC-free + # Prop-A0 floor exponent 1.0 as the only cap + ceiling = 1.0 + if decay_theory is not None: + q_th = getattr(decay_theory, "q", float("nan")) + if np.isfinite(q_th): + ceiling = min(1.0, float(q_th)) + + healthy = level_diff > tol and slope_top > MPCmin + # the fitted exponent LinearInterp would infer at this knot (diagnostic; + # may be <= 0 outside the healthy branch) + Q_fit = ( + (slope_top - MPCmin) * (m_top + hNrm) / level_diff + if abs(level_diff) > 0.0 + else float("nan") + ) + + Q_used = None + if mode == "legacy": + if healthy: + # byte-for-byte the prior PR behavior + f = LinearInterp( + m_temp, c_temp, intercept_limit, MPCmin, decay_extrap_form=decay_form + ) + else: + # Near the line, an above-line transient, or a slope already <= + # MPCmin: legacy naive-linear extrapolation (never blows up). + f = LinearInterp(m_temp, c_temp) + return f + + if mode == "explicit": + if level_diff > tol: + # healthy knot OR the rescue case (slope_top <= MPCmin): the + # explicit-Q relaxed guard attaches a level-matched decaying tail + # where the fitted form would have disabled decay entirely. + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # g ~ C*(m+h)**(-q), q = min(1, q*), is the asymptotically correct + # extrapolation form; unlike naive-linear it rejoins the PF line. + f = LinearInterp( + m_temp, c_temp, intercept_limit, MPCmin, + decay_extrap_form="powerlaw", decay_extrap_Q=Q_explicit, + decay_extrap_terms=decay_terms, + ) + Q_used = Q_explicit + else: + f = LinearInterp(m_temp, c_temp) + else: # guarded_fit + if healthy and Q_fit > ceiling: + warnings.warn( + f"make_cFunc_slice: fitted decay exponent Q={Q_fit:.3f} exceeds " + f"the theoretical ceiling min(1, q*)={ceiling:.3f}; clamped. " + "A top-knot slope this steep signals a coarse or non-converged " + "grid top.", + PFDecayGridWarning, + ) + f = LinearInterp( + m_temp, c_temp, intercept_limit, MPCmin, + decay_extrap_form="powerlaw", decay_extrap_Q=ceiling, + decay_extrap_terms=decay_terms, + ) + Q_used = ceiling + elif healthy: + # inert clamp: the ordinary fitted attach, unchanged + f = LinearInterp( + m_temp, c_temp, intercept_limit, MPCmin, + decay_extrap_form="powerlaw", + ) + Q_used = float(f.decay_extrap_Q) + elif level_diff > tol: + # rescue (see mode table): theory ceiling instead of naive-linear + f = LinearInterp( + m_temp, c_temp, intercept_limit, MPCmin, + decay_extrap_form="powerlaw", decay_extrap_Q=ceiling, + decay_extrap_terms=decay_terms, + ) + Q_used = ceiling + else: + f = LinearInterp(m_temp, c_temp) + + if decay_theory is not None: + f.decay_theory = dict( + q_star=decay_theory.q_star, + q=decay_theory.q, + Q_used=Q_used, + Q_fit=Q_fit, + terms=getattr(f, "decay_extrap_terms", None), + B_psi=decay_theory.B_psi, + lambda_B=decay_theory.lambda_B, + near_resonance=decay_theory.near_resonance, + ) + if (decay_theory.B_psi is not None and level_diff > tol + and id(decay_theory) not in _AMPLITUDE_RATIO_LOGGED): + _AMPLITUDE_RATIO_LOGGED.add(id(decay_theory)) + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The knife-edge window, quantified on HAFiscal's own numbers :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # At q* > 1 near resonance the solved amplitude at any feasible grid + # top badly understates the closed-form boundary value B_psi (42% of + # the way at the HAFiscal grid top); surface the ratio in logs so + # the pre-asymptotic understatement is visible, never absorbed. + _pf_decay_logger.info( + "PF decay (q* > 1): fitted-vs-closed-form amplitude ratio " + "A*(x_top+h)/B_psi = %.3f at the top knot (m_top=%.4g); " + "a ratio well below 1 is the expected pre-asymptotic " + "understatement of the boundary amplitude B_psi=%.4g.", + level_diff * (m_top + hNrm) / decay_theory.B_psi, + m_top, + decay_theory.B_psi, + ) + return f + + def make_aggshock_solution_terminal(CRRA): """ Creates the terminal period solution for an aggregate shock consumer. @@ -191,6 +633,11 @@ def solveConsAggShock( AFunc, Rfunc, wFunc, + MPCmin=None, + hNrm=None, + decay_theory=None, + decay_Q="theory", + decay_terms=2, ): """ Solve one period of a consumption-saving problem with idiosyncratic and @@ -307,7 +754,13 @@ def vPnextFunc(S, a, M): for j in range(Mcount): c_temp = np.insert(cNrmNow[:, j], 0, 0.0) # Add point at bottom m_temp = np.insert(mNrmNow[:, j] - BoroCnstNat_vec[j], 0, 0.0) - cFuncBaseByM_list.append(LinearInterp(m_temp, c_temp)) + cFuncBaseByM_list.append( + make_cFunc_slice( + m_temp, c_temp, MPCmin, hNrm, + decay_theory=decay_theory, decay_Q=decay_Q, + decay_terms=decay_terms, + ) + ) # Construct the overall unconstrained consumption function by combining the M-specific functions BoroCnstNat = LinearInterp( @@ -355,6 +808,11 @@ def solve_ConsAggMarkov( AFunc, Rfunc, wFunc, + MPCmin=None, + hNrm=None, + decay_theory=None, + decay_Q="theory", + decay_terms=2, ): """ Solve one period of a consumption-saving problem with idiosyncratic and @@ -591,11 +1049,19 @@ def solve_ConsAggMarkov( mNrmNow = aNrmNow_tiled + cNrmNow # Loop through the values in Mgrid and make a piecewise linear consumption function for each + # PF decay intercept uses the *current* macro state i's human wealth (None -> legacy). + hNrm_i = None if hNrm is None else hNrm[i] cFuncBaseByM_list = [] for n in range(Mcount): c_temp = np.insert(cNrmNow[n, :], 0, 0.0) # Add point at bottom m_temp = np.insert(mNrmNow[n, :] - BoroCnstNat_vec[n], 0, 0.0) - cFuncBaseByM_list.append(LinearInterp(m_temp, c_temp)) + cFuncBaseByM_list.append( + make_cFunc_slice( + m_temp, c_temp, MPCmin, hNrm_i, + decay_theory=decay_theory, decay_Q=decay_Q, + decay_terms=decay_terms, + ) + ) # Add the M-specific consumption function to the list # Construct the unconstrained consumption function by combining the M-specific functions @@ -831,8 +1297,18 @@ class AggShockConsumerType(IndShockConsumerType): "solver": solveConsAggShock, "track_vars": ["aNrm", "cNrm", "mNrm", "pLvl"], } + # Opt-in: above the top asset gridpoint, decay each cFunc slice toward the + # perfect-foresight asymptote c(m) -> MPCmin*(m + hNrm) instead of extrapolating + # with the last segment's slope. Both default to None -> legacy bare + # LinearInterp (byte-for-byte unchanged). To opt in, set BOTH attributes + # (e.g. via pf_mpc_min / pf_human_wealth_markov at a reference return of the + # caller's choosing); they are threaded to the solver as time-invariant + # parameters. Computing them is deliberately left to the caller: the return + # here is endogenous (R = Rfunc(k)), so no single in-model R is "the" right + # anchor for the PF asymptote. time_inv_ = IndShockConsumerType.time_inv_.copy() - time_inv_ += ["Mgrid", "AFunc", "Rfunc", "wFunc", "PermGroFacAgg"] + time_inv_ += ["Mgrid", "AFunc", "Rfunc", "wFunc", "PermGroFacAgg", "MPCmin", "hNrm"] + time_inv_ += ["decay_theory", "decay_Q", "decay_terms"] try: time_inv_.remove("vFuncBool") time_inv_.remove("CubicBool") @@ -851,6 +1327,30 @@ class AggShockConsumerType(IndShockConsumerType): def __init__(self, **kwds): AgentType.__init__(self, construct=False, **kwds) + # solve_one_cycle reads time_inv_ parameters from the INSTANCE dict + # (HARK/core.py, solve_dict), so the opt-in PF-decay bounds must exist + # there even when unused; None selects the legacy extrapolation path. + # A value passed via **kwds (assign_parameters) is preserved. + if "MPCmin" not in self.__dict__: + self.MPCmin = None + if "hNrm" not in self.__dict__: + self.hNrm = None + # Theory-informed tail policy (see make_cFunc_slice): decay_theory=None + # keeps the prior behavior byte-for-byte; decay_Q='theory' is inert + # without it. Set decay_theory to a pf_decay.PowerLawDecayParams built + # from this type's primitives (e.g. pf_decay.powerlaw_decay_params) to + # attach theory-exponent tails; one params object covers every Markov + # state and M-slice sharing (R, Gamma, beta_eff, rho, psi). Per-state + # PermGroFac heterogeneity would need a per-state params list, which is + # deliberately NOT built here. + if "decay_theory" not in self.__dict__: + self.decay_theory = None + if "decay_Q" not in self.__dict__: + self.decay_Q = "theory" + # 2 = the C1 two-term attachment (the default; SSJ-Jacobian guard, + # see make_cFunc_slice); 1 = the one-term level-matched tail + if "decay_terms" not in self.__dict__: + self.decay_terms = 2 def reset(self): """ diff --git a/HARK/ConsumptionSaving/ConsIndShockModel.py b/HARK/ConsumptionSaving/ConsIndShockModel.py index 0b803a520..447867ec3 100644 --- a/HARK/ConsumptionSaving/ConsIndShockModel.py +++ b/HARK/ConsumptionSaving/ConsIndShockModel.py @@ -13,6 +13,7 @@ See HARK documentation for mathematical descriptions of the models being solved. """ +import warnings from copy import copy import numpy as np @@ -39,6 +40,8 @@ expected, ) from HARK.interpolation import ( + DecayTailInterp, + KappaBarTailInterp, LinearInterp, LowerEnvelope, MargMargValueFuncCRRA, @@ -46,6 +49,11 @@ ValueFuncCRRA, ) from HARK.interpolation import CubicHermiteInterp as CubicInterp +from HARK.ConsumptionSaving.pf_decay import ( + ConstraintEndRegimeWarning, + ce_psi_regime, + powerlaw_decay_params_from_agent, +) from HARK.metric import MetricObject from HARK.rewards import ( CRRAutility, @@ -599,7 +607,7 @@ def calc_vpp_next(shock, a, rfree, crra, perm_gro_fac, vppfunc_next): ) -def solve_one_period_ConsIndShock( +def solve_one_period_ConsIndShock_with_tails( solution_next, IncShkDstn, LivPrb, @@ -611,9 +619,25 @@ def solve_one_period_ConsIndShock( aXtraGrid, vFuncBool, CubicBool, + decay_extrap_form=None, + decay_extrap_Q=None, + decay_extrap_form_lower=None, ): """Solves one period of a consumption-saving model with idiosyncratic shocks to - permanent and transitory income, with one risk free asset and CRRA utility. + permanent and transitory income, with one risk free asset and CRRA utility -- + plus the OPTIONAL theory-pinned tail extrapolators. + + This is the implementation behind ``solve_one_period_ConsIndShock`` (which + delegates here with the three decay options at ``None``, so the two are + byte-for-byte identical on the default path). It exists as a separate + entry point so that the long-standing public solver keeps its exact + signature: HARK's ``solve_one_cycle`` builds the solver's argument dict + from the argument NAMES, so adding keywords to the shared solver would + force every agent type that uses it (including internal re-solve paths + that skip ``pre_solve``, e.g. the HANK Jacobian machinery's + ``solve(presolve=False)``) to carry the new parameters. + ``IndShockConsumerType.pre_solve`` swaps this solver in -- and registers + the option parameters for delivery -- only when an option is enabled. Parameters ---------- @@ -646,6 +670,67 @@ def solve_one_period_ConsIndShock( included in the reported solution. CubicBool: boolean An indicator for whether the solver should use cubic or linear interpolation. + decay_extrap_form : str or None + Optional theory-pinned TOP tail for the consumption function. ``None`` + (default): byte-for-byte the existing behavior (the legacy exponential + decay toward the perfect-foresight asymptote above the top gridpoint). + ``'powerlaw'``: the gap below the asymptote ``MPCmin*(m + hNrm)`` + decays as the theorem-backed power law with explicit exponent + ``decay_extrap_Q`` (the C1 two-term attachment of the ``LinearInterp`` + / ``DecayTailInterp`` machinery). + + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/final_proof_myst.md :: eq-wbar-def :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The wealth coordinate of the gap law is wbar = m + hNrm: HARK's + # solver-side hNrm EXCLUDES current income (= BST's h - 1), so the + # PF asymptote cbar(m) = MPCmin*(m + hNrm) here IS the theory's + # kappa*(m - 1 + h_BST) -- the h-convention bridge; the gap decays + # as wbar**(-min(1, q*)). + + Because the assembled ``cFunc`` of + each backward step is what the NEXT step's Euler expectation + evaluates (``calc_vp_next`` overruns the grid top whenever a large + transitory draw lands there), the tail acts in BOTH roles at once: + inside the solution recursion and on the returned policy. + Theory: statement st-thm-A1 / eq-powerlaw; in practice see + https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/extrapolators-in-practice + decay_extrap_Q : float or None + REQUIRED with ``decay_extrap_form='powerlaw'``: the explicit tail + exponent, the theory value ``min(1, q_star)`` with ``q_star`` from + ``HARK.ConsumptionSaving.pf_decay`` (``powerlaw_decay_params`` / + ``powerlaw_decay_params_from_agent``; ``IndShockConsumerType`` + computes it automatically in ``pre_solve``). An explicit value is + demanded -- rather than falling back to ``LinearInterp``'s 2-knot + fitted exponent -- so that the option always carries the theory + exponent, never a noisy per-iteration fit. + decay_extrap_form_lower : str or None + Optional theory-pinned BOTTOM tail. ``None`` (default): byte-for-byte + the existing behavior (the EGM bottom SECANT between the constraint + corner and the first gridpoint). ``'kappabar'``: queries below the + first EGM gridpoint follow the Theorem CE constraint-end form + ``c = MPCmax*me - K*me**(1+CRRA)``, ``me = m - mNrmMin``, with the + solver's own analytic ``MPCmaxUnc`` (the Prop C2 recursion) and ``K`` + value-matched at the first gridpoint (``KappaBarTailInterp``); MPC + rises to ``MPCmax`` as the constraint is approached instead of the + secant's biased slope, in both roles (the expectation's worst-income + branch queries next period's cFunc below its first gridpoint). + Attaches ONLY when (i) the natural borrowing constraint is the BINDING + one -- ``BoroCnstArt is None`` OR ``BoroCnstNat >= BoroCnstArt`` (equiv. + ``BoroCnstNat >= mNrmMinNow``); an artificial constraint that STRICTLY + binds (``BoroCnstArt > BoroCnstNat``) makes the constraint end a kink + with MPC 1 on the constrained segment, so Theorem CE's + ``MPC -> kappa_bar`` mechanism does not operate there and the tail is + refused with a warning. A slack artificial constraint + (``BoroCnstArt < BoroCnstNat``) does NOT block the tail. The gate is + evaluated per backward step, so for a lifecycle agent whose + ``BoroCnstNat`` varies with age the tail may be active at some ages and + not others. (ii) the calibration passes the Theorem CE-psi regime gate + (``pf_decay.ce_psi_regime``; regime II or undetermined warns a + ``ConstraintEndRegimeWarning`` naming st-rem-CE-regime and REFUSES, + keeping the default secant), and (iii) the per-step knot passes + ``KappaBarTailInterp.try_make``'s corridor + MPC-range exposure gate. + Theory: statement st-thm-CE (q_down = CRRA), st-thm-CE-psi (psi- + general regime I), st-cor-C4 (bottom grid rule), at + https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/ Returns ------- @@ -653,6 +738,27 @@ def solve_one_period_ConsIndShock( Solution to this period's consumption-saving problem with income risk. """ + # Validate the optional decay-extrapolation surface (default None = the + # legacy behavior, byte-for-byte). + if decay_extrap_form not in (None, "powerlaw"): + raise ValueError( + "decay_extrap_form must be None or 'powerlaw', got " + + repr(decay_extrap_form) + ) + if decay_extrap_form_lower not in (None, "kappabar"): + raise ValueError( + "decay_extrap_form_lower must be None or 'kappabar', got " + + repr(decay_extrap_form_lower) + ) + if decay_extrap_form == "powerlaw" and decay_extrap_Q is None: + raise ValueError( + "decay_extrap_form='powerlaw' requires decay_extrap_Q, the " + "explicit theory exponent min(1, q_star) (pf_decay.powerlaw_" + "decay_params; IndShockConsumerType.pre_solve computes it " + "automatically at the agent level)" + ) + if decay_extrap_Q is not None and decay_extrap_form != "powerlaw": + raise ValueError("decay_extrap_Q requires decay_extrap_form='powerlaw'") # Define the current period utility function and effective discount factor uFunc = UtilityFuncCRRA(CRRA) DiscFacEff = DiscFac * LivPrb # "effective" discount factor @@ -736,17 +842,101 @@ def solve_one_period_ConsIndShock( ) else: # Construct the unconstrained consumption function as a linear interpolation - cFuncNowUnc = LinearInterp( - m_for_interpolation, - c_for_interpolation, - cFuncLimitIntercept, - cFuncLimitSlope, - ) + if decay_extrap_form == "powerlaw": + # Theory-pinned top tail, threaded through LinearInterp's own + # powerlaw decay machinery with the explicit exponent + # min(1, q_star); the limit args are the same PF asymptote the + # legacy exponential decay targets. + cFuncNowUnc = LinearInterp( + m_for_interpolation, + c_for_interpolation, + cFuncLimitIntercept, + cFuncLimitSlope, + decay_extrap_form="powerlaw", + decay_extrap_Q=decay_extrap_Q, + ) + else: + cFuncNowUnc = LinearInterp( + m_for_interpolation, + c_for_interpolation, + cFuncLimitIntercept, + cFuncLimitSlope, + ) # Combine the constrained and unconstrained functions into the true consumption function. # LowerEnvelope should only be used when BoroCnstArt is True cFuncNow = LowerEnvelope(cFuncNowUnc, cFuncNowCnst, nan_bool=False) + # Optional theory-pinned tails (decay_extrap_form / decay_extrap_form_lower; + # default None = byte-for-byte the assembly above). They are attached HERE, + # before vPfuncNow is built, because vPfuncNow = u'(cFuncNow(m')) is what + # the PREVIOUS backward step's Euler expectation evaluates (calc_vp_next): + # baking the tails into each period's cFuncNow serves both the in-solve + # role and the returned-policy role at once. + if decay_extrap_form == "powerlaw" and CubicBool: + # The cubic interpolant carries only the legacy exponential decay, so + # wrap the assembled function in the composable tail instead. x_cut is + # REQUIRED: cFuncNow is a LowerEnvelope, which exposes no x_list for + # the wrapper to infer the handoff point from. + cFuncNow = DecayTailInterp( + cFuncNow, + cFuncLimitIntercept, + cFuncLimitSlope, + x_cut=float(m_for_interpolation[-1]), + decay_extrap_form="powerlaw", + decay_extrap_Q=decay_extrap_Q, + ) + if decay_extrap_form_lower == "kappabar": + # Theorem CE bottom tail (KappaBarTailInterp), guarded three ways: + # (i) the natural constraint must be the binding one -- BoroCnstArt is + # None OR BoroCnstNat >= BoroCnstArt, i.e. BoroCnstNat >= mNrmMinNow + # (a slack artificial constraint does NOT block the tail; only one + # that STRICTLY binds does, since its constraint end is a kink with + # MPC 1 and no kappa_bar asymptote). Evaluated per step, so a + # lifecycle gate can flip across ages; + # (ii) the Theorem CE-psi regime gate (st-thm-CE-psi): regime II or + # undetermined means q_down = min(CRRA, s*_+) rather than CRRA + # (st-rem-CE-regime), so the tail is refused with a warning; + # (iii) the per-step corridor + MPC-range exposure gate inside + # try_make (None keeps the default secant for this step). + if BoroCnstNat < mNrmMinNow: + warnings.warn( + "decay_extrap_form_lower='kappabar': the artificial borrowing " + "constraint binds (BoroCnstNat < mNrmMin), so the constraint " + "end is a kink with MPC 1, not the kappa_bar asymptote; " + "keeping the default bottom segment.", + ConstraintEndRegimeWarning, + ) + else: + regime = ce_psi_regime(IncShkDstn, CRRA, PatFac * Rfree / PermGroFac) + if regime["regime"] != "I": + warnings.warn( + "decay_extrap_form_lower='kappabar': constraint-end " + "regime %s -- lambda(psi_min) = %.6g fails the Theorem " + "CE-psi uniform-contraction criterion (%s), so q_down = " + "min(CRRA, s*_+) rather than CRRA and the kappa_bar*me - " + "K*me**(1+CRRA) bottom tail is NOT theorem-backed " + "(st-rem-CE-regime; amplitude rigor open = GAP-CE-psi-II); " + "keeping the default bottom segment." + % ( + regime["regime"], + regime["lambda_min_fiber"], + regime["criterion"], + ), + ConstraintEndRegimeWarning, + ) + else: + kappabar_tail = KappaBarTailInterp.try_make( + cFuncNow, + MPCmaxUnc, + CRRA, + mNrmMinNow, + x_knot=float(m_for_interpolation[1]), + y_knot=float(c_for_interpolation[1]), + ) + if kappabar_tail is not None: + cFuncNow = kappabar_tail + # Make the marginal value function and the marginal marginal value function vPfuncNow = MargValueFuncCRRA(cFuncNow, CRRA) @@ -816,6 +1006,86 @@ def solve_one_period_ConsIndShock( return solution_now +def solve_one_period_ConsIndShock( + solution_next, + IncShkDstn, + LivPrb, + DiscFac, + CRRA, + Rfree, + PermGroFac, + BoroCnstArt, + aXtraGrid, + vFuncBool, + CubicBool, +): + """Solves one period of a consumption-saving model with idiosyncratic shocks to + permanent and transitory income, with one risk free asset and CRRA utility. + + Delegates to :func:`solve_one_period_ConsIndShock_with_tails` with the + optional decay-extrapolation surface at its ``None`` defaults, so the + solve is byte-for-byte the long-standing behavior. The signature is kept + EXACTLY as before on purpose: ``solve_one_cycle`` demands every named + solver argument from the agent, so this public solver must not grow + keywords (see the note in the ``_with_tails`` docstring). To enable the + theory-pinned tails, set ``decay_extrap_form`` / + ``decay_extrap_form_lower`` on an ``IndShockConsumerType`` (whose + ``pre_solve`` swaps the ``_with_tails`` solver in), or call + ``solve_one_period_ConsIndShock_with_tails`` directly. + + Parameters + ---------- + solution_next : ConsumerSolution + The solution to next period's one period problem. + IncShkDstn : distribution.Distribution + A discrete approximation to the income process between the period being + solved and the one immediately following (in solution_next). + LivPrb : float + Survival probability; likelihood of being alive at the beginning of + the succeeding period. + DiscFac : float + Intertemporal discount factor for future utility. + CRRA : float + Coefficient of relative risk aversion. + Rfree : float + Risk free interest factor on end-of-period assets. + PermGroFac : float + Expected permanent income growth factor at the end of this period. + BoroCnstArt: float or None + Borrowing constraint for the minimum allowable assets to end the + period with. If it is less than the natural borrowing constraint, + then it is irrelevant; BoroCnstArt=None indicates no artificial bor- + rowing constraint. + aXtraGrid: np.array + Array of "extra" end-of-period asset values-- assets above the + absolute minimum acceptable level. + vFuncBool: boolean + An indicator for whether the value function should be computed and + included in the reported solution. + CubicBool: boolean + An indicator for whether the solver should use cubic or linear interpolation. + + Returns + ------- + solution_now : ConsumerSolution + Solution to this period's consumption-saving problem with income risk. + + """ + return solve_one_period_ConsIndShock_with_tails( + solution_next, + IncShkDstn, + LivPrb, + DiscFac, + CRRA, + Rfree, + PermGroFac, + BoroCnstArt, + aXtraGrid, + vFuncBool, + CubicBool, + ) + + def solve_one_period_ConsKinkedR( solution_next, IncShkDstn, @@ -1969,6 +2239,11 @@ def calc_stable_points(self, force=False): "BoroCnstArt": 0.0, # Artificial borrowing constraint "vFuncBool": False, # Whether to calculate the value function during solution "CubicBool": False, # Whether to use cubic spline interpolation + # OPTIONAL THEORY-PINNED TAIL EXTRAPOLATION (None = legacy behavior, + # byte-for-byte; see solve_one_period_ConsIndShock) + "decay_extrap_form": None, # None or 'powerlaw': top tail above the grid + "decay_extrap_Q": None, # Explicit top-tail exponent; auto min(1, q_star) + "decay_extrap_form_lower": None, # None or 'kappabar': constraint-end tail } IndShockConsumerType_simulation_default = { # PARAMETERS REQUIRED TO SIMULATE THE MODEL @@ -2053,6 +2328,50 @@ class IndShockConsumerType(PerfForesightConsumerType): Whether to calculate the value function during solution. CubicBool: bool Whether to use cubic spline interpoliation. + decay_extrap_form: str or None + Optional theory-pinned TOP tail for the consumption function beyond + the grid top. ``None`` (default): the legacy exponential decay toward + the perfect-foresight asymptote, byte-for-byte. ``'powerlaw'``: the + gap below the asymptote decays as the theorem-backed power law with + exponent ``min(1, q_star)`` (Theorem A1 / eq-powerlaw of the + power-law-decay theory, + https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/ ), + computed automatically in ``pre_solve`` via + :mod:`HARK.ConsumptionSaving.pf_decay` and attached at every backward + step -- so it acts both INSIDE the solution recursion (the Euler + expectation evaluates next period's cFunc above its grid top) and on + the returned policy. Practical evidence and the two-roles finding: + https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/extrapolators-in-practice + decay_extrap_Q: float or None + Explicit top-tail exponent override (requires + ``decay_extrap_form='powerlaw'``). ``None`` (default): computed + automatically as ``min(1, q_star)`` from the agent's own primitives + in ``pre_solve``, refreshed each solve while it remains + auto-computed. The automatic value uses the t=0 primitives, so + lifecycle calibrations with time-varying ``Rfree``/``PermGroFac``/ + ``LivPrb``/shock processes that want period-specific exponents + should pass an explicit value instead. + decay_extrap_form_lower: str or None + Optional theory-pinned BOTTOM (constraint-end) tail. ``None`` + (default): the legacy EGM bottom secant, byte-for-byte. + ``'kappabar'``: below the first EGM gridpoint consumption follows + Theorem CE's ``c = MPCmax*me - K*me**(1+CRRA)`` with the solver's + analytic maximal MPC, so the MPC rises to ``MPCmax`` at the + constraint (st-thm-CE: the approach exponent is the CRRA itself; + psi-general regime I per st-thm-CE-psi, + https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/ ). + Guarded: attaches only when the NATURAL borrowing constraint binds + (note the IndShock default ``BoroCnstArt=0.0`` usually binds -- + set ``BoroCnstArt=None`` to use this option) and only in the Theorem + CE-psi contraction regime; regime II warns a + ``pf_decay.ConstraintEndRegimeWarning`` (st-rem-CE-regime) and keeps + the default bottom segment. The bottom grid-design rule st-cor-C4 + (``pf_decay.aXtraMin_from_tail_tol``) certifies knot placement. + NOTE (applies to both tail options): they are not yet supported with + ``ConsNewKeynesianModel.calc_jacobian`` -- its ghost agents solve + with ``presolve=False``, so a tails-enabled steady state would mix + with railed transition solves; keep the options at their ``None`` + defaults there. Simulation Parameters --------------------- @@ -2316,6 +2635,117 @@ def pre_solve(self): self.construct("solution_terminal") if not self.quiet: self.check_conditions(verbose=self.verbose) + self._setup_decay_extrap() + + def _setup_decay_extrap(self): + """Prepare the optional theory-pinned tail-extrapolation surface for + the solver (called from ``pre_solve``; a no-op stack of ``None``s by + default -- byte-for-byte the legacy solve). + + * Ensures the three option attributes exist (``decay_extrap_form``, + ``decay_extrap_Q``, ``decay_extrap_form_lower``; subclasses built + from parameter dictionaries that predate the options get ``None``). + * When an option is enabled, swaps ``solve_one_period`` to + ``solve_one_period_ConsIndShock_with_tails`` and registers the + option names in ``time_inv`` so ``AgentType.solve`` delivers them + -- the ``vFuncBool``/``CubicBool`` delivery pattern. The public + ``solve_one_period_ConsIndShock`` keeps its exact legacy signature, + so agents with the options unset (including internal re-solve paths + that skip ``pre_solve``, e.g. ``solve(presolve=False)`` in the HANK + Jacobian machinery) are untouched. Note the corollary: a solve + path that skips ``pre_solve`` never performs the swap, so the + options take effect only through a normal ``solve()``. + * With ``decay_extrap_form='powerlaw'`` and ``decay_extrap_Q=None``, + computes the theory exponent ONCE, pre-solve, at the agent level: + ``q_eff = min(1, q_star)`` from + ``pf_decay.powerlaw_decay_params_from_agent`` (t=0 primitives), + validity-gated on the GIC/RIC/FHWC condition flags -- an invalid + calibration raises with pf_decay's diagnosis rather than solving + with a wrong tail. pf_decay's warning practice (e.g. + ``NearResonanceWarning`` near the q* = 1 knife-edge) surfaces here, + once per solve, not once per backward step. The auto-computed value + is remembered and refreshed on every solve WHILE ``decay_extrap_Q`` + still equals the last auto value (so parameter changes are picked + up); a user-supplied ``decay_extrap_Q`` -- including one assigned + AFTER an auto-computed solve -- differs from the remembered auto + value, is validated, and is respected, never recomputed over. + """ + for name in ("decay_extrap_form", "decay_extrap_Q", "decay_extrap_form_lower"): + if not hasattr(self, name): + setattr(self, name, None) + if self.decay_extrap_form not in (None, "powerlaw"): + raise ValueError( + "decay_extrap_form must be None or 'powerlaw', got " + + repr(self.decay_extrap_form) + ) + if self.decay_extrap_form_lower not in (None, "kappabar"): + raise ValueError( + "decay_extrap_form_lower must be None or 'kappabar', got " + + repr(self.decay_extrap_form_lower) + ) + # decay_extrap_Q is treated as auto-computed only while it is None or + # still equals the REMEMBERED last auto value: a user who assigns an + # explicit Q after an auto solve must not have it recomputed over. + auto_val = getattr(self, "_decay_extrap_Q_auto_value", None) + is_auto = self.decay_extrap_Q is None or ( + auto_val is not None and self.decay_extrap_Q == auto_val + ) + if self.decay_extrap_form == "powerlaw": + if is_auto: + params = powerlaw_decay_params_from_agent(self) + if not params.valid or not np.isfinite(params.q_star): + raise ValueError( + "decay_extrap_form='powerlaw': the theory exponent " + "min(1, q_star) is unavailable for this calibration " + "(GIC=%s RIC=%s FHWC=%s; %s). Fix the calibration or " + "pass an explicit decay_extrap_Q." + % ( + params.GIC, + params.RIC, + params.FHWC, + params.diagnosis or "; ".join(params.warnings), + ) + ) + self.decay_extrap_Q = float(params.q) + self._decay_extrap_Q_auto_value = self.decay_extrap_Q + else: + Q = float(self.decay_extrap_Q) + if not np.isfinite(Q) or Q <= 0.0: + raise ValueError( + "decay_extrap_Q must be a positive finite float, got " + + repr(self.decay_extrap_Q) + ) + # explicitly user-chosen from here on + self._decay_extrap_Q_auto_value = None + else: + if is_auto and self.decay_extrap_Q is not None: + # stale auto-computed exponent from an earlier powerlaw solve + self.decay_extrap_Q = None + self._decay_extrap_Q_auto_value = None + elif self.decay_extrap_Q is not None: + raise ValueError("decay_extrap_Q requires decay_extrap_form='powerlaw'") + if ( + self.decay_extrap_form is not None + or self.decay_extrap_form_lower is not None + ): + if self.solve_one_period is solve_one_period_ConsIndShock: + self.solve_one_period = solve_one_period_ConsIndShock_with_tails + elif self.solve_one_period is not solve_one_period_ConsIndShock_with_tails: + raise ValueError( + "decay_extrap_form / decay_extrap_form_lower require the " + "standard IndShock solver (solve_one_period_ConsIndShock); " + "this agent's solve_one_period is " + repr(self.solve_one_period) + ) + self.add_to_time_inv( + "decay_extrap_form", "decay_extrap_Q", "decay_extrap_form_lower" + ) + elif ( + getattr(self, "solve_one_period", None) + is solve_one_period_ConsIndShock_with_tails + ): + # options disabled again: restore the stock solver, so re-solve + # paths that skip pre_solve stay on the legacy signature + self.solve_one_period = solve_one_period_ConsIndShock def describe_parameters(self): """ diff --git a/HARK/ConsumptionSaving/pf_decay.py b/HARK/ConsumptionSaving/pf_decay.py new file mode 100644 index 000000000..aeafcf730 --- /dev/null +++ b/HARK/ConsumptionSaving/pf_decay.py @@ -0,0 +1,2457 @@ +"""Theory utilities for the power-law decay of the buffer-stock consumption gap. + +Computes, from model primitives, the quantities of the power-law-decay theorem +program that governs how a buffer-stock consumption function approaches its +perfect-foresight asymptote: + + g(x) := kappa*(m + h) - c(m) ~ x^(-min(1, q_star)) as x = m + h -> oo, + +where ``q_star`` is the unique positive root of the eigen-equation + + (E) E[psi^(1+q)] = Rcal * Thorn_Gamma^q, + +with sharp closed-form amplitude ``B_psi`` at ``q_star > 1`` and a ``ln(x)`` +resonance law exactly at ``q_star = 1``. + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §0 "What is q*? (and why min(1, q*))" :: eq (E) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] +# q* = (discounting speed ln Rcal) / (descent speed Lambda) at psi == 1, and in +# general the root of (E). The realized decay exponent is min(1, q*): the gap is +# the sum of a here-and-now 1/x precautionary component and a near-target x^(-q*) +# component, and the slower-fading one wins asymptotically. + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §0 "What is q*? (and why min(1, q*))" :: Prop A0 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] +# The gap can never decay FASTER than 1/x (a prudent consumer always collects at +# least today's Arrow-Pratt premium) — Prop A0's floor, which makes exponential +# decay impossible as an asymptotic form; and since the realized exponent is +# min(1, q*), any steeper fitted exponent is theory-infeasible. + +Conventions (all verified against the theorem program's reference implementations) +----------------------------------------------------------------------------------- +* ``Rcal = R / Gamma`` (FHWC factor; theorem's script-R) +* ``Thorn = (beta_eff * R)^(1/rho)`` (absolute patience factor, APF) +* ``Thorn_R = Thorn / R`` (RPF), ``Thorn_Gamma = Thorn / Gamma`` (GPF) +* ``kappa = 1 - Thorn_R`` (limiting MPC; == ``pf_mpc_min``) +* ``Lambda = ln(1/Thorn_Gamma)`` (log-ladder step; > 0 under GIC) +* ``h = 1/(Rcal - 1)`` (normalized human wealth EXCLUDING + current income, per unit of expected income). + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/ADVERSARIAL_TESTING_GUIDE.md :: 5. LANDMINES — documented evaluation traps and silent-pass hazards :: The `h` human-wealth convention] +# The h-convention trap: BST's h_BST = R/(R-Gamma) = 1 + h INCLUDES current +# income; plugging h_BST into kappa*(m+h) sends the measured gap to -kappa*E_inc, +# a spurious refutation. HARK itself carries BOTH conventions: the solver-side +# ``solution.hNrm`` (recursion ``calc_human_wealth``, terminal 0) EXCLUDES current +# income and equals ``h * E_inc`` here, while the diagnostics dict +# ``bilt['hNrm']`` from ``calc_limiting_values`` = R/(R-G) INCLUDES it. This +# module computes h from primitives and NEVER consumes either solved attribute +# (``solution.hNrm`` is additionally ~11% truncated at HARK's default solve +# tolerance, because the solution distance criterion is vPfunc-based while the +# hNrm recursion contracts only at rate G/R per iteration). + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/statement.md :: 4. Remarks :: Mortality :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] +# Mortality-as-impatience (Remark 9, perpetual-youth): with survival probability +# L, replace beta by beta*L throughout the patience factors (Thorn, Thorn_R, +# Thorn_Gamma, kappa, Lambda, q*); h and Rcal stay mortality-free. This is +# exactly HARK's convention: DiscFacEff = DiscFac*LivPrb enters MPCmin while +# ``calc_human_wealth`` carries no LivPrb. + +Degenerate / pathological inputs (behavior contract) +----------------------------------------------------- +* ``psi == 1`` (PermShkDstn=None or a single unit atom): everything reduces to the + transitory-only (Stage A) theory; q* equals the closed form ``ln(Rcal)/Lambda`` + and the dual root does not exist (compact-support regime). +* **FHWC violated** (``Rcal <= 1``): REFUSES CLEANLY — no exception; ``valid=False`` + with ``h = nan``, ``sigma_B2 = B_psi = c_J = None``, ``q_star = nan`` and a + ``diagnosis`` explaining that L(0) = -ln(Rcal) >= 0 kills the (E)-root. +* **GIC violated** (``Thorn_Gamma >= 1``): ``valid=False`` and a warning, but the + (E)-root is still REPORTED when it exists (a wide psi can make L(q) cross zero + even outside the theorem's hypotheses); with psi == 1 no root exists. +* **RIC violated** (``Thorn_R >= 1``): ``valid=False``, warning; kappa <= 0 is + reported as computed (the PF asymptote itself degenerates). +* ``B_psi`` is ``None`` unless ``q* > 1`` AND ``Rcal*Thorn_Gamma - E[psi^2] > 0`` + (equivalent conditions via ``lambda_B < 1``; both are guarded). +* ``zeta_star`` is ``None`` with a ``dual_diagnosis`` string when the dual root + does not exist (positive log-drift, or no expanding branch). +* **Near-resonance warning** (``NearResonanceWarning``, filterable): fires when + ``|lambda_B - 1| < near_resonance_band`` (default 0.01) — the calibration sits + near the r = g (q* = 1) knife-edge where the pre-asymptotic window is longest. + +Inputs (HARK-style accepted, no HARK import required) +------------------------------------------------------ +Shock distributions may be given as ``(atoms, probs)`` tuples/lists of arrays, as +HARK ``DiscreteDistribution`` objects (duck-typed on ``.atoms`` / ``.pmv``; a +univariate ``atoms`` of shape (1, N) is flattened), or as one-element time-varying +lists ``[dstn]`` of either. Alternatively pass the HARK-style JOINT ``IncShkDstn`` +(atoms row 0 = permanent, row 1 = transitory); then ``sigma_B2`` is computed as +``Var(psi*(theta+h))`` directly on the joint — the master identity's Var(W) — +which equals the closed form under independence and remains the correct Var(W) +even when the joint correlates psi and theta (the theorem's hypotheses, however, +assume psi independent of theta; a ``ShockCorrelationWarning`` flags correlated +joints). + +Numerics: scalar derived objects are computed in ``numpy.longdouble`` (matching +the theorem program's reference implementation ``RB4_egm_lib_B.derived``); +root-finding uses float64 bracket-expansion + bisection to floating-point +resolution. The numpy-only root-finder is kept (rather than scipy's brentq) +because it is the exact code falsified 60/60 against the reference brentq roots +(agreement <= 5e-14 absolute on the estimated HAFiscal calibrations) and it keeps +this module dependency-free beyond numpy. + +Only runtime dependency: numpy. +""" + +import warnings as _warnings +from dataclasses import dataclass, asdict +from typing import Optional, Tuple + +import numpy as np + +__all__ = [ + "PowerLawDecayParams", + "powerlaw_decay_params", + "powerlaw_decay_params_from_agent", + "resonance_constants", + "ValidityThreshold", + "powerlaw_validity_threshold", + "qstar_root", + "dual_root", + "TailDiagnostic", + "powerlaw_tail_diagnostic", + "rel_gap_at", + "aXtraMax_from_tail_tol", + "aXtraMin_from_tail_tol", + "ce_psi_regime", + "qstar_probe", + "StablePoints", + "mNrm_stable_points", + "WealthMassInfo", + "aXtraMax_from_wealth_mass", + "PFDecayConditionWarning", + "PFDecayGridWarning", + "NearResonanceWarning", + "NoDualRootWarning", + "ShockCorrelationWarning", + "ConstraintEndRegimeWarning", + "ErgodicCoverageWarning", + "ErgodicGridDiagnostics", + "ergodic_grid_diagnostics", + "ergodic_grid_diagnostics_from_agent", + "ErgodicGridReport", + "ergodic_grid_report", +] + +_LD = np.longdouble + +# Bracket-expansion cap for both root searches. Generous (not 1024): near the +# r = g knife-edge Lambda = -ln(Thorn_Gamma) -> 0 drives the primal root +# q* = ln(Rcal)/Lambda (and, symmetrically, the dual root zeta*) arbitrarily +# large, so a tight cap would return a spurious "no root" for a perfectly valid +# calibration whose realized exponent min(1, q*) is simply 1. Both L(q) and f(z) +# are evaluated by logsumexp, so a large cap cannot overflow; the expansion loop +# still tightens the upper bracket to the smallest power of two past the root +# before bisection, so the cost of the large cap is only ~30 extra doublings in +# the (unreached) pathological limit. +_BRACKET_CAP = 1.0e12 # bracket-expansion cap for both root searches + + +# ----------------------------------------------------------------- warning taxonomy +class PFDecayConditionWarning(UserWarning): + """A perfect-foresight/buffer-stock condition (FHWC, RIC, GIC) fails, so part + of the power-law-decay theory is undefined or out of scope.""" + + +class PFDecayGridWarning(UserWarning): + """A fitted decay exponent or amplitude attachment looks grid-pathological + (e.g. a fitted exponent above the theoretical ceiling min(1, q*)).""" + + +class NearResonanceWarning(UserWarning): + """The calibration sits near the q* = 1 (r = g) knife-edge, where asymptotic + constants onset only at astronomically large wealth. Filterable so production + loggers can silence it without touching other UserWarnings.""" + + +class NoDualRootWarning(UserWarning): + """The dual (Kesten) root zeta* does not exist for this calibration.""" + + +class ShockCorrelationWarning(UserWarning): + """A joint IncShkDstn correlates psi and theta; the theorem assumes + independence (sigma_B2 is still the exact Var(W) on the joint).""" + + +class ConstraintEndRegimeWarning(UserWarning): + """The constraint end sits in regime II (or is undetermined): the + worst-branch contraction criterion of Theorem CE-psi fails + (lambda(psi_min) = p_eff**(1/CRRA)*Thorn_Gamma/psi_min >= 1), so the + constraint-end approach exponent is q_down = min(CRRA, s*_+) rather than + the CRRA itself (st-rem-CE-regime) and the kappa_bar bottom tail is NOT + theorem-backed there. Filterable, like the other pf_decay warnings.""" + + +# --------------------------------------------------------------------- input handling +def _as_atoms_probs(dstn, name): + """Coerce a shock-distribution input to ``(atoms, probs)`` longdouble arrays. + + Accepts (atoms, probs) tuples/lists, HARK DiscreteDistribution-likes + (``.atoms``/``.pmv``), or one-element time-varying lists of either. + Univariate HARK atoms of shape (1, N) are flattened; probabilities are + validated (nonnegative, sum to 1 within 1e-8, then renormalized exactly) + and the mean is required to be 1 (1e-6), the theorem's normalization + E[psi] = E[theta] = 1. + """ + # unwrap one-element time-varying list [dstn] + if isinstance(dstn, (list, tuple)) and len(dstn) == 1 and ( + hasattr(dstn[0], "pmv") or isinstance(dstn[0], (list, tuple))): + dstn = dstn[0] + if hasattr(dstn, "pmv") and hasattr(dstn, "atoms"): # HARK duck-type + atoms = np.asarray(dstn.atoms) + probs = np.asarray(dstn.pmv) + elif isinstance(dstn, (list, tuple)) and len(dstn) == 2: # (atoms, probs) + atoms = np.asarray(dstn[0]) + probs = np.asarray(dstn[1]) + else: + raise TypeError( + f"{name}: expected (atoms, probs) or a HARK DiscreteDistribution-like " + f"object with .atoms/.pmv, got {type(dstn).__name__}") + if atoms.ndim == 2 and atoms.shape[0] == 1: + atoms = atoms[0] + if atoms.ndim != 1: + raise ValueError(f"{name}: expected univariate atoms, got shape {atoms.shape}") + atoms = atoms.astype(_LD) + probs = probs.astype(_LD) + if atoms.shape != probs.shape: + raise ValueError(f"{name}: atoms/probs shape mismatch " + f"{atoms.shape} vs {probs.shape}") + probs = _validated_probs(probs, name) + mean = float((probs * atoms).sum()) + if abs(mean - 1.0) > 1e-6: + raise ValueError(f"{name}: mean {mean} != 1 (theorem normalization E[.] = 1)") + return atoms, probs + + +def _validated_probs(probs, name): + """Require nonnegative probabilities summing to 1 (tol 1e-8), then + renormalize exactly.""" + if np.any(probs < 0): + raise ValueError(f"{name}: probabilities must be nonnegative") + total = float(probs.sum()) + if abs(total - 1.0) > 1e-8: + raise ValueError(f"{name}: probabilities sum to {total:.10g}, not 1") + return probs / probs.sum() + + +def _as_joint(dstn): + """Coerce a HARK-style joint IncShkDstn to (psi_atoms, theta_atoms, probs). + + Layout per ``ConsIndShockModel.calc_limiting_values``: ``atoms[0]`` = permanent + shock values, ``atoms[1]`` = transitory shock values, ``pmv`` = probabilities. + Also accepts an ``(atoms_2xN, probs)`` tuple with atoms of shape (2, N). + """ + if isinstance(dstn, (list, tuple)) and len(dstn) == 1 and hasattr(dstn[0], "pmv"): + dstn = dstn[0] + if hasattr(dstn, "pmv") and hasattr(dstn, "atoms"): + atoms = np.asarray(dstn.atoms) + probs = np.asarray(dstn.pmv) + elif isinstance(dstn, (list, tuple)) and len(dstn) == 2: + atoms = np.asarray(dstn[0]) + probs = np.asarray(dstn[1]) + else: + raise TypeError("IncShkDstn: expected a joint HARK DiscreteDistribution-like " + "or (atoms_2xN, probs)") + if atoms.ndim != 2 or atoms.shape[0] != 2: + raise ValueError(f"IncShkDstn: joint IncShkDstn atoms must have shape (2, N) " + f"(row 0 = perm, row 1 = tran), got {atoms.shape}") + psi = atoms[0].astype(_LD) + th = atoms[1].astype(_LD) + p = _validated_probs(probs.astype(_LD), "IncShkDstn") + for nm, a in (("perm", psi), ("tran", th)): + mean = float((p * a).sum()) + if abs(mean - 1.0) > 1e-6: + raise ValueError(f"IncShkDstn: E[{nm}] = {mean} != 1") + return psi, th, p + + +def _marginal(atoms, probs): + """Collapse repeated atom values into a proper marginal distribution.""" + vals, inv = np.unique(np.asarray(atoms, float), return_inverse=True) + pm = np.zeros(len(vals), dtype=_LD) + np.add.at(pm, inv, probs) + return vals.astype(_LD), pm + + +def _moments(atoms, probs): + """(mean, variance) of a discrete distribution, longdouble.""" + m1 = (probs * atoms).sum() + return m1, (probs * (atoms - m1) ** 2).sum() + + +# --------------------------------------------------------------------- root finding +def _bisect(f, lo, hi, flo, max_iter=200): + """Bisection to floating-point resolution; requires sign(f(lo)) != sign(f(hi)).""" + for _ in range(max_iter): + mid = 0.5 * (lo + hi) + if mid == lo or mid == hi: + break + fm = f(mid) + if fm == 0.0: + return mid + if (flo < 0.0) == (fm < 0.0): + lo, flo = mid, fm + else: + hi = mid + return 0.5 * (lo + hi) + + +def qstar_root(psi_atoms, psi_probs, Rcal, Thorn_Gamma): + """Unique positive root of (E): E[psi^(1+q)] = Rcal * Thorn_Gamma^q. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §2. Model, conditions, and the imported foundations :: Lemma A5 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Lemma A5: L(q) := ln E[psi^(1+q)] - ln Rcal - q*ln Thorn_Gamma is convex + # with L(0) = -ln Rcal < 0 under FHWC, so (E) has a unique positive root; + # bracket-expansion upward from q=1 then bisection finds it. + + Robust bracket-expansion + bisection, failing closed instead of raising. + + Returns ``(q_star, reason)``: ``(float, None)`` on success, ``(nan, str)`` when + no root is available — either ``L(0) = -ln(Rcal) >= 0`` (FHWC violated) or no + sign change up to the bracket cap (e.g. GIC violated with psi == 1, where L is + non-increasing). + """ + psf = np.asarray(psi_atoms, float) + ppf = np.asarray(psi_probs, float) + RG, PG = float(Rcal), float(Thorn_Gamma) + + ln_RG, ln_PG = np.log(RG), np.log(PG) + ln_psf = np.log(psf) + with np.errstate(divide="ignore"): # a 0-prob atom -> -inf, drops out + ln_ppf = np.log(ppf) + + def L(q): + # L(q) = ln E[psi^(1+q)] - ln Rcal - q*ln Thorn_Gamma. The expectation is + # formed by logsumexp so that psi atoms > 1 cannot overflow at the large q + # reached near the r = g knife-edge (Lambda -> 0 => q* -> oo); identical to + # log(dot(ppf, psf**(1+q))) to machine precision on well-scaled inputs. + t = (1.0 + q) * ln_psf + ln_ppf + tm = t.max() + return float(tm + np.log(np.exp(t - tm).sum()) - ln_RG - q * ln_PG) + + L0 = L(0.0) + if L0 >= 0.0: + return float("nan"), ( + f"no (E)-root: L(0) = -ln(Rcal) = {L0:.6g} >= 0 (FHWC violated: " + f"Rcal = {RG:.6g} <= 1)") + hi = 1.0 + while L(hi) < 0.0 and hi < _BRACKET_CAP: + hi *= 2.0 + if L(hi) < 0.0: + # No crossing within the (generous) cap. L(q) is EVENTUALLY INCREASING iff + # its large-q slope ln(psi_max) - ln(Thorn_Gamma) > 0 (the largest psi atom + # dominates E[psi^(1+q)]); under GIC with E[psi] = 1 that always holds and a + # finite root exists. So a miss here means one of two things, and we must + # NOT conflate them (the old message hard-asserted GIC violation for both): + psi_max = float(psf.max()) + if float(ln_psf.max()) - ln_PG > 0.0: + # L increasing: the root is finite but beyond the cap — an astronomically + # near-resonance calibration (Lambda ~ 0). The realized exponent is 1. + return float("nan"), ( + f"(E)-root exceeds bracket cap {_BRACKET_CAP:g}: L(q) is increasing " + f"(psi_max = {psi_max:.6g} > Thorn_Gamma = {PG:.6g}) so q* is finite " + f"but astronomically large (Lambda = {-ln_PG:.3g} ~ 0, the r = g " + f"knife-edge); the realized decay exponent min(1, q*) = 1") + return float("nan"), ( + f"no (E)-root: L(q) is non-increasing (psi_max = {psi_max:.6g} <= " + f"Thorn_Gamma = {PG:.6g}) — Thorn_Gamma >= 1 (GIC violated) or psi " + f"degenerate") + return _bisect(L, 0.0, hi, L0), None + + +def dual_root(psi_atoms, psi_probs, Thorn_Gamma, LivPrb=1.0): + """Dual (Kesten) root: zeta solving LivPrb * E[(Thorn_Gamma/psi)^zeta] = 1. + + At ``LivPrb = 1`` this is the classical Kesten root zeta* (byte-identical + behavior to the original signature). With ``LivPrb < 1`` it is the + MORTALITY-AUGMENTED dual root zeta_L: perpetual-youth death-with-replacement + acts as a kill rate on the Kesten recursion, and the stationary wealth tail + of the population is Pareto with exponent zeta_L. Two consequences: + (i) existence no longer needs average contraction — for LivPrb < 1 the root + exists whenever an EXPANDING branch exists (P(Thorn_Gamma/psi > 1) > 0), + INCLUDING at the GIC patience cap where zeta* does not (there, mortality is + the only force truncating the tail); (ii) zeta_L > zeta* when both exist + (mortality thins the tail). zeta_L is the patience dial of the wealth + distribution: more patient => Thorn_Gamma nearer 1 => smaller zeta_L => + fatter tail; and zeta_L > 1 is exactly "aggregate wealth is finite". + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §6.1 How rarely is the tail visited? The dual (Kesten) root, for economists :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Normalized wealth follows the Kesten recursion x' = A'x + B' with + # A' = Thorn_Gamma/psi'; the stationary wealth tail is Pareto with exponent + # zeta* solving E[A^zeta] = 1, which exists iff the multiplier contracts on + # average (E[ln A] < 0) and occasionally expands (P(A > 1) > 0). zeta* and + # q* are the two members of one Mellin family: the DUAL root governs the + # wealth-distribution tail, the PRIMAL root q* the consumption function. + # With survival probability L the moment condition gains the kill rate: + # L*E[A^zeta] = 1 (random growth with death, the Toda-reset mechanism). + + Returns ``(zeta_star, E_ln_A, P_A_gt_1, diagnosis)`` with ``zeta_star = None`` + and a non-None diagnosis string when the root does not exist. Root search is + in log space (numerically safe for large zeta) with bracket-expansion + + bisection. A discretized psi is lattice-arithmetic, so zeta* tail statements + hold in the log-periodic (Kevei 2017) sense. + """ + psf = np.asarray(psi_atoms, float) + ppf = np.asarray(psi_probs, float) + PG = float(Thorn_Gamma) + L = float(LivPrb) + ln_L = np.log(L) if 0.0 < L <= 1.0 else float("nan") + if not np.isfinite(ln_L): + return None, float("nan"), float("nan"), ( + f"no dual root: LivPrb = {LivPrb!r} outside (0, 1]") + ln_A = np.log(PG) - np.log(psf) + E_ln_A = float(np.dot(ppf, ln_A)) + P_A_gt_1 = float(ppf[ln_A > 0.0].sum()) + if E_ln_A >= 0.0 and L >= 1.0: + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §6.1 How rarely is the tail visited? The dual (Kesten) root, for economists :: The cap-atom exception :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Positive log-drift is the GIC-cap-atom case: at the patience ceiling the + # mean log-step is positive, normalized wealth drifts UP on average, and + # only mortality-with-replacement truncates that atom's wealth tail — + # which is exactly what LivPrb < 1 supplies (then this branch is skipped + # and the mortality-augmented root exists via the expanding branch). + return None, E_ln_A, P_A_gt_1, ( + f"no dual root: positive log-drift E[ln(Thorn_Gamma/psi)] = " + f"{E_ln_A:+.6g} >= 0 — normalized wealth drifts UP on average; no " + f"stationary distribution from impatience alone (mortality/reset must " + f"truncate the tail; pass LivPrb < 1 for the mortality-augmented root)") + if P_A_gt_1 <= 0.0: + return None, E_ln_A, P_A_gt_1, ( + "no dual root: P(Thorn_Gamma/psi > 1) = 0 — no expanding branch; the " + "ergodic normalized-wealth support is compact (e.g. psi == 1 under " + "GIC), no Pareto tail of any kind" + + ("" if L >= 1.0 else " (mortality cannot create a tail without an " + "expanding branch)")) + ln_p = np.log(ppf) + + def f(z): # ln(LivPrb * E[A^z]) via logsumexp (safe for large z) + t = z * ln_A + ln_p + tm = t.max() + return float(tm + np.log(np.exp(t - tm).sum())) + ln_L + + hi = 1.0 + while f(hi) < 0.0 and hi < _BRACKET_CAP: + hi *= 2.0 + if f(hi) < 0.0: + # f(z) ~ z*ln(A_max) -> +inf whenever P(A > 1) > 0, so a finite root ALWAYS + # exists here and the only way to reach this branch is zeta* beyond the + # (generous) bracket cap — an astronomically near-resonance calibration where + # the single expanding atom has A only marginally above 1. Kept as a numerical + # backstop (with cap = 1e12 it is unreachable for any realistic calibration; + # a tight cap of 1024 used to make it fire on legitimate near-resonance psi). + return None, E_ln_A, P_A_gt_1, ( + f"no dual root found in (0, {_BRACKET_CAP:g}] despite a usable lower " + f"bracket (E[ln A] < 0 or LivPrb < 1) and P(A > 1) > 0: the root " + f"exceeds the bracket cap (astronomically near-resonance; the Pareto " + f"tail is real but its exponent is enormous)") + # Lower bracket: at LivPrb = 1, f(0) = 0 with f'(0) = E[ln A] < 0, so f < 0 + # just inside 0; at LivPrb < 1, f(0) = ln(LivPrb) < 0 outright. Either way, + # halve down from hi/2 until the endpoint is strictly negative (roots < 1). + lo = hi / 2.0 + flo = f(lo) + while flo >= 0.0 and lo > 1e-12: + lo /= 2.0 + flo = f(lo) + if flo >= 0.0: # mathematically unreachable given the branch guards; safety + return None, E_ln_A, P_A_gt_1, ( + "no dual root: lower bracket could not be established " + "(numerical degeneracy)") + return _bisect(f, lo, hi, flo), E_ln_A, P_A_gt_1, None + + +# --------------------------------------------------------------------- result object +@dataclass(frozen=True) +class PowerLawDecayParams: + """All power-law-decay theorem quantities for one calibration. + + ``None`` = not defined for this calibration; ``nan`` = defined but not + computable from these primitives (see ``warnings``/``diagnosis``). + """ + + # primitives (echoed) + Rfree: float + PermGroFac: float + DiscFac: float + CRRA: float + LivPrb: float + beta_eff: float # DiscFac * LivPrb (mortality-as-impatience) + # derived patience/return objects + Rcal: float # R/Gamma (mortality-free) + Thorn: float # (R*beta_eff)**(1/CRRA) (APF) + Thorn_R: float # APF/R (RIC factor) + Thorn_Gamma: float # APF/Gamma (GIC factor) + kappa: float # 1 - Thorn_R == PF MPCmin + Lambda: float # ln(1/Thorn_Gamma) (> 0 iff GIC) + h: float # 1/(Rcal-1), per unit E[inc]; EXCLUDES + # current income == solution.hNrm/E_inc + # shock moments + E_inc: float # E[psi*theta] + E_psi2: float + Var_psi: float + Var_theta: float + # the theorem + q_star: float # unique positive root of (E); nan if none + q: float # min(1, q_star) — realized decay exponent + diagnosis: str # '' or why q_star is nan + sigma_B2: Optional[float] # E[psi^2]*Var(theta) + (1+h)^2*Var(psi) + lambda_B: float # E[psi^2]/(Rcal*Thorn_Gamma) + near_resonance: bool # |lambda_B - 1| < near_resonance_band + resonance_slope: Optional[float] # kappa*(rho+1)*sigma_B2/(2*Lambda): the + # q*=1 ln-x law constant at psi == 1, for + # reference (general-psi exact constant: + # resonance_constants()['C_B']) + B_psi: Optional[float] # closed-form amplitude; None unless q*>1 + c_J: Optional[float] # kappa*(rho+1)*sigma_B2/(2*Thorn_Gamma) + # dual (Kesten) root + zeta_star: Optional[float] + dual_diagnosis: str # 'ok' or why zeta_star is None + E_ln_A: float + P_A_gt_1: float + # condition flags + GIC: bool + RIC: bool + FHWC: bool + valid: bool + warnings: Tuple[str, ...] = () + + def to_dict(self): + return asdict(self) + + +# --------------------------------------------------------------------- main entry +def powerlaw_decay_params(Rfree, PermGroFac, DiscFac, CRRA, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, IncShkDstn=None, + near_resonance_band=0.01, warn=True): + """Compute every power-law-decay theorem quantity from model primitives. + + Parameters + ---------- + Rfree, PermGroFac, DiscFac, CRRA : float + Gross return R, permanent-income growth factor Gamma, discount factor + beta, relative risk aversion rho. Scalars (a one-element list/array is + accepted for each, matching HARK's time-varying parameter style). + LivPrb : float, default 1.0 + Survival probability; enters ONLY via ``beta_eff = DiscFac*LivPrb`` + (mortality-as-impatience; see the module docstring). + PermShkDstn, TranShkDstn : optional + Marginal shock distributions — ``(atoms, probs)`` or HARK + DiscreteDistribution-like (or ``[dstn]``). ``PermShkDstn=None`` means + psi == 1 (the transitory-only theory); a degenerate theta is accepted + but yields sigma_B2 driven by psi only. A theta = 0 unemployment atom + IS allowed (native to the theorem); psi atoms must be strictly positive. + IncShkDstn : optional + HARK-style JOINT distribution (atoms row 0 = perm, row 1 = tran). + Mutually exclusive with PermShkDstn/TranShkDstn. sigma_B2 is then + ``Var(psi*(theta+h))`` on the joint (the master identity's Var(W)), + which equals the closed form under independence and is the correct + Var(W) even under correlation (a ``ShockCorrelationWarning`` then + notes that the theorem's hypotheses assume independence). + near_resonance_band : float, default 0.01 + Threshold on ``|lambda_B - 1|`` for the near-resonance warning/flag. + warn : bool, default True + Emit the categorized Python warnings (``PFDecayConditionWarning``, + ``NearResonanceWarning``, ``NoDualRootWarning``, + ``ShockCorrelationWarning``). The same messages are always recorded in + the returned object's ``warnings`` tuple. Warnings fire once per call + to this utility, never per consumption-function slice. + + Returns + ------- + PowerLawDecayParams + Frozen dataclass with all quantities, condition flags, and warnings. + Never raises on GIC/RIC/FHWC violations (see the module docstring's + behavior contract); raises ValueError/TypeError only on malformed + inputs. + """ + def _scalar(v, name): + if isinstance(v, (list, tuple, np.ndarray)): + v = np.asarray(v).ravel() + if v.size != 1: + raise ValueError(f"{name}: expected a scalar (or length-1 " + f"list/array), got size {v.size}") + v = v[0] + return _LD(float(v)) + + R = _scalar(Rfree, "Rfree") + G = _scalar(PermGroFac, "PermGroFac") + beta = _scalar(DiscFac, "DiscFac") + rho = _scalar(CRRA, "CRRA") + liv = _scalar(LivPrb, "LivPrb") + if R <= 0 or G <= 0 or beta <= 0 or rho <= 0 or not (0 < liv <= 1): + raise ValueError("Rfree, PermGroFac, DiscFac, CRRA must be positive and " + "LivPrb in (0, 1]") + + warn_list = [] + warn_cats = [] + + def _record(msg, category): + warn_list.append(msg) + warn_cats.append(category) + + # ---- shock distributions + joint = None + if IncShkDstn is not None: + if PermShkDstn is not None or TranShkDstn is not None: + raise ValueError("pass IncShkDstn OR (PermShkDstn, TranShkDstn), " + "not both") + psi_j, th_j, p_j = _as_joint(IncShkDstn) + joint = (psi_j, th_j, p_j) + psi_a, psi_p = _marginal(psi_j, p_j) + th_a, th_p = _marginal(th_j, p_j) + E_inc = float((p_j * psi_j * th_j).sum()) + E_prod = float((psi_p * psi_a).sum()) * float((th_p * th_a).sum()) + if abs(E_inc - E_prod) > 1e-10 * max(1.0, abs(E_inc)): + _record( + f"IncShkDstn psi and theta are correlated " + f"(|E[psi*theta]-E[psi]E[theta]| = {abs(E_inc - E_prod):.2e}); " + f"the theorem's hypotheses assume psi independent of theta. " + f"sigma_B2 is the exact Var(W) on the joint; q_star and " + f"zeta_star use the psi marginal.", ShockCorrelationWarning) + else: + if PermShkDstn is None: + psi_a = np.array([_LD(1)]) + psi_p = np.array([_LD(1)]) + else: + psi_a, psi_p = _as_atoms_probs(PermShkDstn, "PermShkDstn") + if TranShkDstn is None: + th_a = np.array([_LD(1)]) + th_p = np.array([_LD(1)]) + else: + th_a, th_p = _as_atoms_probs(TranShkDstn, "TranShkDstn") + E_inc = float((psi_p * psi_a).sum()) * float((th_p * th_a).sum()) + if np.any(psi_a <= 0): + raise ValueError("PermShkDstn: psi atoms must be strictly positive " + "(supp psi subset of (0, inf))") + + # ---- derived patience/return objects (longdouble) + beta_eff = beta * liv + Thorn = (R * beta_eff) ** (1 / rho) + Rcal = R / G + Thorn_R = Thorn / R + Thorn_Gamma = Thorn / G + kappa = 1 - Thorn_R + Lambda = -np.log(Thorn_Gamma) + + FHWC = bool(Rcal > 1) + RIC = bool(Thorn_R < 1) + GIC = bool(Thorn_Gamma < 1) + if not FHWC: + _record(f"FHWC VIOLATED: Rcal = R/Gamma = {float(Rcal):.6g} <= 1 — " + f"human wealth h = 1/(Rcal-1) undefined/infinite; the PF " + f"asymptote and every h-dependent quantity are unavailable", + PFDecayConditionWarning) + if not RIC: + _record(f"RIC VIOLATED: Thorn_R = {float(Thorn_R):.6g} >= 1 — " + f"kappa = 1 - Thorn_R <= 0, the PF asymptote degenerates", + PFDecayConditionWarning) + if not GIC: + _record(f"GIC VIOLATED: Thorn_Gamma = {float(Thorn_Gamma):.6g} >= 1 " + f"(Lambda = {float(Lambda):.6g} <= 0) — the theorem's " + f"buffer-stock hypotheses fail; any reported (E)-root is " + f"outside the theorem's scope", PFDecayConditionWarning) + + # h EXCLUDES current income: h = h_BST - 1 (see the h-convention tag in the + # module docstring; never read solution.hNrm or bilt['hNrm'] here). + h = 1 / (Rcal - 1) if FHWC else _LD(float("nan")) + + # ---- shock moments + E_psi2 = float((psi_p * psi_a ** 2).sum()) + _, Var_psi = _moments(psi_a, psi_p) + _, Var_theta = _moments(th_a, th_p) + Var_psi, Var_theta = float(Var_psi), float(Var_theta) + + # ---- eigen-root q*, realized exponent + q_star, q_reason = qstar_root(psi_a, psi_p, Rcal, Thorn_Gamma) + q = min(1.0, q_star) if np.isfinite(q_star) else float("nan") + + # ---- lambda_B and near-resonance warning (h-free) + lambda_B = float(E_psi2 / (Rcal * Thorn_Gamma)) + near_res = bool(abs(lambda_B - 1.0) < near_resonance_band) + if near_res: + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The knife-edge window, quantified on HAFiscal's own numbers :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Near the r = g knife-edge (lambda_B near 1, Lambda near 0) the + # asymptotic regime begins only at astronomically large wealth: at the + # GIC-cap calibration the compensated gap has covered only 42% of the + # way to B_psi at the HAFiscal grid top, 91% even at 130x the grid top. + # The plateau-onset scale is ln x >~ ln x_c + O(1)/(q*-1). + _record( + f"NEAR-RESONANCE: lambda_B = E[psi^2]/(Rcal*Thorn_Gamma) = " + f"{lambda_B:.6f} is within {near_resonance_band:.0%} of the q* = 1 " + f"(r = g) knife-edge — the pre-asymptotic window is long: on any " + f"feasible grid a FITTED tail constant will understate the " + f"closed-form amplitude B_psi, and B_psi (if defined) is approached " + f"only at astronomically large wealth. Treat B_psi as an asymptotic " + f"boundary value, not a fit target.", NearResonanceWarning) + + # ---- sigma_B2, B_psi, c_J (need h) + sigma_B2 = None + B_psi = None + c_J = None + resonance_slope = None + if FHWC: + if joint is not None: + # Master identity Var(W), W = psi*(theta+h) - (1+h): exact on the + # joint, equals the closed form under independence. + psi_j, th_j, p_j = joint + W = psi_j * (th_j + h) - (1 + h) + EW = (p_j * W).sum() + sigma_B2 = float((p_j * (W - EW) ** 2).sum()) + else: + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §5. Theorem III: permanent shocks — the human-wealth-revaluation channel :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # sigma_B^2 = Var(W) = E[psi^2]*Var(theta) + (1+h)^2*Var(psi): the + # (1+h)^2*Var(psi) term is the human-wealth revaluation channel — a + # permanent shock reprices the whole future income stream. + sigma_B2 = float(_LD(E_psi2) * _LD(Var_theta) + + (1 + h) ** 2 * _LD(Var_psi)) + c_J = float(kappa * (rho + 1) * _LD(sigma_B2) / (2 * Thorn_Gamma)) + if float(Lambda) > 0.0: + resonance_slope = float(kappa * (rho + 1) * _LD(sigma_B2) + / (2 * Lambda)) + denom = float(Rcal * Thorn_Gamma) - E_psi2 + if np.isfinite(q_star) and q_star > 1.0 and denom > 0.0: + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §5. Theorem III: permanent shocks — the human-wealth-revaluation channel :: Theorem γ-B :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Theorem III (= Theorem γ-B): at q* > 1, + # x*g(x) -> B_psi = kappa*(rho+1)*sigma_B^2 / (2*(Rcal*Thorn_Gamma - E[psi^2])), + # the closed-form boundary amplitude (Gordon-convergent perpetuity of + # precautionary premia); the denominator is positive iff lambda_B < 1 + # iff q* > 1, so both gates below are equivalent and both guarded. + B_psi = float(kappa * (rho + 1)) * sigma_B2 / (2.0 * denom) + + # ---- dual (Kesten) root + zeta_star, E_ln_A, P_A_gt_1, zeta_diag = dual_root(psi_a, psi_p, Thorn_Gamma) + if zeta_diag is not None and E_ln_A >= 0.0: + _record(f"NO DUAL ROOT (positive log-drift): {zeta_diag}", + NoDualRootWarning) + + if warn: + for msg, cat in zip(warn_list, warn_cats): + _warnings.warn(msg, cat, stacklevel=2) + + return PowerLawDecayParams( + Rfree=float(R), PermGroFac=float(G), DiscFac=float(beta), CRRA=float(rho), + LivPrb=float(liv), beta_eff=float(beta_eff), + Rcal=float(Rcal), Thorn=float(Thorn), Thorn_R=float(Thorn_R), + Thorn_Gamma=float(Thorn_Gamma), kappa=float(kappa), Lambda=float(Lambda), + h=float(h), + E_inc=E_inc, E_psi2=E_psi2, Var_psi=Var_psi, Var_theta=Var_theta, + q_star=q_star, q=q, diagnosis=(q_reason or ""), + sigma_B2=sigma_B2, lambda_B=lambda_B, + near_resonance=near_res, resonance_slope=resonance_slope, + B_psi=B_psi, c_J=c_J, + zeta_star=zeta_star, dual_diagnosis=(zeta_diag or "ok"), + E_ln_A=E_ln_A, P_A_gt_1=P_A_gt_1, + GIC=GIC, RIC=RIC, FHWC=FHWC, + valid=bool(GIC and RIC and FHWC), + warnings=tuple(warn_list), + ) + + +# --------------------------------------------------------------- agent convenience +def _time_indexed(value, t): + """Scalarize a possibly time-varying HARK parameter the way the solvers do: + index a list/tuple (or an object-array/IndexDistribution-like sequence) at + ``t``; pass scalars through.""" + if isinstance(value, (list, tuple)): + return value[t] + if isinstance(value, np.ndarray) and value.ndim >= 1: + return value[t] + return value + + +def powerlaw_decay_params_from_agent(agent, t=0, near_resonance_band=0.01, + warn=True): + """Convenience wrapper for IndShockConsumerType-family agents. + + Reads ``Rfree[t]``, ``PermGroFac[t]``, ``DiscFac``, ``CRRA``, ``LivPrb[t]``, + ``PermShkDstn[t]`` and ``TranShkDstn[t]`` (falling back to the JOINT + ``IncShkDstn[t]`` when the marginals are absent, e.g. hand-built income + processes), scalarizing time-varying lists the same way the solvers do. + + Compute-from-primitives rule: this NEVER reads ``agent.solution[...].hNrm`` + (truncated at default solve tolerance) nor ``agent.bilt['hNrm']`` (BST + convention, includes current income) — see the h-convention tag in the + module docstring. The solver-side theorem-convention human wealth + in HARK's income units is ``params.h * params.E_inc``. + """ + kwargs = dict( + Rfree=_time_indexed(agent.Rfree, t), + PermGroFac=_time_indexed(agent.PermGroFac, t), + DiscFac=_time_indexed(agent.DiscFac, t), + CRRA=_time_indexed(agent.CRRA, t), + LivPrb=_time_indexed(agent.LivPrb, t), + near_resonance_band=near_resonance_band, + warn=warn, + ) + perm = getattr(agent, "PermShkDstn", None) + tran = getattr(agent, "TranShkDstn", None) + if perm is not None and tran is not None: + return powerlaw_decay_params( + PermShkDstn=perm[t], TranShkDstn=tran[t], **kwargs) + inc = getattr(agent, "IncShkDstn", None) + if inc is None: + raise ValueError("agent has neither (PermShkDstn, TranShkDstn) nor " + "IncShkDstn") + return powerlaw_decay_params(IncShkDstn=inc[t], **kwargs) + + +# --------------------------------------------------------------- resonance helper +def resonance_constants(Rfree, PermGroFac, DiscFac, CRRA, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, IncShkDstn=None, + warn=True): + """Sharp constants of the q* = 1 resonance case. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §4. Theorem II: the trichotomy — three things a linear recursion can do at a boundary :: Theorem γ-R :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # At q* = 1 (Rcal*Thorn_Gamma = 1, the r = g knife-edge) the shell recursion + # escapes linearly instead of contracting to a value: the gap obeys the ln-x + # law (x/ln x)*g(x) -> C_B — a slope, not a value. This is a knife-edge, not + # a neighborhood: the crossover to it is non-uniform on any window with + # |q*-1|*ln x = O(1). + + At exact resonance (``E[psi^2] = Rcal*Thorn_Gamma``, i.e. lambda_B = 1) the + gap obeys ``x*g(x)/ln(x) -> C_B`` with + + C_B = kappa*(rho+1)*sigma_B^2 / (2*E[psi^2]*Lprime(1)), + Lprime(1) = E[psi^2 * ln psi]/E[psi^2] - ln(Thorn_Gamma), + + (the psi^2-tilted mean log-step) and the per-tilted-rung Cesaro increment is + ``c_J/Rcal``; at exact resonance ``c_J/Rcal == C_B * Lprime(1)`` identically. + + Returns a dict: ``C_B``, ``cJ_over_Rcal``, ``Lprime1``, ``lambda_B``, + ``resonance_residual`` (= |lambda_B - 1|; the constants are the theorem's + ONLY at exact resonance — a warning-sized residual means you are in the + q* != 1 regime and should use B_psi or the fitted amplitude instead), plus + the underlying ``theory`` result (a PowerLawDecayParams). + """ + th = powerlaw_decay_params( + Rfree, PermGroFac, DiscFac, CRRA, LivPrb=LivPrb, + PermShkDstn=PermShkDstn, TranShkDstn=TranShkDstn, IncShkDstn=IncShkDstn, + warn=warn) + if th.sigma_B2 is None: + raise ValueError("resonance_constants: sigma_B2 unavailable " + f"(warnings: {list(th.warnings)})") + # rebuild the psi marginal exactly as the main entry did + if IncShkDstn is not None: + psi_j, _, p_j = _as_joint(IncShkDstn) + psi_a, psi_p = _marginal(psi_j, p_j) + elif PermShkDstn is not None: + psi_a, psi_p = _as_atoms_probs(PermShkDstn, "PermShkDstn") + else: + psi_a, psi_p = np.array([_LD(1)]), np.array([_LD(1)]) + psf, ppf = np.asarray(psi_a, float), np.asarray(psi_p, float) + E2 = float(np.dot(ppf, psf ** 2)) + Elog = float(np.dot(ppf, psf ** 2 * np.log(psf))) + Lprime1 = Elog / E2 - float(np.log(th.Thorn_Gamma)) + C_B = th.kappa * (th.CRRA + 1.0) * th.sigma_B2 / (2.0 * E2 * Lprime1) + cJ_over_Rcal = th.c_J / th.Rcal + return dict(C_B=C_B, cJ_over_Rcal=cJ_over_Rcal, Lprime1=Lprime1, + lambda_B=th.lambda_B, resonance_residual=abs(th.lambda_B - 1.0), + theory=th) + + +# ----------------------------------------------------- analytic validity floor +@dataclass(frozen=True) +class ValidityThreshold: + """Result of :func:`powerlaw_validity_threshold`: the proofs' explicit + guaranteed-validity floor ``wbar0`` for the top gap law, in the total-wealth + coordinate ``wbar = m + hNrm`` (same coordinate the gap ``g(wbar)`` decays + in). ``nan`` when the calibration violates the theorem conditions (GIC / + FHWC / RIC); see ``valid`` / ``diagnosis``. All constants are floats.""" + + wbar0: float # x0 (5.0): full floor = max(x0^0, 2*K_hat) + wbar0_hat_free: float # x0^0 (5.0a): the K_hat-free floor (5 primitive terms) + binding_term: str # which entry of (5.0)/(5.0a) attains the max + terms: dict # every candidate term of the max, by name + C0: float # max(1 - theta_min, (theta_max - 1) + Rcal*gbar) + gbar: float # kappa*h (the gap ceiling) + mbar: float # kappa*h/Thorn_R (m above which the gap is strict) + zeta: float # C0/(1 - Thorn_Gamma) + K_hat: float # 2*(K_L + K_R) (Cor. 5.2); nan if omitted + K_L: float + K_R: float + c2: float # (rho(rho+1)/2)*(1 - tbar)**(-(rho+2)) + tbar: float # 1/(8(rho+1)) + theta_min: float + theta_max: float + valid: bool + diagnosis: str + warnings: Tuple[str, ...] = () + + def to_dict(self): + return asdict(self) + + +def powerlaw_validity_threshold(Rfree, PermGroFac, DiscFac, CRRA, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, + IncShkDstn=None, warn=True): + """The proofs' explicit guaranteed-validity threshold ``wbar0`` for the + top power-law gap law, computed from primitives. + + The gap theorems are ASYMPTOTIC: the one-step gap machinery is PROVEN for + ``wbar >= wbar0``, where ``wbar0`` (written ``x0`` in the proof) has a fully + explicit primitive formula. This is a DIAGNOSTIC, never a refusal gate: + + * ``wbar0`` = the guaranteed-validity floor with explicit constants, which + are DELIBERATELY CRUDE (statement.md Remark 7). It is NOT where the + extrapolation first becomes accurate in practice — empirically that + happens much earlier (typically near ``m ~ hNrm``, and the operative + quality rule is :func:`aXtraMax_from_tail_tol`). Use ``wbar0`` to say + "the tail is PROVABLY valid beyond here", not "the grid must reach here". + + # THEOREM-REF[BufferStockTheory-Latest @ a5cef781 :: theory/powerlaw-decay/stage_A_proof.md :: §5. Linearization control: the gap equation with explicit remainders (L4) :: (5.0a)] + # Public: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/ + # The Stage-A (psi == 1) guaranteed-validity floor is + # x0^0 := max{ h + mbar, (8(rho+1)*gbar/kappa + C0)/Thorn_Gamma, + # 8(rho+1)*C0/Thorn_Gamma, (h + 1 + C0)/Thorn_Gamma, 2*zeta } + # (5.0a), and the full floor x0 := max{ x0^0, 2*K_hat } (5.0), with + # gbar := kappa*h, mbar := kappa*h/Thorn_R, C0 := max(1 - theta_min, + # (theta_max - 1) + Rcal*gbar), zeta := C0/(1 - Thorn_Gamma), tbar := + # 1/(8(rho+1)), c2 := (rho(rho+1)/2)(1 - tbar)^(-(rho+2)), K_hat := + # 2(K_L + K_R) (Cor. 5.2), K_L := c2*Thorn_Gamma*gbar/(rho*Rcal*kappa), + # and K_R the x0^0-anchored constant of (5.5)/line 308. The wealth + # coordinate is wbar = m + h with (Rcal - 1)h = 1 (h EXCLUDES current + # income == HARK's solver-side hNrm under E[theta] = 1). + + This is the STAGE-A (transitory-only, ``psi == 1``) certified threshold: the + patience/return objects (``kappa``, ``Thorn_Gamma``, ``Thorn_R``, ``Rcal``, + ``h``) come from the full primitives, and ``theta_min``/``theta_max``/``C0`` + from the TRANSITORY marginal. For a psi-general calibration the number is a + (crude, conservative) heuristic floor rather than a proven one, and should + be read as one of three layered criteria — ``wbar0`` (guaranteed floor), + :func:`aXtraMax_from_tail_tol` (the operative ex-post quality rule), and the + pedagogical "top knot above human wealth" (``m >~ hNrm``). + + Parameters + ---------- + Rfree, PermGroFac, DiscFac, CRRA, LivPrb : float + Model primitives, exactly as :func:`powerlaw_decay_params`. + PermShkDstn, TranShkDstn, IncShkDstn : optional + Shock distributions (marginals or a HARK joint), as + :func:`powerlaw_decay_params`. ``theta_min``/``theta_max`` are read off + the transitory marginal (a ``theta = 0`` unemployment atom is native and + drives ``C0`` through ``1 - theta_min``); ``PermShkDstn`` affects only + the patience objects, not the Stage-A constants. + warn : bool, default True + Emit categorized warnings (the underlying ``powerlaw_decay_params`` + condition warnings). Always recorded in the returned ``warnings``. + + Returns + ------- + ValidityThreshold + Frozen dataclass with ``wbar0`` (full), ``wbar0_hat_free`` (K_hat-free), + the binding term, and every constituent constant. Never raises on + condition violations (returns ``nan`` + ``diagnosis``); raises only on + malformed inputs (via ``powerlaw_decay_params``). + """ + th = powerlaw_decay_params( + Rfree, PermGroFac, DiscFac, CRRA, LivPrb=LivPrb, + PermShkDstn=PermShkDstn, TranShkDstn=TranShkDstn, IncShkDstn=IncShkDstn, + warn=warn) + + # transitory marginal -> theta_min, theta_max (mirrors the main entry) + if IncShkDstn is not None: + _psi_j, th_j, p_j = _as_joint(IncShkDstn) + th_a, _th_p = _marginal(th_j, p_j) + elif TranShkDstn is not None: + th_a, _th_p = _as_atoms_probs(TranShkDstn, "TranShkDstn") + else: + th_a = np.array([_LD(1)]) + theta_min = float(np.min(th_a)) + theta_max = float(np.max(th_a)) + + rho = float(th.CRRA) + kappa = float(th.kappa) + h = float(th.h) + Thorn_Gamma = float(th.Thorn_Gamma) + Thorn_R = float(th.Thorn_R) + Rcal = float(th.Rcal) + + # the threshold is only defined under GIC (Thorn_Gamma < 1), FHWC + # (Rcal > 1 => finite positive h), and RIC (kappa > 0). Fail closed to nan + # with a diagnosis rather than raising (diagnostic contract). + bad = [] + if not (np.isfinite(Thorn_Gamma) and Thorn_Gamma < 1.0): + bad.append("GIC fails (Thorn_Gamma >= 1): the comparison ladder does " + "not contract, so wbar0 is undefined") + if not (np.isfinite(Rcal) and Rcal > 1.0 and np.isfinite(h) and h > 0): + bad.append("FHWC fails (Rcal <= 1): human wealth h is not finite/" + "positive, so the gap coordinate wbar = m + h degenerates") + if not (np.isfinite(kappa) and kappa > 0): + bad.append("RIC fails (kappa <= 0): the PF asymptote MPCmin has no " + "interior, so the gap law has no target") + if bad: + return ValidityThreshold( + wbar0=float("nan"), wbar0_hat_free=float("nan"), + binding_term="", terms={}, C0=float("nan"), + gbar=float("nan"), mbar=float("nan"), zeta=float("nan"), + K_hat=float("nan"), K_L=float("nan"), K_R=float("nan"), + c2=float("nan"), tbar=float("nan"), + theta_min=theta_min, theta_max=theta_max, + valid=False, diagnosis="; ".join(bad), warnings=th.warnings) + + # §5 constants (verbatim from the pinned displays) + gbar = kappa * h # ln.28/123/189: gbar = kappa*h + mbar = kappa * h / Thorn_R # ln.124/152: mbar = kappa*h/Thorn_R + C_plus = (theta_max - 1.0) + Rcal * gbar # ln.215: C+ = (theta_max-1) + Rcal*gbar + C0 = max(1.0 - theta_min, C_plus) # ln.215: C0 = max(1-theta_min, C+) + zeta = C0 / (1.0 - Thorn_Gamma) # ln.255: zeta = C0/(1-Thorn_Gamma) + tbar = 1.0 / (8.0 * (rho + 1.0)) # ln.248: tbar = 1/(8(rho+1)) + c2 = (rho * (rho + 1.0) / 2.0) * (1.0 - tbar) ** (-(rho + 2.0)) # ln.250 + + # x0^0 (5.0a): the five K_hat-free primitive terms + terms = { + "h + mbar": h + mbar, + "(8(rho+1)*gbar/kappa + C0)/Thorn_Gamma": + (8.0 * (rho + 1.0) * gbar / kappa + C0) / Thorn_Gamma, + "8(rho+1)*C0/Thorn_Gamma": 8.0 * (rho + 1.0) * C0 / Thorn_Gamma, + "(h + 1 + C0)/Thorn_Gamma": (h + 1.0 + C0) / Thorn_Gamma, + "2*zeta": 2.0 * zeta, + } + x00 = max(terms.values()) + + # K_R (line 308): tail terms anchored at x0^0 (never x0) — the non-circularity + # repair; K_L (line 307); K_hat := 2(K_L + K_R) (Cor. 5.2, line 372). + f = 8.0 / 7.0 + g_kg = gbar / (kappa * Thorn_Gamma) # gbar/(kappa*Thorn_Gamma) == h/Thorn_Gamma + K_L = c2 * Thorn_Gamma * gbar / (rho * Rcal * kappa) + K_R = f * ( + f * C0 / Thorn_Gamma + + (1.0 / rho) * ( + c2 * f * g_kg + + rho ** 2 * C0 / Thorn_Gamma + + rho * c2 * f * (C0 / Thorn_Gamma) * g_kg / x00 + + rho * c2 * C0 ** 2 / (Thorn_Gamma ** 2 * x00) + + c2 ** 2 * (C0 ** 2 / Thorn_Gamma ** 2) * (f * g_kg) / x00 ** 2 + ) + ) + K_hat = 2.0 * (K_L + K_R) + + # x0 (5.0): full floor = max(x0^0, 2*K_hat) + terms_full = dict(terms) + terms_full["2*K_hat"] = 2.0 * K_hat + wbar0 = max(terms_full.values()) + binding_term = max(terms_full, key=terms_full.get) + + return ValidityThreshold( + wbar0=float(wbar0), wbar0_hat_free=float(x00), + binding_term=binding_term, terms=terms_full, + C0=float(C0), gbar=float(gbar), mbar=float(mbar), zeta=float(zeta), + K_hat=float(K_hat), K_L=float(K_L), K_R=float(K_R), + c2=float(c2), tbar=float(tbar), + theta_min=theta_min, theta_max=theta_max, + valid=bool(th.valid), diagnosis="", warnings=th.warnings) + + +# --------------------------------------------------------------- tail diagnostic +@dataclass +class TailDiagnostic: + """Result of :func:`powerlaw_tail_diagnostic` (all fields per the theorem's + Figure-4 presentation).""" + + verdict: str # 'CONFIRMED' | 'PRE_ASYMPTOTIC' | 'INCONSISTENT' + # | 'UNMEASURABLE' + s_grid: np.ndarray # trial exponents q + trial_offsets + slopes: np.ndarray # compensated slope d ln(x^s * gap)/d ln(x) per trial s + # (gamma-T prediction: ~ s - q_true, flat only at the + # true exponent) + q_theory: float # min(1, q_star) used as the tested exponent + window: tuple # (m_lo, m_hi) of the points that survived the guard + n_points: int # points surviving the gap guard + notes: str + + +def powerlaw_tail_diagnostic(cFunc, MPCmin, hNrm, params, m_lo=None, m_hi=None, + n_pts=40, trial_offsets=(-0.15, 0.0, +0.15), + flat_tol=0.08, inconsistency_tol=0.5, + guard_rel_gap=1e-9): + """Wrong-exponent detection: a cheap post-solve grid test (no re-solve). + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §4. Theorem II: the trichotomy — three things a linear recursion can do at a boundary :: Theorem γ-T :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Theorem γ-T (wrong-exponent detection): compensate the gap by a trial + # exponent s — only s = min(1, q*) makes the compensated series flat + # (bounded with a positive limit); any other s makes it drift with sign + # s - min(1, q*). The compensated-flatness test below is that theorem + # read as a diagnostic on a solved consumption function. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: A built-in diagnostic :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The grid-depth migration of measured exponents toward min(1, q*) is the + # theorem-backed convergence signature for solver validation; a flat point + # far from min(1, q*) is the actionable wrong-exponent signature (broken + # grid / wrong MPCmin-hNrm reference / non-converged solve). + + Sweeps ``cFunc`` once on a log-spaced window, forms the gap + ``g(m) = MPCmin*(m + hNrm) - cFunc(m)`` against ``x = m + hNrm``, drops + points below the float-cancellation floor (``gap/c <= guard_rel_gap`` or + ``gap <= 0``), then fits the windowed log-log slope of the gap over >= 20 + points (NEVER the 2-knot top-slope estimator, which is grid-non-monotone). + With a single-window fit the compensated slope is exactly affine in the + trial exponent, ``slope(s) = s - Q_local`` with ``Q_local`` the fitted + local exponent, so the verdict reduces to the location of the flat point + ``s = Q_local`` relative to ``q = min(1, q_star)`` — pre-registered + semantics (not tuned post hoc), with ``center := slope(s=q) = q - Q_local``: + + * ``UNMEASURABLE``: fewer than ``n_pts//2`` points survive the guard + (deep-grid float cancellation), or ``params.q`` is nan (theory refused). + * ``CONFIRMED``: ``|center| <= flat_tol`` (default 0.08; the phase-1 + measurement on a deep real solve was +0.039). + * ``PRE_ASYMPTOTIC``: ``flat_tol < center <= inconsistency_tol`` — the + local exponent sits BELOW min(1, q*), the theorem-backed transient side + (the measured exponent migrates upward toward min(1, q*) with grid + depth); expected at near-resonance calibrations, where a note is + appended. + * ``INCONSISTENT``: ``center > inconsistency_tol`` (default 0.5: the flat + point is far from min(1, q*) — e.g. the h-convention trap turns the gap + into a constant, Q_local ~ 0) OR ``center < -flat_tol`` (any local + exponent STEEPER than min(1, q*): the impossibility-floor side, which no + transient of the true solution produces over a measurable window — Prop A0 + forbids a gap fading faster than 1/x). Equivalently, any ``center`` outside + the symmetric CONFIRMED band ``[-flat_tol, +flat_tol]`` on the steeper + (negative) side is INCONSISTENT; the PRE_ASYMPTOTIC band is one-sided + (``flat_tol < center <= inconsistency_tol``), because migration toward + min(1, q*) only ever comes from BELOW. The actionable verdict: broken grid, + wrong MPCmin/hNrm reference, or a non-converged solve. + + Parameters + ---------- + cFunc : callable + Solved 1D consumption function (a slice of a 2D solution or a 1D + cFunc); called as ``cFunc(m_array)``. + MPCmin : float + PF asymptote slope kappa (from primitives, e.g. ``params.kappa``). + hNrm : float + Theorem-convention human wealth IN THE MODEL'S INCOME UNITS, i.e. + ``params.h * params.E_inc`` computed from primitives. Do NOT pass + ``solution.hNrm`` (tolerance-truncated) or ``bilt['hNrm']`` (includes + current income); either poisons the gap — see the h-convention tag in + the module docstring. + params : PowerLawDecayParams + Theory quantities for the same primitives; supplies q = min(1, q*). + m_lo, m_hi : float, optional + Test window. ``m_hi`` defaults to half the top of the solved grid when + discoverable (``cFunc.x_list``), else it is required; ``m_lo`` + defaults to ``0.1*m_hi``. + n_pts : int + Log-spaced sweep size (>= 20 recommended). + trial_offsets : tuple of float + Figure-4 probe offsets around q (must include 0.0). + flat_tol, inconsistency_tol, guard_rel_gap : float + Pre-registered thresholds described above. + + Returns + ------- + TailDiagnostic + """ + if 0.0 not in tuple(trial_offsets): + raise ValueError("trial_offsets must include 0.0 (the s = q probe)") + q = float(getattr(params, "q", float("nan"))) + s_grid = np.array(sorted(q + np.asarray(trial_offsets, dtype=float))) + if not np.isfinite(q): + return TailDiagnostic( + verdict="UNMEASURABLE", s_grid=s_grid, + slopes=np.full(s_grid.shape, np.nan), q_theory=q, + window=(np.nan, np.nan), n_points=0, + notes="theory exponent undefined (q_star is nan): " + + (params.diagnosis or "see params.warnings"), + ) + if m_hi is None: + x_list = getattr(cFunc, "x_list", None) + if x_list is None: + raise ValueError( + "m_hi is required when the solved grid top is not discoverable " + "from cFunc (no x_list attribute)" + ) + m_hi = 0.5 * float(np.asarray(x_list)[-1]) + if m_lo is None: + m_lo = 0.1 * m_hi + if not (0.0 < m_lo < m_hi): + raise ValueError(f"need 0 < m_lo < m_hi, got ({m_lo}, {m_hi})") + + m = np.geomspace(m_lo, m_hi, int(n_pts)) + c = np.asarray(cFunc(m), dtype=float) + x = m + hNrm + gap = MPCmin * x - c + keep = np.isfinite(gap) & np.isfinite(c) & (gap > 0.0) & (c > 0.0) + keep &= np.where(keep, gap > guard_rel_gap * np.abs(c), False) + n_keep = int(keep.sum()) + notes = [] + if n_keep < int(n_pts) // 2: + return TailDiagnostic( + verdict="UNMEASURABLE", s_grid=s_grid, + slopes=np.full(s_grid.shape, np.nan), q_theory=q, + window=(float(m[keep][0]), float(m[keep][-1])) if n_keep else + (np.nan, np.nan), + n_points=n_keep, + notes=f"only {n_keep}/{int(n_pts)} points survive the " + f"gap/c > {guard_rel_gap:g} guard (float-cancellation floor " + "or negative gap: wrong reference or window too deep)", + ) + ln_x = np.log(x[keep]) + ln_gap = np.log(gap[keep]) + # windowed log-log fit over the surviving points (the >= 20-point + # estimator; never the 2-knot top slope) + Q_local = -float(np.polyfit(ln_x, ln_gap, 1)[0]) + slopes = s_grid - Q_local + center = q - Q_local + if abs(center) <= flat_tol: + verdict = "CONFIRMED" + elif flat_tol < center <= inconsistency_tol: + verdict = "PRE_ASYMPTOTIC" + if bool(getattr(params, "near_resonance", False)): + notes.append( + "expected at this near-resonance calibration; deepen the grid " + "only if the exponent itself is under test" + ) + else: + verdict = "INCONSISTENT" + if center < 0.0: + notes.append( + f"local exponent {Q_local:.3f} STEEPER than min(1, q*) = " + f"{q:.3f}: impossibility-floor side (Prop A0)" + ) + else: + notes.append( + f"flat point at s = {Q_local:.3f}, far below min(1, q*) = " + f"{q:.3f}: wrong-exponent signature (broken grid, wrong " + "MPCmin/hNrm reference, or non-converged solve)" + ) + notes.insert(0, f"local exponent Q_local = {Q_local:.4f}; " + f"center slope(s=q) = {center:+.4f}") + return TailDiagnostic( + verdict=verdict, s_grid=s_grid, slopes=slopes, q_theory=q, + window=(float(m[keep][0]), float(m[keep][-1])), n_points=n_keep, + notes="; ".join(notes), + ) + + +# --------------------------------------------------------------------------- +# Certified grid-extent criterion +# --------------------------------------------------------------------------- +def rel_gap_at(cFunc, m, MPCmin, hNrm): + """Relative consumption gap (kappa*(m + h) - c(m)) / c(m) at ``m``. + + The measurement the extent criterion consumes, and the quantity its + ex-post certificate bounds. ``MPCmin``/``hNrm`` must come from + primitives (``params.kappa`` and ``params.h * params.E_inc`` in solver + units) — never from ``solution.hNrm`` (tolerance-truncated) or + ``bilt['hNrm']`` (h + 1 convention). + """ + m = np.atleast_1d(np.asarray(m, float)) + c = np.asarray(cFunc(m), float) + with np.errstate(divide="ignore", invalid="ignore"): + out = (MPCmin * (m + hNrm) - c) / c + # scalar in -> scalar out; empty in -> empty out (never index size 0) + return float(out[0]) if out.size == 1 else out + + +def aXtraMax_from_tail_tol(m_ref, rel_gap_ref, q_eff, hNrm, tail_tol, + safety=1.5, B_psi=None, MPCmin=None): + """Grid-extent inversion: the m at which the RELATIVE consumption gap + reaches ``tail_tol`` — i.e. where the grid may stop and hand the tail to + the power-law extrapolation with a certified error bound. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/statement.md :: Theorem A1 :: leading exponent is `min(1, q*)` :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # gap(x) ≍ x^(-min(1,q*)) with x = m + h; with c ~ kappa*x the RELATIVE + # gap decays one power faster, gap/c ∝ x^(-(1+q)), so a reference + # measurement inverts to x_top = x_ref*(rel_gap_ref/tail_tol)^(1/(1+q)). + + Two routes, chosen by what the calibration offers: + + * **q* > 1 (closed-form GUARANTEE — preferred when available).** Pass + ``B_psi`` and ``MPCmin`` (``params.B_psi``, ``params.kappa``): the + compensated gap x*g(x) climbs MONOTONICALLY to B_psi, so + ``gap(x) <= B_psi/x`` everywhere and + ``x_top = sqrt(B_psi/(MPCmin*tail_tol))`` guarantees the relative gap + at the top is <= tail_tol (up to an O(tail_tol) denominator + correction). Measured at the GIC-cap anchor: ex-post 6.9e-5 at + tail_tol = 1e-4. + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/alt_proof_compactified.md :: Theorem γ-B (Stage-B boundary value). PROVEN-HERE. :: `M_n` increasing to the plateau :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/alt-proof-compactified/] + # W1(z) = x*g(x) extends continuously to z = 0 with boundary value + # B_psi, approached monotonically from below at q* > 1 — hence + # B_psi/x bounds the gap from above on the whole tail. + * **q* <= 1 (measured inversion).** No closed-form amplitude exists; + invert the measured reference gap with the power law above. CAUTION, + near-resonance: when ``lambda_B`` is within the warning band the + compensated amplitude is still climbing on any feasible window, the + relative gap decays SLOWER than the assumed power law, and the + inversion under-sizes (measured at the near-resonance q* > 1 anchor: + ex-post 1.8x the target, converging only slowly under repair — use the + closed form there instead). Measured on the estimated HAFiscal + anchors: HS ex-post 0.40x tail_tol, College-top 0.98x (the default + safety=1.5 just covers it). + + Ex-post certificate (the guarantee that motivates the criterion): the true + consumption function is strictly concave with c' > MPCmin at every finite + m (Carroll-Kimball concavity + RIC/FHWC), so the gap is positive and + strictly DECREASING; a level-matched, monotone-decaying below-line tail + extrapolant therefore stays, together with the true c, inside the band + [PF-line - gap(m_top), PF-line] for every m >= m_top, giving pointwise + |c_extrap - c_true| <= gap(m_top) and relative error <= + rel_gap_at(cFunc, m_top, ...) <= tail_tol. ALWAYS check it after solving + on the delivered grid (it is the binding guarantee; the a-priori sizing is + a heuristic); on failure, one repair step is + ``x_top' = x_top * (measured/tail_tol)^(1/(1+q_eff))``. The + amplitude-jump mode of the tail law is excluded from this certificate (it + is not level-matched). + + Parameters + ---------- + m_ref : float + Reference point (typically the top endogenous gridpoint of a coarse + solve) where the relative gap was MEASURED. + rel_gap_ref : float + ``rel_gap_at(cFunc, m_ref, MPCmin, hNrm)`` from that solve. + q_eff : float + The inversion exponent's q. Use ``min(params.q, Q_local)`` with + Q_local the fitted local log-log gap slope of the coarse solve. The + min() is a GUARD for deep reference solves, where the local exponent + can sit below min(1, q*); at shallow, h-dominated coarse windows the + fitted slope is INFLATED (a tiny ln-x window) and the guard is + inert — inverting with the asymptotic q is then the binding choice + and errs conservative at q* < 1. The guard cannot help at + q* >= 1 (it caps at 1): use the closed-form route there. + hNrm : float + ``params.h * params.E_inc`` (solver units; see the h-convention + warning in this module's docstring). + tail_tol : float + Target relative gap at the top. Values below 1e-6 are clamped: the + measured float64 gap dies into cancellation around gap/c ~ 1e-10, so + tighter targets are not certifiable ex post. + safety : float + Multiplies the ratio before inversion (default 1.5) to absorb + reference-measurement error; must be finite and positive (else nan). + The ex-post certificate remains the binding check. + B_psi, MPCmin : float, optional + When BOTH are finite and positive (q* > 1 calibrations), the + closed-form guaranteed route is used and the measured-reference + arguments are ignored. + + Returns + ------- + float + The certified grid top (aXtraMax-scale, same units as m_ref), or nan + when the inputs cannot support the inversion (non-finite or + non-positive rel_gap_ref, q_eff, or safety). + """ + tail_tol = max(float(tail_tol), 1.0e-6) if np.isfinite(tail_tol) \ + else float("nan") + if not np.isfinite(tail_tol): + return float("nan") + if B_psi is not None and MPCmin is not None: + B_psi, MPCmin = float(B_psi), float(MPCmin) + if np.isfinite(B_psi) and B_psi > 0.0 and np.isfinite(MPCmin) \ + and MPCmin > 0.0 and np.isfinite(hNrm): + x_top = np.sqrt(B_psi / (MPCmin * tail_tol)) + return float(x_top - hNrm) + return float("nan") + m_ref, rel_gap_ref = float(m_ref), float(rel_gap_ref) + q_eff, hNrm, safety = float(q_eff), float(hNrm), float(safety) + if not (np.isfinite(m_ref) and np.isfinite(rel_gap_ref) + and rel_gap_ref > 0.0 and np.isfinite(q_eff) and q_eff > 0.0 + and np.isfinite(hNrm) and np.isfinite(safety) and safety > 0.0): + return float("nan") + x_ref = m_ref + hNrm + x_top = x_ref * (safety * rel_gap_ref / tail_tol) ** (1.0 / (1.0 + q_eff)) + return float(x_top - hNrm) + + +def aXtraMin_from_tail_tol(me_ref, c_ref, kap_bar, rho, tail_tol, + safety=1.5, K=None): + """Grid-extent inversion at the CONSTRAINT end: the aXtraMin at which the + bottom knot sits inside the constraint asymptote (the kap_bar line) to + relative tolerance ``tail_tol`` -- the mirror of ``aXtraMax_from_tail_tol``. + + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-cor-C4 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Bottom grid rule (st-eq-C4): the relative deviation of c from the + # kap_bar line is (kap_bar - c/me)/kap_bar = (K/kap_bar)*me**rho + # (Theorem CE), so the bottom knot satisfies the tolerance iff + # me_0 <= (tail_tol*kap_bar/K)**(1/rho). + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # The law inverted: c(me) = kap_bar*me - K*me**(1+rho)*(1+o(1)), exponent + # q_down = rho exactly (no eigenvalue problem at this end -- rho is the + # CRRA, so no root-finder is needed, unlike the top twin's q_star). + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-prop-C1 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # kap_bar = 1 - wp**(1/rho)*Thorn_R with wp the worst-atom mass; in HARK, + # solution.MPCmax (the analytic Prop C2 recursion, exact at any horizon). + + Route: K is the one calibration-specific amplitude (explicit only modulo + boundary data -- statement.md Remark C2), so it is MEASURED by + value-matching at a reference node of a coarse solve, + ``K = (kap_bar*me_ref - c_ref)/me_ref**(1+rho)``, then inverted through + st-eq-C4. Pass ``K`` directly to skip the measurement (e.g. reusing a + previously certified amplitude). + + The returned aXtraMin CONVERTS the knot rule from m-space to a-space: the + bottom EGM knot generated by a grid point ``a`` sits at + ``me_0 = a + c(me_0)``, and concavity plus c(mNrmMin) = 0 give + ``c <= kap_bar*me`` everywhere, hence ``me_0 <= a/(1 - kap_bar)``; so + ``aXtraMin = (1 - kap_bar)*me_target`` GUARANTEES ``me_0 <= me_target`` + (requires kap_bar < 1, true whenever wp > 0 and Thorn_R > 0). + + Ex-post certificate (the binding check, as for the top twin): after + solving on the delivered grid, measure the relative deviation + ``(kap_bar - c_1/me_1)/kap_bar`` at the bottom knot; on failure repair with + ``me_target' = me_target*(tail_tol/measured)**(1/rho)`` and re-solve. + + Parameters + ---------- + me_ref : float + Excess resources ``m - mNrmMin`` at the reference node of a coarse + solve (its bottom knot, typically). Must lie in the constraint-end + regime (relative deviation well below 1 -- st-cor-C4's own criterion). + c_ref : float + Consumption at that node. + kap_bar : float + The maximal MPC (HARK: ``solution.MPCmax``; infinite horizon: + ``1 - wp**(1/rho)*Thorn_R``). Must be in (0, 1). + rho : float + CRRA. The constraint-end exponent itself (Theorem CE). + tail_tol : float + Target relative deviation from the kap_bar line at the bottom knot. + Clamped below at 1e-6 (float64 cancellation floor, as in the top twin: + the measured gap kap_bar*me - c dies into cancellation near the + constraint). + safety : float + Divides the target knot before conversion (default 1.5), absorbing + reference-measurement error; must be finite and positive (else nan). + K : float or None + Optional explicit amplitude, overriding the measured route. + + Returns + ------- + float + The certified ``aXtraMin`` (a-space, same normalization as me_ref), or + ``nan`` when the inputs cannot support the inversion (non-finite or + out-of-range inputs, or a reference node OUTSIDE the constraint-end + regime: K <= 0 means c_ref >= kap_bar*me_ref, i.e. the node is above + the kap_bar line -- wrong kap_bar or a knot too high, st-cor-C4). + """ + tail_tol = max(float(tail_tol), 1.0e-6) if np.isfinite(tail_tol) \ + else float("nan") + if not np.isfinite(tail_tol): + return float("nan") + kap_bar, rho, safety = float(kap_bar), float(rho), float(safety) + if not (np.isfinite(kap_bar) and 0.0 < kap_bar < 1.0 + and np.isfinite(rho) and rho > 0.0 + and np.isfinite(safety) and safety > 0.0): + return float("nan") + if K is None: + me_ref, c_ref = float(me_ref), float(c_ref) + if not (np.isfinite(me_ref) and me_ref > 0.0 + and np.isfinite(c_ref) and c_ref > 0.0): + return float("nan") + K = (kap_bar * me_ref - c_ref) / me_ref ** (1.0 + rho) + K = float(K) + if not (np.isfinite(K) and K > 0.0): + return float("nan") # reference node outside the regime (st-cor-C4) + me_target = (tail_tol * kap_bar / K) ** (1.0 / rho) / safety + return float((1.0 - kap_bar) * me_target) + + +def ce_psi_regime(IncShkDstn, CRRA, Thorn_Gamma): + """Classify the constraint end of a calibration: regime I (uniform + contraction, approach exponent ``q_down = CRRA``, Theorem CE-psi) versus + regime II (expanding worst fiber, ``q_down = min(CRRA, s*_+)``, + st-rem-CE-regime -- where the kappa_bar bottom tail is NOT theorem-backed). + + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE-psi :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Theorem CE-psi (regime I): if p_eff**(1/CRRA)*Thorn_Gamma < psi_min + # (lambda(psi_min) < 1), the constraint-end approach exponent is + # q_down = CRRA, MPC -> kap_bar, no log-periodic prefactor. + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-rem-CE-regime :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Regime II (lambda(psi_min) > 1): the (E-psi_down) eigen-equation + # acquires a positive root s*_+ and q_down = min(CRRA, s*_+); whether + # the x**(s*_+) mode has generic nonzero amplitude is OPEN + # (GAP-CE-psi-II). + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-def-ce-psi-objects :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # p_eff = the worst-JOINT-atom mass (the fiber-selection effect, proof + # par.4): the pmv mass at the minimum of the joint income product + # psi*theta -- exactly HARK's WorstIncPrb accounting + # (ConsIndShockModel.calc_worst_inc_prob). + + Parameters + ---------- + IncShkDstn : DiscreteDistribution-like or (atoms_2xN, probs) + HARK-style JOINT income distribution (atoms row 0 = permanent, row 1 + = transitory); accepts the same shapes as ``powerlaw_decay_params``. + CRRA : float + Relative risk aversion (the theorem's rho). + Thorn_Gamma : float + Growth patience factor (Rfree*DiscFac*LivPrb)**(1/CRRA)/PermGroFac -- + mortality folded as impatience, the module-wide convention. At the + solver's assembly site this is ``PatFac*Rfree/PermGroFac``. + + Returns + ------- + dict + ``regime`` ('I' | 'II' | 'undetermined'), ``lambda_min_fiber``, + ``psi_min``, ``p_eff``, ``Thorn_Gamma``, ``CRRA``, ``criterion`` + (the primitive inequality, printed), ``anchor`` (the statement.md + anchor the classification rests on). The boundary + ``lambda_min_fiber == 1`` is classified 'II' (the theorem's + hypothesis is the STRICT inequality); 'undetermined' is the + fail-closed verdict for non-finite inputs. + """ + psi_j, th_j, p_j = _as_joint(IncShkDstn) + # worst-JOINT-atom mass, float64 arithmetic mirroring calc_worst_inc_prob + # (ties summed; with a zero transitory atom every psi fiber of the worst + # event attains the joint minimum 0, so p_eff = the full worst-theta mass) + psi_f = np.asarray(psi_j, float) + p_f = np.asarray(p_j, float) + income = psi_f * np.asarray(th_j, float) + p_eff = float(p_f[income == income.min()].sum()) + psi_min = float(psi_f[p_f > 0].min()) + rho = float(CRRA) + PG = float(Thorn_Gamma) + with np.errstate(invalid="ignore"): + lam = p_eff ** (1.0 / rho) * PG / psi_min + if not (np.isfinite(lam) and rho > 0.0 and psi_min > 0.0 and p_eff > 0.0): + return dict( + regime="undetermined", lambda_min_fiber=float("nan"), + psi_min=psi_min, p_eff=p_eff, Thorn_Gamma=PG, CRRA=rho, + criterion="lambda(psi_min) not computable from these inputs", + anchor="st-rem-CE-regime") + regime = "I" if lam < 1.0 else "II" + return dict( + regime=regime, lambda_min_fiber=float(lam), psi_min=psi_min, + p_eff=p_eff, Thorn_Gamma=PG, CRRA=rho, + criterion="p_eff**(1/CRRA)*Thorn_Gamma %s psi_min (lambda = %.6g)" + % ("<" if regime == "I" else ">=", lam), + anchor=("st-thm-CE-psi" if regime == "I" else "st-rem-CE-regime")) + + +# --------------------------------------------------------------------------- +# The operator eigen-probe: numerical q* without the eigen-equation +# --------------------------------------------------------------------------- +def qstar_probe(Rfree, PermGroFac, DiscFac, CRRA, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, IncShkDstn=None, + x0s=(1e6, 1e7, 1e8), eps=1e-4, s_lo=0.05, s_hi=5.0, + one_step=None, MPCmin=None, hNrm=None): + """Measure the decay-exponent root q* NUMERICALLY from the model's own + one-period backward operator — no closed-form eigen-equation required. + + The exponent is the eigenvalue condition of the period operator acting on + power-law perturbations of the PF asymptote. Working in the EGM's native + END-OF-PERIOD-ASSET coordinate (the probe never touches a grid): at deep + a = x0 - hNrm, apply one backward step to the trial next-period function + + c_trial(m') = MPCmin*(m' + hNrm) - eps*MPCmin*x0*((m' + hNrm)/x0)^(-s), + + difference the eps > 0 and eps = 0 responses (cancels the one-period + Arrow-Pratt premium, isolates the linear response), and root-find the s + at which the per-period gap multiplier equals one. Normalizing the trial + at the probe point is LOAD-BEARING: an unnormalized eps*x^(-s) trial at + x = 1e8 is ~1e-40, beneath float64 resolution of the gap. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §0 "What is q*? (and why min(1, q*))" :: eq (E) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The analytic eigen-equation E[psi^(1+q)] = Rcal*Thorn_Gamma^q is this + # probe evaluated on paper: the root of the one-period multiplier on + # x^(-q) gap perturbations. The probe is the model-agnostic form. + + # THEOREM-REF[BufferStockTheory-Latest @ 0d1f88d8 :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: The operator eigen-probe :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # Validation (F9): matches the analytic q* to 5.6e-6 / 8.6e-6 / 5.0e-5 + # at the HS / CTOP / CCAP anchors with ~1e-6 depth-consistency, while + # estimation FROM SOLVED VALUES is 15-40% off even on deep windows (the + # h-shift starves the identifying x-variation). Outside GIC the probe + # (true finite-x operator) and the (E)-root (idealized limit) disagree — + # report q_hat only alongside the condition flags. + + Portability: for the standard CRRA model, primitives suffice (the step is + built internally). For OTHER models, pass ``one_step(c_trial, a_array) -> + c_today_array`` (the model's backward step applied to a supplied + next-period consumption function) together with that model's PF limits + ``MPCmin``/``hNrm`` — anything solved by time iteration has all three. + + Returns + ------- + (q_hat, consistency, diagnosis) : (float, float, str) + q_hat: the multiplier's unit root (realized decay exponent is + min(1, q_hat) — the universal 1/x premium floor, Prop A0); nan when + no root lies in (s_lo, s_hi) or inputs fail (never raises). + consistency: relative spread of the multiplier across the probe + depths ``x0s`` at the root (a built-in self-check; ~1e-6 measured). + diagnosis: 'ok' or the failure reason. + """ + try: + if MPCmin is None or hNrm is None: + base = powerlaw_decay_params( + Rfree, PermGroFac, DiscFac, CRRA, LivPrb=LivPrb, + PermShkDstn=PermShkDstn, TranShkDstn=TranShkDstn, + IncShkDstn=IncShkDstn, warn=False) + MPCmin = base.kappa if MPCmin is None else MPCmin + hNrm = base.h * base.E_inc if hNrm is None else hNrm + MPCmin, hNrm = float(MPCmin), float(hNrm) + if not (np.isfinite(MPCmin) and MPCmin > 0.0 and np.isfinite(hNrm)): + return float("nan"), float("nan"), \ + "PF asymptote unavailable (MPCmin/hNrm non-finite)" + if one_step is None: + R = float(_time_indexed(Rfree, 0)) + G = float(_time_indexed(PermGroFac, 0)) + L = float(_time_indexed(LivPrb, 0)) + beta, rho = float(DiscFac), float(CRRA) + if IncShkDstn is not None: + psi_j, th_j, wp = _as_joint(IncShkDstn) + psi_j = np.asarray(psi_j, float) + th_j = np.asarray(th_j, float) + wp = np.asarray(wp, float) + else: + if PermShkDstn is None: + psi_a, psi_p = np.array([1.0]), np.array([1.0]) + else: + psi_a, psi_p = _as_atoms_probs(PermShkDstn, "PermShkDstn") + th_a, th_p = _as_atoms_probs(TranShkDstn, "TranShkDstn") + PSI, TH = np.meshgrid(np.asarray(psi_a, float), + np.asarray(th_a, float), indexing="ij") + wp = np.outer(np.asarray(psi_p, float), + np.asarray(th_p, float)).ravel() + psi_j, th_j = PSI.ravel(), TH.ravel() + wp = wp / wp.sum() + + def one_step(c_trial, a): + m_img = (R / (G * psi_j))[None, :] * a[:, None] + th_j[None, :] + c_next = c_trial(m_img) + rhs = beta * L * R * (wp[None, :] + * (G * psi_j[None, :]) ** (-rho) + * c_next ** (-rho)).sum(1) + return rhs ** (-1.0 / rho) + + def lam_at(s, x0): + a = np.array([x0 - hNrm]) + c0 = MPCmin * x0 + + def response(e): + def c_trial(m_img): + x_img = m_img + hNrm + return MPCmin * x_img - e * c0 * (x_img / x0) ** (-s) + c_t = one_step(c_trial, a) + x_t = float(a[0] + c_t[0] + hNrm) + return float(MPCmin * x_t - c_t[0]), x_t + + g1, x_t = response(0.0) + g2, _ = response(eps) + return (g2 - g1) / (eps * c0 * (x_t / x0) ** (-s)) + + def lam(s): + vals = [lam_at(s, float(x0)) for x0 in x0s] + m = float(np.mean(vals)) + return m, (float(np.std(vals) / abs(m)) if m != 0.0 + else float("inf")) + + lo, hi = float(s_lo), float(s_hi) + flo = lam(lo)[0] - 1.0 + fhi = lam(hi)[0] - 1.0 + if not (np.isfinite(flo) and np.isfinite(fhi)) or flo * fhi > 0: + return float("nan"), float("nan"), \ + f"no unit-multiplier root in ({s_lo}, {s_hi}) (fails closed)" + for _ in range(60): + mid = 0.5 * (lo + hi) + fm = lam(mid)[0] - 1.0 + if flo * fm <= 0: + hi = mid + else: + lo, flo = mid, fm + q_hat = 0.5 * (lo + hi) + return float(q_hat), lam(q_hat)[1], "ok" + except Exception as exc: # behavior contract: never raises + return float("nan"), float("nan"), f"probe failed: {exc!r}" + + +# --------------------------------------------------------------------------- +# Stable points (targets), with the mortality-adjusted (L*R) loci +# --------------------------------------------------------------------------- +@dataclass(frozen=True) +class StablePoints: + """Roots of the consumption function against the stable-point loci, in + BOTH coordinates: the m-roots and their end-of-period-asset images + a = m - c(m) (the grid-relevant objects — the asset grid, the consumed + function c(a), and everything the solver controls live in a-space).""" + mNrmTrg: float # E[m'] = m (needs GIC-Mod); nan if no crossing + mNrmStE: float # E[psi*m'] = m, balanced LEVEL growth (GIC-Raw) + mNrmTrg_mort: float # Delta-m=0 locus with R -> LivPrb*R (GIC-Mod-Liv) + mNrmStE_mort: float # balanced-growth locus with R -> LivPrb*R + aNrmTrg: float = float("nan") + aNrmStE: float = float("nan") + aNrmTrg_mort: float = float("nan") + aNrmStE_mort: float = float("nan") + E_theta: float = float("nan") + E_inv_psi: float = float("nan") + notes: str = "" + + +def mNrm_stable_points(cFunc, Rfree, PermGroFac, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, IncShkDstn=None, + m_hi=1.0e4): + """All four stable points of a solved consumption function: the two + classical loci and their mortality-adjusted twins, plus a-space images. + + Loci (independent shocks, E[psi] = 1; a(m) = m - c(m)): + Trg (E[m'] = m): c(m) = m - (m - E[theta]) / ((R/Gamma)*E[1/psi]) + StE (E[psi*m'] = m): c(m) = m - (m - E[theta]) * Gamma/R + *_mort: replace R by LivPrb*R. With perpetual-youth replacement + (newborns at a = 0 drawing the same transitory income), mortality + factors EXACTLY as this return shave in the cross-sectional mean + dynamics, so the adjusted loci exist under GIC-Mod-Liv / GIC-Raw-Liv + even when the unadjusted target does not (the pure-GIC case). + + # THEOREM-REF[BufferStockTheory-Latest @ 0d1f88d8 :: theory/powerlaw-decay/grid_design_final_spec.md :: The findings ledger (each measured this arc, HS/CTOP/CCAP anchors) :: mortality EXACTLY as a return shave :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # Measured at the GIC-cap atom (GIC-Mod fails; unadjusted StE = 340.9): + # the L*R-adjusted balanced-growth root 40.18 lands on the + # Harmenberg-neutral ergodic mean 41.23 to 2.5% (Jensen sign correct, + # a(m) convex). The MEASURE picks the locus: neutral-measure + # aggregation has E_N[1/psi] = 1, so its mean-dynamics anchor is the + # StE locus; the raw cross-section carries E[1/psi] and anchors on Trg. + + ``cFunc`` is any callable m -> c (duck-typed, like + ``powerlaw_tail_diagnostic``). Roots by bisection on + [E[theta]/2 + 1e-6, m_hi]; nan (never an exception) when a locus does not + cross — existence requires the corresponding growth condition. + """ + R = float(_time_indexed(Rfree, 0)) + G = float(_time_indexed(PermGroFac, 0)) + L = float(_time_indexed(LivPrb, 0)) + if IncShkDstn is not None: + psi_j, th_j, wp = _as_joint(IncShkDstn) + psi_j = np.asarray(psi_j, float) + th_j = np.asarray(th_j, float) + wp = np.asarray(wp, float) / np.asarray(wp, float).sum() + E_th = float((wp * th_j).sum()) + E_ip = float((wp / psi_j).sum()) + else: + if PermShkDstn is None: + E_ip = 1.0 + else: + pa, pp = _as_atoms_probs(PermShkDstn, "PermShkDstn") + E_ip = float((np.asarray(pp, float) / np.asarray(pa, float)).sum()) + ta, tp = _as_atoms_probs(TranShkDstn, "TranShkDstn") + E_th = float((np.asarray(tp, float) * np.asarray(ta, float)).sum()) + + def _root(locus): + lo = 0.5 * E_th + 1e-6 + hi = float(m_hi) + f = lambda m: float(np.atleast_1d(cFunc(np.array([m])))[0]) - locus(m) + try: + flo, fhi = f(lo), f(hi) + except Exception: + return float("nan") + if not (np.isfinite(flo) and np.isfinite(fhi)) or flo * fhi > 0: + return float("nan") + for _ in range(200): + mid = 0.5 * (lo + hi) + fm = f(mid) + if flo * fm <= 0: + hi = mid + else: + lo, flo = mid, fm + if hi - lo < 1e-12 * max(1.0, mid): + break + return 0.5 * (lo + hi) + + m_trg = _root(lambda m: m - (m - E_th) / ((R / G) * E_ip)) + m_ste = _root(lambda m: m - (m - E_th) * G / R) + m_trg_L = _root(lambda m: m - (m - E_th) / ((L * R / G) * E_ip)) + m_ste_L = _root(lambda m: m - (m - E_th) * G / (L * R)) + + def _a_of(m): + if not np.isfinite(m): + return float("nan") + return float(m - np.atleast_1d(cFunc(np.array([m])))[0]) + + return StablePoints( + mNrmTrg=m_trg, mNrmStE=m_ste, + mNrmTrg_mort=m_trg_L, mNrmStE_mort=m_ste_L, + aNrmTrg=_a_of(m_trg), aNrmStE=_a_of(m_ste), + aNrmTrg_mort=_a_of(m_trg_L), aNrmStE_mort=_a_of(m_ste_L), + E_theta=E_th, E_inv_psi=E_ip, + notes="mort loci: R -> LivPrb*R (exact for cross-sectional mean " + "dynamics under perpetual-youth a=0 newborns)") + + +# --------------------------------------------------------------------------- +# The aXtraMax mass rule: measure the stationary wealth quantile +# --------------------------------------------------------------------------- +@dataclass +class WealthMassInfo: + """Diagnostics bundle for :func:`aXtraMax_from_wealth_mass`.""" + a_max: float = float("nan") + zeta_L: Optional[float] = None # mortality-augmented dual root + zeta_diagnosis: Optional[str] = None + anchor_a: float = float("nan") # a-image of the mortality-adjusted StE + a_cover: float = float("nan") # covering-grid top used for measurement + cover_adequate: bool = False + quantile_table: dict = None # {(measure, eps): a-quantile} 3x2 dial + measure: str = "" + eps_wealth: float = float("nan") + safety: float = float("nan") + diagnosis: str = "" + + +def aXtraMax_from_wealth_mass(cFunc, Rfree, PermGroFac, DiscFac, CRRA, + LivPrb=1.0, PermShkDstn=None, TranShkDstn=None, + IncShkDstn=None, eps_wealth=1e-4, + measure="wealth", safety=1.25, probe_count=1024, + a_cover=None, round_to=None): + """The asset-grid top as a MEASURED stationary wealth quantile: the + smallest a_max leaving at most ``eps_wealth`` of the chosen mass above the + grid, from the deterministic stationary distribution implied by ``cFunc``. + + Why each choice (the design rationale, in code where it belongs): + + * WHY MEASURED, not closed-form: the Pareto-asymptotic inversion of the + tail exponent is a poor quantile estimator in exactly the regimes that + matter — measured against truth it UNDERSHOOTS (unsafely) when the tail + exponent is large (the quantile sits only a few multiples of the target, + before the Pareto regime begins) and overshoots by orders of magnitude + as the exponent nears 1 (hypersensitive (1/(zeta-1)) power). The closed + form therefore serves ONLY as the generous covering-grid top for this + measurement; the RULE is the measured quantile. + * WHY the tail exponent still matters — zeta_L, the MORTALITY-AUGMENTED + dual root (``dual_root(..., LivPrb)``), is the patience dial and the + existence test: it exists whenever an expanding branch exists, including + at the GIC patience cap where the classical Kesten root does not + (mortality truncates the patient tail); more patience => smaller zeta_L + => deeper top, automatically. zeta_L <= 1 means aggregate wealth is not + finite: no wealth-measure top exists and this function REFUSES rather + than gridding away an economic property (fresh anchors measured 9.74 / + 4.34 / 1.92 across the patience range). + * WHY the WEALTH measure (default): moment consumers (Lorenz-type + statistics, aggregate wealth) weight states by wealth, and the wealthy + few dominate those moments; the agent measure is offered for + distributional work where heads count. Wealth quantiles are deeper than + agent quantiles at every tolerance (measured 3-7x at the patient edge). + * WHY eps_wealth may be MODERATE: with the power-law tail law attached + above a_max, the mass beyond the grid is PRICED (to corridor accuracy), + not lost — the induced wealth-moment bias is of order eps_wealth times + the tail-law error, several orders below eps_wealth itself. + * WHY the anchor: the covering scale is the a-image of the + mortality-adjusted balanced-growth point (``mNrm_stable_points``), + which tracks the stationary mean even in the pure-GIC case. + * Populations: apply per type/atom and take the max — the most patient + atom binds the common grid. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §6.1 How rarely is the tail visited? The dual (Kesten) root, for economists :: Patience fattens the tail :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The stationary wealth tail is Pareto with the dual-root exponent, and + # patience fattens it — the theory reason the grid top must be + # patience-sensitive, delivered here through zeta_L and the measurement. + + # THEOREM-REF[BufferStockTheory-Latest @ 716cfd82 :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: THE RULE IS MEASURED :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # F10 of the grid-design record: the measured-quantile rule, the + # cover-only role of the closed form (0.38x unsafe / 128x over at the + # patience extremes), and the fresh quantile table across + # (measure, eps) on the anchor calibrations. + + Parameters: ``cFunc`` = a solved consumption function (a coarse solve + suffices — the stationary objects it implies are second-order-insensitive + to solve refinement); shock inputs as elsewhere in this module; + ``eps_wealth`` = the one tolerance (share of the chosen mass allowed above + the grid); ``measure`` in {"wealth", "agent"}; ``safety`` multiplies the + measured quantile; ``a_cover`` overrides the covering top; ``round_to`` + optionally ceils a_max to a multiple. Returns ``(a_max, WealthMassInfo)`` + — a_max nan (never an exception) with ``info.diagnosis`` on refusal. The + certificate is one re-call on a cFunc RE-SOLVED with the returned top: + unchanged within a few percent closes the loop. The certificate step is + LOAD-BEARING for patient types whose own body extends beyond the first + solve's top (measured at the patience-cap anchor: the single-call + quantile is 33% short when the coarse top sits inside that atom's body; + the certified re-call lands within 3.4% of truth — and within 0.1% at + the impatient anchors). Deterministic throughout (power iteration from + the uniform start; no RNG). + """ + info = WealthMassInfo(measure=str(measure), eps_wealth=float(eps_wealth), + safety=float(safety), quantile_table={}) + try: + theory = powerlaw_decay_params( + Rfree, PermGroFac, DiscFac, CRRA, LivPrb=LivPrb, + PermShkDstn=PermShkDstn, TranShkDstn=TranShkDstn, + IncShkDstn=IncShkDstn, warn=False) + R = float(_time_indexed(Rfree, 0)) + G = float(_time_indexed(PermGroFac, 0)) + L = float(_time_indexed(LivPrb, 0)) + if IncShkDstn is not None: + psi_j, th_j, wp = _as_joint(IncShkDstn) + psi_j = np.asarray(psi_j, float) + th_j = np.asarray(th_j, float) + wp = np.asarray(wp, float) + psi_a, psi_p = _marginal(psi_j, wp) + psi_a, psi_p = np.asarray(psi_a, float), np.asarray(psi_p, float) + else: + if PermShkDstn is None: + psi_a, psi_p = np.array([1.0]), np.array([1.0]) + else: + pa, pp = _as_atoms_probs(PermShkDstn, "PermShkDstn") + psi_a, psi_p = np.asarray(pa, float), np.asarray(pp, float) + ta, tp = _as_atoms_probs(TranShkDstn, "TranShkDstn") + PSI, TH = np.meshgrid(psi_a, np.asarray(ta, float), indexing="ij") + wp = np.outer(psi_p, np.asarray(tp, float)).ravel() + psi_j, th_j = PSI.ravel(), TH.ravel() + wp = wp / wp.sum() + + z, _, _, z_diag = dual_root(psi_a, psi_p, theory.Thorn_Gamma, + LivPrb=L) + info.zeta_L, info.zeta_diagnosis = z, z_diag + if measure == "wealth" and z is not None and z <= 1.05: + info.diagnosis = ( + f"REFUSED: zeta_L = {z:.4f} <= 1.05 — aggregate wealth is not " + f"(or barely) finite; a wealth-measure grid top is economically " + f"arbitrary. Use measure='agent' or revisit the calibration.") + return float("nan"), info + + # anchor scale: a-image of the mortality-adjusted balanced-growth root + sp = mNrm_stable_points(cFunc, R, G, LivPrb=L, + PermShkDstn=(psi_a, psi_p), + TranShkDstn=(np.asarray(th_j, float), + wp) if IncShkDstn is not None + else TranShkDstn) + anchor = sp.aNrmStE_mort + if not np.isfinite(anchor) or anchor <= 0: + anchor = sp.aNrmStE + if not np.isfinite(anchor) or anchor <= 0: + anchor = max(sp.E_theta, 1.0) + info.anchor_a = float(anchor) + + # covering top: closed-form Pareto inversion, clipped generously — + # cover-only (see docstring); compact/no-tail case gets a flat multiple + if a_cover is None: + if z is not None and z > 1.05: + a_cover = anchor * (1.0 / min(eps_wealth, 1e-4)) \ + ** (1.0 / (z - 1.0)) + a_cover = float(np.clip(a_cover, 50.0 * anchor, + 1.0e4 * anchor)) + else: + a_cover = 50.0 * anchor + a_cover = float(a_cover) + + eps_grid = (1e-2, 1e-3, 1e-4) + for _attempt in range(4): + info.a_cover = a_cover + # m-cover such that a(m_cover) >= a_cover + m_cover = a_cover + for _ in range(80): + a_at = m_cover - float(np.atleast_1d( + cFunc(np.array([m_cover])))[0]) + if a_at >= a_cover: + break + m_cover *= 1.5 + probe = np.geomspace(1e-4, m_cover, int(probe_count)) + c_p = np.asarray(cFunc(probe), float) + a_of = np.maximum(probe - c_p, 0.0) + # deterministic neutral-measure kernel with mortality + a=0 newborns + wN = wp * psi_j + wN = wN / wN.sum() + images = (R / (G * psi_j))[None, :] * a_of[:, None] \ + + th_j[None, :] + H = probe.size + idx = np.clip(np.searchsorted(probe, images) - 1, 0, H - 2) + lo_g = probe[idx] + hi_g = probe[idx + 1] + frac = np.clip((images - lo_g) / (hi_g - lo_g), 0.0, 1.0) + P = np.zeros((H, H)) + rows = np.repeat(np.arange(H), images.shape[1]) + np.add.at(P, (rows, idx.ravel()), + (np.broadcast_to(wN, images.shape) + * (1.0 - frac)).ravel()) + np.add.at(P, (rows, (idx + 1).ravel()), + (np.broadcast_to(wN, images.shape) * frac).ravel()) + nb_img = th_j[None, :] + nb_idx = np.clip(np.searchsorted(probe, nb_img) - 1, 0, H - 2) + nb_frac = np.clip((nb_img - probe[nb_idx]) + / (probe[nb_idx + 1] - probe[nb_idx]), 0.0, 1.0) + nb = np.zeros(H) + np.add.at(nb, nb_idx.ravel(), (wN * (1.0 - nb_frac)).ravel()) + np.add.at(nb, (nb_idx + 1).ravel(), (wN * nb_frac).ravel()) + P = L * P + (1.0 - L) * nb[None, :] + pi = np.full(H, 1.0 / H) + for _ in range(20000): + nxt = pi @ P + nxt /= nxt.sum() + if np.abs(nxt - pi).sum() < 1.0e-12: + pi = nxt + break + pi = nxt + table = {} + for name, mass in (("agent", pi), ("wealth", pi * a_of)): + mm = mass / mass.sum() if mass.sum() > 0 else mass + cm = np.cumsum(mm) + for e in eps_grid: + table[(name, e)] = float(np.interp(1.0 - e, cm, a_of)) + info.quantile_table = table + e_req = float(eps_wealth) + mm = (pi * a_of if measure == "wealth" else pi) + mm = mm / mm.sum() + a_q = float(np.interp(1.0 - e_req, np.cumsum(mm), a_of)) + if a_q <= 0.9 * a_of[-1]: + info.cover_adequate = True + break + a_cover *= 4.0 # quantile at the cover edge: extend and re-measure + a_max = a_q * float(safety) + if round_to: + a_max = float(np.ceil(a_max / round_to) * round_to) + info.a_max = a_max + info.diagnosis = "ok" if info.cover_adequate else \ + "WARNING: quantile still at the covering-grid edge after extension" + return a_max, info + except Exception as exc: # behavior contract: never raises + info.diagnosis = f"failed: {exc!r}" + return float("nan"), info + + +# =================================================================================== +# Ergodic-grid coverage diagnostics: the ex-ante patience screen + postmortem report. +# +# Per the owner ruling (2026-07-15), the ex-ante NUMERIC CORE is NOT reimplemented +# here: it is IMPORTED from BufferStockTheory-Latest's +# ``theory/powerlaw-decay/ergodic_coverage_lib.py`` (the single source of truth for +# this theory), so that no slightly-differing variant of the roots/ladder/regime +# logic ever exists in HARK. This module supplies only (a) the HARK agent-adapter +# around that imported core (Task A) and (b) a HARK-native postmortem that runs the +# agent's own simulation (Task B). THEORY OF RECORD: ``ergodic_coverage.md`` in +# BST-Latest; cite, do not re-derive. +# =================================================================================== + + +class ErgodicCoverageWarning(UserWarning): + """Advisory warning for a pathological ergodic-coverage screen (e.g. an + infinite-mean tail, or simulated mass spilling above the grid top). Filterable; + never raised as an exception, never triggers an action.""" + + +_ERGODIC_LIB_REQUIRED_API = 1 + + +def _get_ergodic_coverage_lib(): + """Soft-import BST-Latest's ``ergodic_coverage_lib`` — the imported numeric core. + + The import is LAZY (at call time, never at HARK import time): the ergodic screen + needs a local ``BufferStockTheory-Latest`` checkout, which public HARK CI does + not carry, so ``import HARK...pf_decay`` never fails on its account and only + *calling* an ergodic diagnostic without the module raises. Make the module + importable by putting its directory (``theory/powerlaw-decay``) on ``sys.path``, + or by setting ``$BST_POWERLAW_DECAY_DIR`` to it. The packaging/fork decision is + deliberately deferred to PR-merge time; do NOT vendor or reimplement the six + functions (own-request record: BST-Latest + ``_ai/prompts_local/20260715-1328h_prompt_from-HARK1782_...``). + """ + def _try_import(): + import ergodic_coverage_lib as _lib + return _lib + + try: + lib = _try_import() + except ImportError as exc: + import os + import sys + d = os.environ.get("BST_POWERLAW_DECAY_DIR") + if d and os.path.isdir(d) and d not in sys.path: + sys.path.insert(0, d) + try: + lib = _try_import() + except ImportError: + lib = None + else: + lib = None + if lib is None: + raise ImportError( + "the ergodic-coverage screen needs BufferStockTheory-Latest's " + "ergodic_coverage_lib (theory/powerlaw-decay). Put that directory " + "on sys.path, or set $BST_POWERLAW_DECAY_DIR to it. This is a " + "dev-phase diagnostic imported from the theory repo (the single " + "source of truth for the ergodic theory); it is dormant wherever " + "that checkout is absent. See PR #1782." + ) from exc + api = getattr(lib, "API_LEVEL", None) + if api != _ERGODIC_LIB_REQUIRED_API: + raise ImportError( + "ergodic_coverage_lib API_LEVEL %r != the %d this adapter targets — " + "the imported numeric core changed its contract. Update pf_decay's " + "ergodic adapter to the new level (or pin an older " + "ergodic_coverage_lib)." % (api, _ERGODIC_LIB_REQUIRED_API) + ) + return lib + + +@dataclass(frozen=True) +class ErgodicGridDiagnostics: + """Result of :func:`ergodic_grid_diagnostics` — the ex-ante patience screen for + grid design (numeric core imported from BST-Latest ``ergodic_coverage_lib``; + theory of record ``ergodic_coverage.md``). Plain floats/bools/strings.""" + + # patience ladder, raw vs mortality-EFFECTIVE (ergodic_coverage.md §1) + APF_raw: float + APF_eff: float + GPF_raw: float + GPF_eff: float + MPCmin_eff: float # 1 - APF_eff/R (== the solved policy's MPCmin) + E_inv_psi: float + E_log_psi: float + GICNrm_raw: float # GPF_raw*E[1/psi] (< 1 <=> GIC-Nrm holds) + GICNrm_eff: float + GICNrm_raw_holds: bool + GICNrm_eff_holds: bool + gicnrm_split: bool # raw vs effective disagree (mortality-financed) + drift_eff: float + psi_min: float + p_coef_gt1: float + # regime + tail exponents (ergodic_coverage.md §3, §7) + regime: str # 'bounded' | 'powerlaw' + m_sup_cap: float # bounded regime only, else nan + alpha_survivor: float + alpha_counting: float + alpha_harmenberg: float + alpha_survivor_rawbeta: float # the no-mortality shadow root + alpha_survivor_reason: Optional[str] + alpha_counting_reason: Optional[str] + alpha_harmenberg_reason: Optional[str] + alpha_survivor_rawbeta_reason: Optional[str] + # lognormal closed-form COMPASS (a cross-check, not a pin; ergodic_coverage.md §3) + alpha_survivor_lognormal: float + alpha_counting_lognormal: float + alpha_harmenberg_lognormal: float + # pathology flags + survivor_nonergodic: bool + infinite_mean: bool # alpha_counting <= 1 + infinite_variance: bool # alpha_counting <= 2 + # provenance + ergodic_lib_version: str + notes: Tuple[str, ...] = () + + def to_dict(self): + return asdict(self) + + +def ergodic_grid_diagnostics(Rfree, PermGroFac, DiscFac, CRRA, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=None, IncShkDstn=None, + warn=True): + """Ex-ante patience screen for grid design, from primitives only. + + A thin adapter over the IMPORTED numeric core + (BST-Latest ``ergodic_coverage_lib``): it reads the model's OWN discretized + permanent-shock atoms and calls ``ergodic_regime_psi`` / ``kesten_lognormal_roots`` + / ``kesten_root_psi``; it re-implements none of that math. + + The *shape* of the ergodic distribution of the market-resources ratio ``m`` is + pinned down ex ante by patience primitives (a Kesten random-growth process), so + the tail exponent is available WITHOUT a solve; only the distribution's location + needs one. See ``ergodic_coverage.md`` (theory of record) — this docstring cites, + it does not restate the derivations. + + Two consumer rules honored here (ergodic_coverage.md §1, §8): + + * **Effective patience** — ``DiscFac`` is the RAW discount factor; mortality + enters via ``LivPrb`` (the imported core forms ``beta_eff = DiscFac*LivPrb`` + internally, the HARK ``DiscFacEff`` convention). Do NOT pre-multiply. + * **Own-atoms rule** — the screen runs on the model's own discretized ``psi`` + atoms (menu swaps move ``alpha_counting`` materially); the lognormal closed + forms are reported only as a compass. + + Parameters + ---------- + Rfree, PermGroFac, DiscFac, CRRA, LivPrb : float + Primitives (scalars or length-1 lists), exactly as + :func:`powerlaw_decay_params`. ``DiscFac`` RAW (see above). + PermShkDstn, TranShkDstn, IncShkDstn : optional + The permanent-shock atoms drive the screen; the transitory ``xi_max`` sets + the bounded-regime cap. Marginals ``(PermShkDstn, TranShkDstn)`` or the + joint ``IncShkDstn`` (marginalized), as :func:`powerlaw_decay_params`. + warn : bool, default True + Emit an advisory :class:`ErgodicCoverageWarning` only when the counting-tail + mean is infinite (``alpha_counting <= 1``). All flags are in the result. + + Returns + ------- + ErgodicGridDiagnostics + + Raises + ------ + ImportError + (at call time) if ``ergodic_coverage_lib`` is not importable — see + :func:`_get_ergodic_coverage_lib`. + """ + lib = _get_ergodic_coverage_lib() + + # --- permanent-shock atoms (own-atoms rule) + transitory xi_max + if IncShkDstn is not None: + if PermShkDstn is not None or TranShkDstn is not None: + raise ValueError("pass IncShkDstn OR (PermShkDstn, TranShkDstn), not both") + psi_j, th_j, p_j = _as_joint(IncShkDstn) + psi_a, psi_p = _marginal(psi_j, p_j) + th_a, _th_p = _marginal(th_j, p_j) + xi_max = float(np.max(th_a)) + else: + if PermShkDstn is None: + psi_a = np.array([_LD(1)]) + psi_p = np.array([_LD(1)]) + else: + psi_a, psi_p = _as_atoms_probs(PermShkDstn, "PermShkDstn") + if TranShkDstn is None: + xi_max = float("nan") + else: + th_a, _th_p = _as_atoms_probs(TranShkDstn, "TranShkDstn") + xi_max = float(np.max(th_a)) + + psf = np.asarray(psi_a, float) + ppf = np.asarray(psi_p, float) + + def _scalar(v): + if isinstance(v, (list, tuple, np.ndarray)): + return float(np.asarray(v).ravel()[0]) + return float(v) + + R = _scalar(Rfree) + G = _scalar(PermGroFac) + beta = _scalar(DiscFac) + rho = _scalar(CRRA) + liv = _scalar(LivPrb) + + # sigma^2 of ln psi from the atoms (for the lognormal COMPASS only) + E_log = float((ppf * np.log(psf)).sum()) if psf.size > 1 else 0.0 + sigma2 = float((ppf * np.log(psf) ** 2).sum() - E_log ** 2) if psf.size > 1 else 0.0 + + # --- imported numeric core (no math reimplemented here) + scr = lib.ergodic_regime_psi(R, G, beta, rho, psf, ppf, xi_max, LivPrb=liv) + lad = scr["ladder"] + logn = lib.kesten_lognormal_roots(lad["GPF_eff"], sigma2, LivPrb=liv) \ + if sigma2 > 0 else dict(alpha_surv=float("nan"), + alpha_count=float("nan"), alpha_harm=float("nan")) + raw_shadow, raw_reason = lib.kesten_root_psi( + lad["GPF_raw"], psf, ppf, kill=1.0, tilt=0.0) + + # --- package HARK-side (field names preserved from the imported contract) + notes = [] + if scr["gicnrm_split"]: + notes.append( + "raw and effective GIC-Nrm disagree: with mortality folded in " + "(DiscFacEff) the distribution is tamed, but the raw-beta ladder would " + "not tame it — tameness is mortality-financed (ergodic_coverage.md §1).") + if scr.get("survivor_nonergodic"): + notes.append( + "survivor measure is non-ergodic (nonnegative log-drift); the " + "agent-counting measure with death+reset is still stationary (§3).") + if scr.get("infinite_mean"): + notes.append("alpha_counting <= 1: the ratio distribution has INFINITE " + "MEAN — simulated means/top-shares will not converge (§7).") + elif scr.get("infinite_variance"): + notes.append("alpha_counting <= 2: INFINITE VARIANCE — simulated Lorenz/" + "top-share statistics will not converge (§7).") + notes.append("Kesten exponent is the m >> hNrm asymptote; where the ergodic " + "body sits below hNrm, measured Hill exponents read ABOVE alpha " + "(pre-asymptotic thinning, ergodic_coverage.md §5).") + + if warn and scr.get("infinite_mean"): + _warnings.warn( + "ergodic_grid_diagnostics: alpha_counting = %.4g <= 1 (infinite-mean " + "ratio distribution); the no-mortality shadow exponent is %.4g. " + "Coverage/top-share statistics will not converge (ergodic_coverage.md " + "§7)." % (scr.get("alpha_counting", float("nan")), raw_shadow), + ErgodicCoverageWarning, stacklevel=2) + + return ErgodicGridDiagnostics( + APF_raw=lad["APF_raw"], APF_eff=lad["APF_eff"], + GPF_raw=lad["GPF_raw"], GPF_eff=lad["GPF_eff"], + MPCmin_eff=lad["kap_min_eff"], E_inv_psi=lad["E_inv_psi"], + E_log_psi=lad["E_log_psi"], + GICNrm_raw=lad["gicnrm_raw"], GICNrm_eff=lad["gicnrm_eff"], + GICNrm_raw_holds=bool(lad["gicnrm_raw"] < 1.0), + GICNrm_eff_holds=bool(lad["gicnrm_eff"] < 1.0), + gicnrm_split=scr["gicnrm_split"], + drift_eff=lad["drift_eff"], psi_min=lad["psi_min"], + p_coef_gt1=lad["p_coef_gt1"], + regime=scr["regime"], m_sup_cap=scr["m_sup_cap"], + alpha_survivor=scr["alpha_surv"], + alpha_counting=scr["alpha_count"], + alpha_harmenberg=scr["alpha_harm"], + alpha_survivor_rawbeta=float(raw_shadow), + alpha_survivor_reason=scr.get("alpha_surv_reason"), + alpha_counting_reason=scr.get("alpha_count_reason"), + alpha_harmenberg_reason=scr.get("alpha_harm_reason"), + alpha_survivor_rawbeta_reason=raw_reason, + alpha_survivor_lognormal=float(logn["alpha_surv"]), + alpha_counting_lognormal=float(logn["alpha_count"]), + alpha_harmenberg_lognormal=float(logn["alpha_harm"]), + survivor_nonergodic=scr["survivor_nonergodic"], + infinite_mean=scr["infinite_mean"], + infinite_variance=scr["infinite_variance"], + ergodic_lib_version=getattr(lib, "__version__", "?"), + notes=tuple(notes), + ) + + +def ergodic_grid_diagnostics_from_agent(agent, t=0, warn=True): + """Ex-ante patience screen for an ``IndShockConsumerType``-family agent. + + Mirrors :func:`powerlaw_decay_params_from_agent`: reads ``Rfree[t]``, + ``PermGroFac[t]``, ``DiscFac`` (RAW), ``CRRA``, ``LivPrb[t]`` and the agent's + OWN ``PermShkDstn[t]`` / ``TranShkDstn[t]`` atoms (own-atoms rule; falls back to + the joint ``IncShkDstn[t]``). No solve, no RNG. + """ + kwargs = dict( + Rfree=_time_indexed(agent.Rfree, t), + PermGroFac=_time_indexed(agent.PermGroFac, t), + DiscFac=_time_indexed(agent.DiscFac, t), + CRRA=_time_indexed(agent.CRRA, t), + LivPrb=_time_indexed(agent.LivPrb, t), + warn=warn, + ) + perm = getattr(agent, "PermShkDstn", None) + tran = getattr(agent, "TranShkDstn", None) + if perm is not None and tran is not None: + return ergodic_grid_diagnostics( + PermShkDstn=perm[t], TranShkDstn=tran[t], **kwargs) + inc = getattr(agent, "IncShkDstn", None) + if inc is None: + raise ValueError("agent has neither (PermShkDstn, TranShkDstn) nor " + "IncShkDstn") + return ergodic_grid_diagnostics(IncShkDstn=inc[t], **kwargs) + + +@dataclass +class ErgodicGridReport: + """Result of :func:`ergodic_grid_report` — the postmortem coverage certificate + (HARK-native simulation; the Hill estimator is imported from + ``ergodic_coverage_lib``).""" + + mNrmStE: float + mNrmTrg: float + bottom_knot: float # first EGM m-gridpoint + top_knot: float # last EGM m-gridpoint + mass_below_bottom: float + mass_above_top: float + mass_above_thresholds: dict # {threshold: P(m > threshold)} + quantiles: dict # {0.5:..., ..., 0.9999:..., 'max':...} + hill: dict # {top_frac: Hill exponent} + alpha_counting_exante: float # predicted Kesten counting exponent + rel_gap_at_top: float # accuracy at the top knot (coverage vs accuracy) + coverage_framing: str # tails-on vs tails-off reading (§7) + n_draws: int + seed: int + notes: Tuple[str, ...] = () + + def to_dict(self): + return asdict(self) + + +def ergodic_grid_report(agent, t=0, AgentCount=10000, T_sim=1200, discard=800, + seed=0, thresholds=(20.0,), warn=True): + """Postmortem grid-coverage certificate for a SOLVED agent (HARK-native). + + The division of labor (owner ruling): the ex-ante numeric core lives in + BST-Latest; HARK owns the agent glue and this postmortem orchestration. Works on + a ``deepcopy`` of ``agent`` — it never mutates, re-grids, or re-solves the user's + agent — runs the agent's own fixed-seed simulation, and pairs grid COVERAGE + (simulated mass outside the grid) with grid ACCURACY (``rel_gap_at`` the top + knot). These are different questions at different scales + (ergodic_coverage.md §7). Advisory only; never re-grids. + + Parameters + ---------- + agent : IndShockConsumerType + A SOLVED infinite-horizon single-period agent (``cycles=0``). + t : int, default 0 + AgentCount, T_sim, discard, seed : int + Fixed-seed simulation controls; the last ``T_sim - discard`` periods are + pooled. Determinism is guaranteed by ``seed``. + thresholds : iterable of float + Extra ``m`` levels to report ``P(m > threshold)`` for (e.g. HARK's default + ``aXtraMax`` of 20). + warn : bool, default True + Emit an advisory :class:`ErgodicCoverageWarning` only on genuine pathology + (mass above the top knot > 1%, or an ex-ante ``alpha_counting <= 2``). + + Returns + ------- + ErgodicGridReport + """ + from copy import deepcopy + + lib = _get_ergodic_coverage_lib() # for the imported hill_exponent + a = deepcopy(agent) # never mutate the user's agent + + # stable points (conditions are raw-beta; the raw-vs-effective distinction is + # documented in ergodic_grid_diagnostics — do not refactor HARK's machinery) + if not getattr(a, "conditions", None): + a.check_conditions(verbose=False) + a.calc_stable_points(force=True) + sol = a.solution[t] + mNrmStE = float(getattr(sol, "mNrmStE", float("nan"))) + mNrmTrg = float(getattr(sol, "mNrmTrg", float("nan"))) + + # first/last EGM m-gridpoints (duck-typed through tail wrappers / lower-envelope) + def _knots(cf): + f = cf + for _ in range(8): + if hasattr(f, "x_list"): + xl = np.asarray(f.x_list, float) + return float(xl[1]), float(xl[-1]) + if hasattr(f, "functions"): + f = f.functions[0] + elif hasattr(f, "interp"): + f = f.interp + else: + break + return float("nan"), float("nan") + + bottom_knot, top_knot = _knots(sol.cFunc) + + # fixed-seed simulation of the SOLVED policy + a.track_vars = ["mNrm"] + a.T_sim = int(T_sim) + a.AgentCount = int(AgentCount) + a.seed = int(seed) + with _warnings.catch_warnings(): + _warnings.simplefilter("ignore") + a.initialize_sim() + a.simulate() + pooled = np.asarray(a.history["mNrm"][int(discard):], float).ravel() + pooled = pooled[np.isfinite(pooled)] + + quantiles = {q: float(np.quantile(pooled, q)) + for q in (0.5, 0.9, 0.99, 0.999, 0.9999)} + quantiles["max"] = float(pooled.max()) + hill = {f: lib.hill_exponent(pooled, top_frac=f) for f in (0.01, 0.005, 0.001)} + mass_above_top = float(np.mean(pooled > top_knot)) if np.isfinite(top_knot) else float("nan") + mass_below_bottom = float(np.mean(pooled < bottom_knot)) if np.isfinite(bottom_knot) else float("nan") + mass_thr = {float(x): float(np.mean(pooled > float(x))) for x in thresholds} + + # ex-ante prediction + accuracy at the top knot (coverage vs accuracy pairing) + diag = ergodic_grid_diagnostics_from_agent(agent, t=t, warn=False) + hh = powerlaw_decay_params_from_agent(agent, warn=False).h + if np.isfinite(top_knot): + rg = float(np.atleast_1d( + rel_gap_at(sol.cFunc, np.array([top_knot]), diag.MPCmin_eff, hh))[0]) + else: + rg = float("nan") + + tails_on = getattr(agent, "decay_extrap_form", None) is not None + framing = ("tails ON: mass above the top knot is the share of the population " + "SERVED BY the power-law extrapolation (accuracy there set by " + "rel_gap_at)" if tails_on else + "tails OFF: mass above the top knot rides HARK's default linear/" + "exponential extension (rel_gap_at measures the handoff error)") + + notes = [framing, + "coverage (mass above the grid) and accuracy (rel_gap at the knot) are " + "different questions at different scales (ergodic_coverage.md §7).", + "Hill exponents read ABOVE the ex-ante alpha_counting when the body " + "sits below hNrm (pre-asymptotic thinning, §5); Hill BELOW the root " + "is the anomaly worth investigating."] + + if warn and (mass_above_top > 0.01 or (np.isfinite(diag.alpha_counting) + and diag.alpha_counting <= 2.0)): + _warnings.warn( + "ergodic_grid_report: coverage looks pathological — mass above the top " + "knot = %.3g%%, ex-ante alpha_counting = %.4g. This is advisory: solve " + "on a wider aXtraMax if you need that mass covered; HARK does not " + "re-grid for you (ergodic_coverage.md §7)." + % (100.0 * mass_above_top, diag.alpha_counting), + ErgodicCoverageWarning, stacklevel=2) + + return ErgodicGridReport( + mNrmStE=mNrmStE, mNrmTrg=mNrmTrg, + bottom_knot=bottom_knot, top_knot=top_knot, + mass_below_bottom=mass_below_bottom, mass_above_top=mass_above_top, + mass_above_thresholds=mass_thr, quantiles=quantiles, hill=hill, + alpha_counting_exante=diag.alpha_counting, rel_gap_at_top=rg, + coverage_framing=framing, n_draws=int(pooled.size), seed=int(seed), + notes=tuple(notes), + ) diff --git a/HARK/interpolation.py b/HARK/interpolation.py index 8ad5be3ba..683fd1b9b 100644 --- a/HARK/interpolation.py +++ b/HARK/interpolation.py @@ -884,6 +884,106 @@ class LinearInterp(HARKinterpolator1D): default behavior of np.maximum(np.searchsorted(self.x_list[:-1], x), 1). WARNING: User is responsible for verifying that their custom indexer is actually correct versus default behavior. + decay_extrap_form : str + Functional form of the decay toward the limiting linear function when + ``intercept_limit`` and ``slope_limit`` are provided (irrelevant + otherwise). ``'exp'`` (default, the long-standing behavior): the gap + below the limiting line decays exponentially in ``x - x_list[-1]``. + ``'powerlaw'``: the gap decays as a power law, + ``gap(x) = A*((x + h)/(x_list[-1] + h))**(-Q)`` with pivot + ``h = intercept_limit/slope_limit`` and ``Q = B*(x_list[-1] + h)``. + Both forms match the level and the slope of the interpolant at the top + gridpoint, so neither needs parameters beyond the limiting line; over a + short span above the grid they coincide (the exponential is the + local linearization of the power law), but the power law is the + asymptotically correct tail for buffer-stock consumption functions, + whose gap below the perfect-foresight asymptote ``MPCmin*(x + hNrm)`` + decays polynomially, not exponentially (``h`` is then human wealth). + ``'powerlaw'`` requires ``slope_limit > 0``, a top knot strictly below + the limiting line with slope strictly above ``slope_limit``, and + ``x_list[-1] + h > 0``; if violated it warns and disables decay + extrapolation (``decay_extrap == False``) rather than risk a divergent + tail. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The power-law gap tail C*(x + h)**(-q) is the theorem's extrapolation + # form of record for buffer-stock consumption functions; the + # exponential form is not merely inaccurate but impossible as an + # asymptotic form (Prop A0) — 'exp' stays only as the legacy default. + decay_extrap_Q : float or None (default), keyword-only + ``None``: byte-for-byte the behavior described above for both forms + (the power-law exponent is FITTED from the top two knots as + ``Q = B*(x_list[-1] + h)``). A positive float (requires + ``decay_extrap_form='powerlaw'``, else ``ValueError``): use this + EXPLICIT decay exponent instead of the fitted one. The gap amplitude + ``A`` stays the level gap at the top knot, so the tail is + level-matched (continuous) by construction, but its slope no longer + matches the interpolant's top-segment slope: the derivative just above + the top knot is ``slope_limit + Q*A/pivot``, i.e. a C1 kink of size + ``(Q_fit - Q)*A/pivot`` relative to the fitted tangent (tiny in + absolute terms when ``A`` is small and the pivot large, but + sign-indefinite). Because the exponent no longer needs to be inferred + from the top-segment slope, the slope-tangency part of the powerlaw + validity guard is relaxed: only ``slope_limit > 0``, a top knot + strictly below the limiting line, and a positive pivot are required — + in particular a top slope at or below ``slope_limit`` (fitted + ``B <= 0``, where the fitted form must disable decay) still attaches + an explicit-Q tail. ``self.decay_extrap_Q_source`` records + ``'explicit'`` vs ``'fitted'`` for introspection. The C1-kink + description above applies to the ONE-TERM variant + (``decay_extrap_terms=1``); the default two-term attachment is C1. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The gap extrapolant g ~ C*(x + h)**(-q) with q = min(1, q*) is the + # asymptotically correct form for buffer-stock consumption functions; + # this keyword is the hook that lets callers pin the exponent to the + # theory value (or any explicit value) instead of the 2-knot fit. + decay_extrap_terms : int, keyword-only (default 2) + Consulted only with ``decay_extrap_Q``. ``2`` (default): the C1 + TWO-TERM attachment ``gap = A*z**(-Q) + A2*z**(-(Q+1))`` with + ``z = (x+h)/(x_top+h)``, level- AND slope-matched at the top knot + (``A2 = gap*(Q_fit - Q)``, ``A = gap - A2``, where ``Q_fit`` is the + 2-knot fitted exponent); it carries the theory exponent as the + leading term, collapses EXACTLY to the one-term tail when + ``Q_fit == Q``, and preserves below-the-line, ``c' > slope_limit``, + and the leading exponent whenever it attaches. Guard: ``Q_fit >= + Q + 1`` (top segment locally steeper than theory+1 — a coarse or + non-converged grid top) warns and falls back to one term. ``1``: the + level-matched one-term tail with the documented C1 kink. + + WHY the two-term default: it guards against Jacobian problems in + SSJ-type (sequence-space Jacobian) approaches. Policy derivatives + are primitive inputs to SSJ fake-news/Jacobian construction and to + automatic or numerical differentiation through the solution; a C1 + kink at the attachment point makes those derivatives discontinuous + for queries crossing it, producing noisy or discontinuous Jacobian + rows for high-wealth states. The second exponent is ``Q + 1`` (NOT + the theory-subleading pair, whose spacing ``|q* - 1|`` vanishes at + the near-resonance calibrations and blows the amplitudes up): it is + an attachment (boundary-layer) term absorbing exactly the one-term + kink, not an asymptotic claim. + + # THEOREM-REF[BufferStockTheory-Latest @ 3f4b021e :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: F11 — The C1 two-term attachment :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # F11: closed-form amplitudes B = G*(Q_fit - Q), A = G*(1+Q-Q_fit) + # from the level+slope matching conditions; conditioning argument + # for Q+1 over the theory-subleading pair; property proofs + # (below-line, MPC floor, concavity condition, guard + fallback). + q_diagnostics : tuple or None (default), keyword-only + Opaque measurement-diagnostics rider for explicit-Q callers, stashed + verbatim as ``self.local_q_diag`` and never consulted by evaluation. + Convention (pinned to the downstream reference implementation this + keyword mirrors -- HAFiscal's ``PowerLawDecayLinearInterp``, whose + attach sites pass the local two-secant measurement): the tuple + ``(Q1, Q2, drift)`` of the two log-log secant exponents of the gap + in the shifted abscissa ``x + h`` over the top three knots, plus the + drift advisory ``Q2 - Q1``; ``decay_extrap_Q`` is then ``Q2`` (the + most local secant). Post-solve tools read the measurement off + converged slices via ``getattr(slice, "local_q_diag", None)``. + Present (default ``None``) whether or not decay engages, including + on the guard-disable path -- diagnostics must survive exactly the + cases one wants to inspect. Both spellings (keyword + ``q_diagnostics``, attribute ``local_q_diag``) are API: they are + the drop-in contract with the reference implementation. """ distance_criteria = ["x_list", "y_list"] @@ -897,6 +997,11 @@ def __init__( lower_extrap=False, pre_compute=False, indexer=None, + decay_extrap_form="exp", + *, + decay_extrap_Q=None, + decay_extrap_terms=2, + q_diagnostics=None, ): # Make the basic linear spline interpolation self.x_list = _coerce_1d_grid(x_list) @@ -907,19 +1012,59 @@ def __init__( self.indexer = indexer # Make a decay extrapolation + if decay_extrap_form not in ("exp", "powerlaw"): + raise ValueError( + "decay_extrap_form must be 'exp' or 'powerlaw', got " + + repr(decay_extrap_form) + ) + self.decay_extrap_form = decay_extrap_form + # Measurement-diagnostics rider (reference-implementation contract: + # both the keyword and the attribute spelling are API; see docstring). + # Stashed before any decay validation so it survives guard-disable. + self.local_q_diag = q_diagnostics + if decay_extrap_Q is not None: + if decay_extrap_form != "powerlaw": + raise ValueError( + "decay_extrap_Q requires decay_extrap_form='powerlaw'" + ) + if intercept_limit is None or slope_limit is None: + raise ValueError( + "decay_extrap_Q requires intercept_limit and slope_limit" + ) + decay_extrap_Q = float(decay_extrap_Q) + if not np.isfinite(decay_extrap_Q) or decay_extrap_Q <= 0.0: + raise ValueError( + "decay_extrap_Q must be a positive finite float, got " + + repr(decay_extrap_Q) + ) + if isinstance(decay_extrap_terms, bool) or decay_extrap_terms not in (1, 2): + raise ValueError( + "decay_extrap_terms must be 1 or 2, got " + + repr(decay_extrap_terms) + ) if intercept_limit is not None and slope_limit is not None: slope_at_top = (y_list[-1] - y_list[-2]) / (x_list[-1] - x_list[-2]) level_diff = intercept_limit + slope_limit * x_list[-1] - y_list[-1] slope_diff = slope_limit - slope_at_top + if decay_extrap_Q is not None: + # Explicit-exponent power law: level-matched at the top knot, + # exponent supplied by the caller (relaxed guard; see docstring) + self.intercept_limit = intercept_limit + self.slope_limit = slope_limit + self._init_explicit_Q_decay( + level_diff, slope_diff, decay_extrap_Q, decay_extrap_terms + ) # If the model that can handle uncertainty has been calibrated with # with uncertainty set to zero, the 'extrapolation' will blow up # Guard against that and nearby problems by testing slope equality - if not np.isclose(slope_limit, slope_at_top, atol=1e-15): + elif not np.isclose(slope_limit, slope_at_top, atol=1e-15): self.decay_extrap_A = level_diff self.decay_extrap_B = -slope_diff / level_diff self.intercept_limit = intercept_limit self.slope_limit = slope_limit self.decay_extrap = True + if decay_extrap_form == "powerlaw": + self._init_powerlaw_decay(level_diff, slope_diff) else: self.decay_extrap = False else: @@ -932,6 +1077,132 @@ def __init__( ) self.intercepts = self.y_list[:-1] - self.slopes * self.x_list[:-1] + def _init_powerlaw_decay(self, level_diff, slope_diff): + """Set up the power-law decay tail, or fall back to no decay (with a + warning) if the required configuration does not hold. + + The gap below the limiting line is + ``gap(x) = A * ((x + h)/(x_top + h))**(-Q)`` with pivot + ``h = intercept_limit/slope_limit`` and ``Q = B*(x_top + h)``, which + matches the level AND the slope of the interpolant at the top + gridpoint -- the same two conditions the exponential form matches, so + no parameters beyond (``intercept_limit``, ``slope_limit``) are + needed. A valid power-law tail requires the top knot strictly below + the limiting line (``level_diff > 0``) and approaching it (top slope + strictly above ``slope_limit``, i.e. ``B > 0``), plus + ``slope_limit > 0`` and a positive pivot ``x_top + h``. For a + converged consumption function these hold by Carroll-Kimball (1996) + concavity, so a violation signals bad inputs; decay is then disabled + outright rather than risk a divergent tail. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §2. Model, conditions, and the imported foundations :: Carroll–Kimball 1996 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Imported foundations L0–L2′: a converged buffer-stock consumption + # function is strictly increasing and strictly concave (Carroll–Kimball + # 1996) and approaches its PF asymptote from below with slope falling + # to the limiting MPC — hence level_diff > 0 and B > 0 at a valid knot. + """ + x_top = self.x_list[-1] + ok = self.slope_limit > 0.0 and level_diff > 0.0 and self.decay_extrap_B > 0.0 + if ok: + pivot = x_top + self.intercept_limit / self.slope_limit + ok = pivot > 0.0 + if not ok: + warnings.warn( + "LinearInterp(decay_extrap_form='powerlaw'): the top knot is " + "not strictly below the limiting line with slope strictly " + f"above slope_limit (level_diff={level_diff:.6g}, " + f"slope_diff={slope_diff:.6g}, slope_limit={self.slope_limit:.6g}); " + "disabling decay extrapolation for this interpolant." + ) + self.decay_extrap = False + return + self.decay_extrap_pivot = pivot + self.decay_extrap_Q = self.decay_extrap_B * pivot + self.decay_extrap_Q_source = "fitted" + + def _init_explicit_Q_decay(self, level_diff, slope_diff, Q, terms=2): + """Set up the power-law decay tail with an EXPLICIT exponent ``Q``, or + fall back to no decay (with a warning) if the relaxed guard fails. + + With ``terms=2`` (the default; see ``decay_extrap_terms``) the gap is + the C1 two-term attachment + ``gap(x) = A*z**(-Q) + A2*z**(-(Q+1))``, ``z = (x+h)/(x_top+h)``, + level- AND slope-matched at the top knot: + ``A2 = level_diff*(Q_fit - Q)``, ``A = level_diff - A2`` (F11 closed + forms; collapses to one term exactly when ``Q_fit == Q``); when the + fitted rate is theory-infeasibly steep (``Q_fit >= Q + 1``, where the + leading amplitude would turn negative) it warns and falls back to one + term. With ``terms=1`` the gap is the one-term + ``A * ((x + h)/(x_top + h))**(-Q)`` with ``A = level_diff`` + (level-matched only; C1 kink ``(Q_fit - Q)*A/pivot`` at the knot). + Because ``Q`` is not inferred from the top-segment + slope, only ``slope_limit > 0``, ``level_diff > 0`` (top knot strictly + below the limiting line), and a positive pivot are required — NOT + ``decay_extrap_B > 0``: a top slope at or below ``slope_limit`` (where + the fitted form must disable decay) is exactly the rescue case an + explicit exponent exists to serve (the two-term rescue extends the + body smoothly; the one-term rescue kinks upward at the knot). + """ + x_top = self.x_list[-1] + ok = self.slope_limit > 0.0 and level_diff > 0.0 + pivot = None + if ok: + pivot = x_top + self.intercept_limit / self.slope_limit + ok = pivot > 0.0 + if not ok: + warnings.warn( + "LinearInterp(decay_extrap_Q=...): explicit-exponent decay " + "requires slope_limit > 0, a top knot strictly below the " + f"limiting line, and a positive pivot (level_diff=" + f"{level_diff:.6g}, slope_limit={self.slope_limit:.6g}); " + "disabling decay extrapolation for this interpolant." + ) + self.decay_extrap = False + return + # fitted-rate diagnostic; also the slope input of the two-term form + self.decay_extrap_B = -slope_diff / level_diff + self.decay_extrap_pivot = pivot + self.decay_extrap_Q = Q + self.decay_extrap_Q_source = "explicit" + Q_fit = self.decay_extrap_B * pivot + A2 = level_diff * (Q_fit - Q) if np.isfinite(Q_fit) else np.nan + if terms == 2 and np.isfinite(Q_fit) and Q_fit < Q + 1.0 and A2 != 0.0: + # THEOREM-REF[BufferStockTheory-Latest @ 3f4b021e :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: F11 — The C1 two-term attachment :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # Level+slope matching with the theory exponent leading gives + # A2 = G*(Q_fit - Q), A = G - A2; the second exponent Q+1 keeps + # the system conditioned at near-resonance calibrations where + # the theory-subleading pair collides. An EXACT collapse + # (Q == Q_fit, A2 == 0) stores the one-term representation + # below instead, so it is byte-identical to terms=1 including + # derivatives; a non-finite Q_fit (degenerate top segment, + # infinite pivot) falls back rather than attach a NaN tail. + self.decay_extrap_A2 = A2 + self.decay_extrap_A = level_diff - self.decay_extrap_A2 + self.decay_extrap_terms = 2 + else: + if terms == 2 and not (np.isfinite(Q_fit) and Q_fit < Q + 1.0): + warnings.warn( + "LinearInterp(decay_extrap_Q=..., decay_extrap_terms=2): " + f"the fitted rate Q_fit={Q_fit:.4g} is not finite and " + f"strictly below Q+1={Q + 1.0:.4g} (at or above it, the " + "two-term leading amplitude would be non-positive; " + "non-finite signals a degenerate top segment); falling " + "back to the one-term level-matched tail (C1 kink at " + "the knot). A steep fitted rate at a human-wealth-" + "dominated grid top is usually coordinate " + "amplification rather than anomalous decay (the two are " + "separated by the diagnosis where available): remedies " + "are (a) extend the grid top " + f"toward Q*hNrm ~= {Q * (pivot - x_top):.4g} (the " + "human-wealth-scale rule), or (b) wrap the body in " + "DecayTailInterp(decay_extrap_form='moderation_tail') " + "(C1 at any cut, no guard)." + ) + self.decay_extrap_A = level_diff + self.decay_extrap_A2 = 0.0 + self.decay_extrap_terms = 1 + self.decay_extrap = True + def _segment_index(self, x): """Return the bracketing right-endpoint index for each query in ``x``.""" if self.indexer is None: @@ -978,14 +1249,53 @@ def _apply_lower_bound(self, x, y, dydx): def _apply_upper_decay(self, x, y, dydx): """In-place: replace queries above ``x_list[-1]`` with the limiting linear - plus exponential-decay envelope. ``y`` and ``dydx`` may each be ``None`` - to skip; no-op when ``self.decay_extrap`` is False.""" + function minus a decaying gap (exponential or power-law, per + ``decay_extrap_form``). ``y`` and ``dydx`` may each be ``None`` to + skip; no-op when ``self.decay_extrap`` is False.""" if not self.decay_extrap or (y is None and dydx is None): return above = x > self.x_list[-1] if not np.any(above): return x_temp = x[above] - self.x_list[-1] + if getattr(self, "decay_extrap_form", "exp") == "powerlaw": + if getattr(self, "decay_extrap_terms", 1) == 2: + # C1 two-term attachment (F11): gap = A*z**(-Q) + A2*z**(-(Q+1)) + # in z = (x+h)/(x_top+h); level- and slope-matched at the knot + # with the theory exponent leading. Same stable exp/log1p + # evaluation; both terms underflow to the line at depth. + lw = np.log1p(x_temp / self.decay_extrap_pivot) + w1 = np.exp(-self.decay_extrap_Q * lw) + w2 = np.exp(-(self.decay_extrap_Q + 1.0) * lw) + decay = self.decay_extrap_A * w1 + self.decay_extrap_A2 * w2 + if y is not None: + y[above] = ( + self.intercept_limit + self.slope_limit * x[above] - decay + ) + if dydx is not None: + # d(-gap)/dx = +(Q*A*z**(-Q) + (Q+1)*A2*z**(-(Q+1)))/(x+h) + dydx[above] = self.slope_limit + ( + self.decay_extrap_Q * self.decay_extrap_A * w1 + + (self.decay_extrap_Q + 1.0) * self.decay_extrap_A2 * w2 + ) / (x_temp + self.decay_extrap_pivot) + return + # gap = A * ((x + h)/(x_top + h))**(-Q), computed via exp/log1p for + # numerical stability. For x_temp << x_top + h it reduces to + # A*exp(-B*x_temp): the exponential form is the local linearization + # of this one, which is why fits over a short span above the grid + # cannot tell them apart while the tails differ materially. + decay = self.decay_extrap_A * np.exp( + -self.decay_extrap_Q * np.log1p(x_temp / self.decay_extrap_pivot) + ) + if y is not None: + y[above] = self.intercept_limit + self.slope_limit * x[above] - decay + if dydx is not None: + # d(-gap)/dx = +(Q/(x + h))*gap, with x + h = x_temp + pivot + dydx[above] = ( + self.slope_limit + + self.decay_extrap_Q / (x_temp + self.decay_extrap_pivot) * decay + ) + return decay = self.decay_extrap_A * np.exp(-self.decay_extrap_B * x_temp) if y is not None: y[above] = self.intercept_limit + self.slope_limit * x[above] - decay @@ -1045,6 +1355,1007 @@ def _evalAndDer(self, x): return y, dydx +class DecayTailInterp(HARKinterpolator1D): + """ + A composable decay-tail wrapper over ANY 1D interpolant. At and below a + handoff point ``x_cut`` every query is delegated, unchanged, to the wrapped + interpolant (its own lower extrapolation and NaN semantics pass through); + above ``x_cut`` the function is the limiting linear function + ``intercept_limit + slope_limit*x`` minus a decaying gap -- the same tail + family ``LinearInterp`` builds in (exponential or power-law, fitted or + explicit exponent), with the same validity guards and the same numerically + stable evaluation, but sourced from the wrapped function's level (and, for + the fitted forms, slope) at ``x_cut`` instead of from the top two knots of + a grid. + + This decouples the tail LAW from the in-grid REPRESENTATION. Previously + the decay machinery existed only baked into ``LinearInterp`` + (``CubicInterp`` carries only the legacy exponential form, with no + power-law option and no explicit-exponent hook), so the asymptotically + correct power-law tail could not be attached to a cubic body, an + econforge interpolant, or a fitted functional form. With this wrapper the + body is swappable: anything callable on numpy arrays composes with the + same tail. Compare :class:`HARK.econforgeinterp.DecayInterp`, which wraps + N-dimensional econforge interpolants with ad-hoc decay-weight schemes + toward a general limit function but provides no derivatives and not the + power-law gap law; and ``ConsAggShockModel.make_cFunc_slice``, the policy + layer that chooses WHICH tail to attach to consumption slices -- this + class is the mechanism such policy layers can target. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The power-law gap tail C*(x + h)**(-q) is the theorem's extrapolation + # form of record for buffer-stock consumption functions, and it is a + # property of the FUNCTION being approximated, not of the interpolation + # scheme used inside the grid -- which is why the tail is factored out + # here as a wrapper composable over any in-grid representation. + + Parameters + ---------- + interp : callable + The wrapped interpolant: any object mapping a numpy array of query + points to a numpy array of values (all HARK 1D interpolants qualify). + A ``derivative`` method is required by the FITTED tail forms + (``decay_extrap_Q=None``) and by the DEFAULT two-term explicit mode + (``decay_extrap_terms=2``), both of which read the slope at + ``x_cut``; the one-term explicit mode (``decay_extrap_terms=1``) + works on any bare callable. + intercept_limit : float + Intercept of the limiting linear function (required). + slope_limit : float + Slope of the limiting linear function (required). + x_cut : float or None (default) + The handoff point: queries strictly above it get the decay tail. + Defaults to ``interp.x_list[-1]`` when the wrapped interpolant exposes + a grid; otherwise it must be supplied. ``x_cut`` need not be a knot: + a cut above the body's grid composes the tail with the body's own + extrapolation (the level is read wherever the cut is), and a cut + below the body's top TRUNCATES the body there and replaces the rest + with the tail law -- useful for stopping a solved function at a + certified point without rebuilding it. + decay_extrap_form : str + ``'powerlaw'`` (default): the gap decays as + ``gap(x) = A*((x + h)/(x_cut + h))**(-Q)`` with pivot + ``h = intercept_limit/slope_limit``; the asymptotically correct tail + for buffer-stock consumption functions. ``'exp'``: the legacy + exponential ``gap(x) = A*exp(-B*(x - x_cut))``. The exponential form + is retained for parity with ``LinearInterp``'s long-standing default + and is DEPRECATED here (selecting it warns): it is not merely less + accurate but impossible as an asymptotic form for the consumption + gap, so new code should have no reason to choose it. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/statement.md :: Proposition A0 (no exponential decay — GIC-free) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Prop A0: the true gap below the perfect-foresight asymptote can + # never decay faster than 1/x, so any exponential tail understates + # it asymptotically -- the reason 'exp' is deprecated at birth in + # this new API while remaining LinearInterp's untouched legacy + # default. + + ``'moderation_tail'``: tail-only use of the Method-of-Moderation + COORDINATES. The solved body is untouched, and this is NOT the full + Method of Moderation solution representation (see the + MethodOfModeration paper) -- it borrows MoM's coordinates for the + extrapolation region only. With ``mEx = x - x_min`` and + ``hEx = intercept_limit/slope_limit + x_min``, the gap is expressed + as ``omega = gap/(slope_limit*hEx)`` in (0, 1) -- the position + between the limiting ("optimist") line and the gap ceiling anchored + at ``x_min`` (the "pessimist" line) -- and, through + ``chi = log((1 - omega)/omega)`` and ``mu = log(mEx)``, the tail is + + ``chi(mu) = chi_cut + Q*u + (chip_cut - Q)*(1 - exp(-u))``, + ``u = mu - mu_cut >= 0``. + + The decay law is the POWER LAW ``gap ~ mEx**(-Q)``, asymptotically + the same law as ``'powerlaw'``; the logistic link is only the + coordinate system carrying it (this is NOT logistic or exponential + decay of the gap). Properties, each unit-tested: level- AND + slope-matched (C1) at ANY cut with NO guard -- the bounded + ``exp(-u)`` correction absorbs an arbitrarily steep local slope, + exactly the ``Q_fit >= Q + 1`` region where the two-term power-law + attachment must fall back to a kinked one-term tail; the gap stays + strictly inside ``(0, slope_limit*hEx)`` for all finite queries + (bounds by construction); and it collapses exactly to the + pinned-slope line when ``chip_cut == Q``. The derivative floor + ``f' > slope_limit`` holds wherever the gap is locally shrinking + (asymptotically always); at a cut where the body's gap is WIDENING + (``chip_cut < 0``, a configuration the fitted forms must refuse), + C1 fidelity necessarily continues the widening before the tail + bends toward the line -- the bounds still hold throughout. Requires + ``decay_extrap_Q`` (there is NO fitted mode: fitting the asymptotic + slope from the cut is definitionally the tangent extrapolation this + form exists to correct) and ``x_min``, and always reads the body's + derivative at the cut. Applicability violations RAISE (they signal + inconsistent inputs) instead of warn-and-disable: the body's level + at the cut must lie strictly between the pessimist and optimist + lines. + decay_extrap_Q : float or None (default), keyword-only + ``None``: the tail is FITTED -- level- and slope-matched at ``x_cut`` + exactly as ``LinearInterp`` fits from its top two knots (the wrapped + interpolant's ``derivative(x_cut)`` supplies the slope). A positive + float (requires ``decay_extrap_form='powerlaw'``): use this EXPLICIT + decay exponent; the tail is level-matched (continuous) by + construction, and under the default ``decay_extrap_terms=2`` it is + slope-matched (C1) as well; only the one-term variant + (``decay_extrap_terms=1``) has the C1 kink whose size the + ``LinearInterp.decay_extrap_Q`` documentation derives. + The theory exponent for buffer-stock consumption functions is + ``min(1, q*)`` from :mod:`HARK.ConsumptionSaving.pf_decay`. + decay_extrap_terms : int, keyword-only (default 2) + Consulted only with ``decay_extrap_Q``. ``2`` (default): the C1 + TWO-TERM attachment ``gap = A*z**(-Q) + A2*z**(-(Q+1))``, level- AND + slope-matched at ``x_cut`` with the theory exponent leading (F11 + closed forms ``A2 = gap*(Q_fit - Q)``, ``A = gap - A2``; collapses + exactly to one term when the local fitted rate equals ``Q``; warns + and falls back to one term when ``Q_fit >= Q + 1``). ``1``: the + level-matched one-term tail (C1 kink; the only explicit mode + available to derivative-less bodies). + + WHY the two-term default: it guards against Jacobian problems in + SSJ-type (sequence-space Jacobian) approaches -- policy derivatives + are primitive inputs to SSJ Jacobian/fake-news construction and to + differentiation through the solution, and a C1 kink at the + attachment point makes them discontinuous for queries crossing the + cut. The second exponent is ``Q + 1`` (an attachment term absorbing + exactly the one-term kink), NOT the theory-subleading pair, whose + spacing ``|q* - 1|`` vanishes at near-resonance calibrations. + + # THEOREM-REF[BufferStockTheory-Latest @ 3f4b021e :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: F11 — The C1 two-term attachment :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # F11: derivation of the closed-form amplitudes, the conditioning + # argument for Q+1, and the property proofs (below-line, MPC + # floor, concavity condition, guard + one-term fallback). + x_min : float or None (default), keyword-only + The lower support point of the moderation coordinates (for + consumption functions: ``mNrmMin``, the "pessimist" bound anchoring + the gap ceiling; often <= 0 under a natural borrowing constraint). + REQUIRED by ``decay_extrap_form='moderation_tail'`` (which needs + ``mEx = x - x_min > 0`` at the cut and + ``hEx = intercept_limit/slope_limit + x_min > 0``). OPTIONAL with + ``'powerlaw'``: supplying it enriches the two-term guard-trip + warning with the exact steepness diagnosis (``s_mu``, the + coordinate amplification factor ``1 + hEx/mEx``, and the guard-safe + grid boundary ``mEx > Q*hEx``). Rejected with ``'exp'``. + + Notes + ----- + LEVEL CONTINUITY AT ``x_cut`` IS AN INVARIANT OF THIS CLASS: every tail + it can attach is level-matched to the wrapped function at the cut (the + amplitude is always the level gap read there), so the composed function + never jumps. There is deliberately no amplitude-override hook: imposing + an external amplitude (e.g. a closed-form boundary value) at a + pre-asymptotic cut forces a level discontinuity, which is never + acceptable. Under the default two-term attachment the composed function + is C1 at the cut as well; the one discontinuity the class can exhibit + is the documented C1 (derivative-only) kink of the one-term + explicit-exponent mode (``decay_extrap_terms=1``). + + Validity guards mirror ``LinearInterp``: the fitted forms require the + level at ``x_cut`` strictly below the limiting line, approaching it + (slope above ``slope_limit``), ``slope_limit > 0``, and a positive pivot; + the explicit-exponent form relaxes the slope condition (its rescue case). + On guard failure the wrapper warns and DISABLES the tail, and queries + above ``x_cut`` simply delegate to the wrapped interpolant -- the + composable analog of ``LinearInterp`` falling back to its naive top- + segment extrapolation. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §2. Model, conditions, and the imported foundations :: Carroll–Kimball 1996 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Imported foundations L0-L2': a converged buffer-stock consumption + # function is strictly increasing and strictly concave (Carroll-Kimball + # 1996) and approaches its PF asymptote from below with slope falling + # to the limiting MPC -- the configuration the guards enforce at x_cut; + # a violation signals bad inputs, so the tail is refused rather than + # risk a divergent extrapolation. + + ``distance_criteria`` recurses into the wrapped interpolant only: with + the limiting line and tail policy fixed, the tail parameters are + deterministic functions of the body, so successive-iterate distances + through the body control the total distance in solver convergence checks. + + Fine print (adversarially established): the tail parameters are a + CONSTRUCTION-TIME snapshot -- mutating the wrapped interpolant afterward + moves the body but not the tail. Limiting-line parameters are coerced to + float64, so byte-parity with ``LinearInterp`` holds for float64/Python- + float inputs (exotic dtypes like float32 round differently in + LinearInterp's raw-dtype arithmetic). Parity also presumes a + non-degenerate top knot: on a grid whose last two x-values coincide, + LinearInterp's fitted tail is internally inconsistent (it level-matches a + knot value its own evaluator does not return), while this wrapper + level-matches what the body actually evaluates to at ``x_cut``. + """ + + distance_criteria = ["interp"] + + def __init__( + self, + interp, + intercept_limit, + slope_limit, + x_cut=None, + decay_extrap_form="powerlaw", + *, + decay_extrap_Q=None, + decay_extrap_terms=2, + x_min=None, + ): + self.interp = interp + if decay_extrap_form not in ("exp", "powerlaw", "moderation_tail"): + raise ValueError( + "decay_extrap_form must be 'exp', 'powerlaw', or " + "'moderation_tail', got " + repr(decay_extrap_form) + ) + if isinstance(decay_extrap_terms, bool) or decay_extrap_terms not in (1, 2): + raise ValueError( + "decay_extrap_terms must be 1 or 2, got " + + repr(decay_extrap_terms) + ) + self.decay_extrap_form = decay_extrap_form + if intercept_limit is None or slope_limit is None: + raise ValueError( + "DecayTailInterp requires intercept_limit and slope_limit " + "(the limiting linear function is what the tail decays toward)" + ) + if x_min is not None: + if decay_extrap_form == "exp": + raise ValueError( + "x_min parameterizes the moderation coordinates (and the " + "power-law guard diagnosis); it is meaningless with " + "decay_extrap_form='exp'" + ) + x_min = float(x_min) + if not np.isfinite(x_min): + raise ValueError("x_min must be finite, got " + repr(x_min)) + self.decay_x_min = x_min + if decay_extrap_Q is not None: + if decay_extrap_form not in ("powerlaw", "moderation_tail"): + raise ValueError( + "decay_extrap_Q requires decay_extrap_form='powerlaw' " + "or 'moderation_tail'" + ) + decay_extrap_Q = float(decay_extrap_Q) + if not np.isfinite(decay_extrap_Q) or decay_extrap_Q <= 0.0: + raise ValueError( + "decay_extrap_Q must be a positive finite float, got " + + repr(decay_extrap_Q) + ) + if decay_extrap_form == "moderation_tail": + if decay_extrap_Q is None: + raise ValueError( + "decay_extrap_form='moderation_tail' requires " + "decay_extrap_Q (the explicit asymptotic exponent; the " + "theory value is min(1, q*) from " + "HARK.ConsumptionSaving.pf_decay) -- there is no fitted " + "mode: fitting the asymptotic slope at the cut is the " + "tangent extrapolation this form exists to correct" + ) + if x_min is None: + raise ValueError( + "decay_extrap_form='moderation_tail' requires x_min " + "(the moderation coordinates' lower support point; " + "mNrmMin for consumption functions)" + ) + if decay_extrap_terms == 1: + raise ValueError( + "decay_extrap_terms does not apply to " + "decay_extrap_form='moderation_tail' (the form is C1 by " + "construction); leave it at its default" + ) + if x_cut is None: + x_list = getattr(interp, "x_list", None) + if x_list is None: + raise ValueError( + "x_cut is required: the wrapped interpolant exposes no " + "x_list grid to supply a default handoff point" + ) + x_cut = x_list[-1] + self.x_cut = float(x_cut) + if not np.isfinite(self.x_cut): + raise ValueError( + "x_cut must be finite, got " + repr(x_cut) + ) + if decay_extrap_form == "exp": + warnings.warn( + "DecayTailInterp(decay_extrap_form='exp'): the exponential " + "tail is retained only for parity with LinearInterp's legacy " + "default and is deprecated in this class; the power-law form " + "is the asymptotically correct tail (and the default).", + DeprecationWarning, + stacklevel=2, + ) + self.intercept_limit = float(intercept_limit) + self.slope_limit = float(slope_limit) + + # Tail inputs, read from the wrapped function where LinearInterp + # reads its top two knots: the level at x_cut always; the slope at + # x_cut only for the fitted forms (explicit-Q needs none, so any + # bare callable composes). + level_at_cut = float(np.asarray(self.interp(np.array([self.x_cut]))).ravel()[0]) + if not np.isfinite(level_at_cut): + raise ValueError( + "DecayTailInterp: the wrapped interpolant returns a non-finite " + f"level ({level_at_cut!r}) at x_cut={self.x_cut!r} -- x_cut is " + "outside its usable domain (e.g. below a lower_extrap=False " + "grid bottom); choose a cut where the body is defined" + ) + # np.float64, NOT a Python float: LinearInterp's grid-sourced + # level_diff is a numpy scalar, so its fitted-B division by an + # exactly-zero gap yields inf/nan (exp form then correctly returns + # the line; powerlaw warn-disables) -- a Python float would raise + # ZeroDivisionError on the same reachable boundary configuration. + level_diff = np.float64( + self.intercept_limit + self.slope_limit * self.x_cut - level_at_cut + ) + self.decay_extrap = False + if decay_extrap_form == "moderation_tail": + der = getattr(interp, "derivative", None) + if der is None: + raise ValueError( + "DecayTailInterp: the moderation tail slope-matches at " + "x_cut and needs the wrapped interpolant's derivative " + "there; this interpolant has none" + ) + slope_at_cut = float( + np.asarray(der(np.array([self.x_cut]))).ravel()[0] + ) + if not np.isfinite(slope_at_cut): + raise ValueError( + "DecayTailInterp: the wrapped interpolant returns a " + f"non-finite derivative ({slope_at_cut!r}) at " + f"x_cut={self.x_cut!r}; the moderation tail needs a " + "finite slope there" + ) + self._init_moderation_tail(level_diff, decay_extrap_Q, slope_at_cut) + return + if decay_extrap_Q is not None: + slope_at_cut = None + if decay_extrap_terms == 2: + der = getattr(interp, "derivative", None) + if der is None: + raise ValueError( + "DecayTailInterp: the default two-term (C1) tail " + "slope-matches at x_cut and needs the wrapped " + "interpolant's derivative there; this interpolant " + "has none -- pass decay_extrap_terms=1 for the " + "level-matched one-term tail" + ) + slope_at_cut = float( + np.asarray(der(np.array([self.x_cut]))).ravel()[0] + ) + if not np.isfinite(slope_at_cut): + raise ValueError( + "DecayTailInterp: the wrapped interpolant returns a " + f"non-finite derivative ({slope_at_cut!r}) at " + f"x_cut={self.x_cut!r}; the two-term tail needs a " + "finite slope there -- fix the cut or pass " + "decay_extrap_terms=1" + ) + self._init_explicit_tail( + level_diff, decay_extrap_Q, decay_extrap_terms, slope_at_cut + ) + return + der = getattr(interp, "derivative", None) + if der is None: + raise ValueError( + "DecayTailInterp: the fitted decay forms infer the tail from " + "the wrapped interpolant's slope at x_cut, but this " + "interpolant has no derivative method; pass decay_extrap_Q " + "for a level-matched explicit-exponent tail instead" + ) + slope_at_cut = float(np.asarray(der(np.array([self.x_cut]))).ravel()[0]) + if not np.isfinite(slope_at_cut): + raise ValueError( + "DecayTailInterp: the wrapped interpolant returns a non-finite " + f"derivative ({slope_at_cut!r}) at x_cut={self.x_cut!r}; the " + "fitted decay forms need a finite slope there -- fix the cut " + "or pass decay_extrap_Q" + ) + # Zero-uncertainty guard, exactly as in LinearInterp: a body already + # ON the limiting line has no gap to decay. + if not np.isclose(self.slope_limit, slope_at_cut, atol=1e-15): + slope_diff = self.slope_limit - slope_at_cut + self.decay_extrap_A = level_diff + self.decay_extrap_B = -slope_diff / level_diff + self.decay_extrap = True + if decay_extrap_form == "powerlaw": + self._init_powerlaw_fitted_tail(level_diff, slope_diff) + + def _init_powerlaw_fitted_tail(self, level_diff, slope_diff): + """Fitted power-law tail setup, mirroring + ``LinearInterp._init_powerlaw_decay`` with the top knot replaced by + ``x_cut``: level- and slope-matched there, valid only when the body + sits strictly below the limiting line and approaches it; otherwise + warn and disable (queries above ``x_cut`` then delegate to the body). + """ + ok = self.slope_limit > 0.0 and level_diff > 0.0 and self.decay_extrap_B > 0.0 + if ok: + pivot = self.x_cut + self.intercept_limit / self.slope_limit + ok = pivot > 0.0 + if not ok: + warnings.warn( + "DecayTailInterp(decay_extrap_form='powerlaw'): the wrapped " + "interpolant at x_cut is not strictly below the limiting " + "line with slope strictly above slope_limit (level_diff=" + f"{level_diff:.6g}, slope_diff={slope_diff:.6g}, " + f"slope_limit={self.slope_limit:.6g}); disabling decay " + "extrapolation -- queries above x_cut delegate to the " + "wrapped interpolant." + ) + self.decay_extrap = False + return + self.decay_extrap_pivot = pivot + self.decay_extrap_Q = self.decay_extrap_B * pivot + self.decay_extrap_Q_source = "fitted" + + def _init_explicit_tail(self, level_diff, Q, terms, slope_at_cut): + """Explicit-exponent tail setup, mirroring + ``LinearInterp._init_explicit_Q_decay`` (relaxed guard: no slope + condition -- the rescue case an explicit exponent exists to serve). + Level-matched at the cut unconditionally (the class invariant); with + ``terms=2`` (default) also slope-matched (the F11 C1 attachment, + ``A2 = gap*(Q_fit - Q)``, ``A = gap - A2``), falling back to one + term with a warning when ``Q_fit >= Q + 1``.""" + ok = self.slope_limit > 0.0 and level_diff > 0.0 + pivot = None + if ok: + pivot = self.x_cut + self.intercept_limit / self.slope_limit + ok = pivot > 0.0 + if not ok: + warnings.warn( + "DecayTailInterp(decay_extrap_Q=...): explicit-exponent decay " + "requires slope_limit > 0, a level at x_cut strictly below " + f"the limiting line, and a positive pivot (level_diff=" + f"{level_diff:.6g}, slope_limit={self.slope_limit:.6g}); " + "disabling decay extrapolation -- queries above x_cut " + "delegate to the wrapped interpolant." + ) + self.decay_extrap = False + return + self.decay_extrap_pivot = pivot + self.decay_extrap_Q = Q + self.decay_extrap_Q_source = "explicit" + if terms == 2: + # same expressions as LinearInterp's setup, sourced from the + # wrapped function's slope reading (byte-parity on HARK bodies) + slope_diff = self.slope_limit - slope_at_cut + self.decay_extrap_B = -slope_diff / level_diff + Q_fit = self.decay_extrap_B * pivot + A2 = level_diff * (Q_fit - Q) if np.isfinite(Q_fit) else np.nan + if np.isfinite(Q_fit) and Q_fit < Q + 1.0 and A2 != 0.0: + # THEOREM-REF[BufferStockTheory-Latest @ 3f4b021e :: theory/powerlaw-decay/grid_design_final_spec.md :: THE SPEC (owner-proposed scheme, sharpened by F1–F8) :: F11 — The C1 two-term attachment :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/grid-design-final-spec/] + # F11 closed forms: level+slope matching with the theory + # exponent leading; an EXACT collapse (A2 == 0) stores the + # one-term representation (byte-identical to terms=1); a + # non-finite Q_fit falls back rather than attach NaN. + self.decay_extrap_A2 = A2 + self.decay_extrap_A = level_diff - self.decay_extrap_A2 + self.decay_extrap_terms = 2 + self.decay_extrap = True + return + if not (np.isfinite(Q_fit) and Q_fit < Q + 1.0): + hNrm = pivot - self.x_cut + diag = "" + if self.decay_x_min is not None and np.isfinite(Q_fit): + hEx = hNrm + self.decay_x_min + mEx_cut = self.x_cut - self.decay_x_min + if mEx_cut > 0.0 and hEx > 0.0: + # exact steepness decomposition: + # Q_fit = s_mu * (1 + hEx/mEx); guard-safe boundary + # mEx > Q*hEx (the human-wealth-scale rule) + amp = 1.0 + hEx / mEx_cut + diag = ( + f" Diagnosis (x_min={self.decay_x_min:.6g}): " + f"s_mu={Q_fit / amp:.4g} times coordinate " + f"amplification {amp:.4g}; guard-safe boundary " + f"mEx_cut > Q*hEx = {Q * hEx:.4g}, vs mEx_cut = " + f"{mEx_cut:.4g} here." + ) + warnings.warn( + "DecayTailInterp(decay_extrap_Q=..., decay_extrap_terms=" + f"2): the fitted rate Q_fit={Q_fit:.4g} is not finite " + f"and strictly below Q+1={Q + 1.0:.4g} (at or above it, " + "the two-term leading amplitude would be non-positive; " + "non-finite signals a degenerate reading); falling back " + "to the one-term level-matched tail (C1 kink at the " + "cut). A steep fitted rate at a human-wealth-dominated " + "cut is usually coordinate amplification rather than " + "anomalous decay (the diagnosis below separates the two " + "when x_min is supplied): remedies are (a) extend the " + "grid top toward Q*hNrm ~= " + f"{Q * hNrm:.4g} (the human-wealth-scale rule), or (b) " + "decay_extrap_form='moderation_tail' (C1 at any cut, no " + f"guard; requires x_min).{diag}" + ) + else: + # fitted-rate diagnostic is undefined without a body slope reading + self.decay_extrap_B = np.nan + self.decay_extrap_A = level_diff + self.decay_extrap_A2 = 0.0 + self.decay_extrap_terms = 1 + self.decay_extrap = True + + def _init_moderation_tail(self, level_diff, Q, slope_at_cut): + """Moderation-coordinates C1 tail setup (tail-only; NOT the full + Method of Moderation -- see the class docstring). Level- and + slope-matched at ANY cut with no guard: the bounded ``exp(-u)`` + correction absorbs an arbitrarily steep local slope, so there is no + analog of the two-term ``Q_fit >= Q + 1`` fallback. Violations of + the moderation premises signal INCONSISTENT INPUTS and raise + (unlike the fitted/explicit power-law guards, which warn and + disable): the wrapped body's level at the cut must lie strictly + between the limiting ("optimist") line and the gap ceiling anchored + at ``x_min`` (the "pessimist" line). + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # Same decay law as the power-law forms: chi(mu) asymptotically + # linear with slope Q in mu = ln(x - x_min) is IDENTICALLY + # gap ~ mEx**(-Q); the moderation coordinates change only the + # ATTACHMENT (C1 at any cut, bounds built in), not the law. + """ + x_min = self.decay_x_min + if not self.slope_limit > 0.0: + raise ValueError( + "moderation_tail requires slope_limit > 0, got " + f"{self.slope_limit!r}" + ) + if not x_min < self.x_cut: + raise ValueError( + "moderation_tail requires x_min < x_cut, got " + f"x_min={x_min!r} >= x_cut={self.x_cut!r}" + ) + hEx = self.intercept_limit / self.slope_limit + x_min + if not hEx > 0.0: + raise ValueError( + "moderation_tail requires hEx = intercept_limit/slope_limit " + "+ x_min > 0 (the optimist-pessimist gap ceiling scale), " + f"got hEx={hEx!r}" + ) + ceiling = self.slope_limit * hEx + if not 0.0 < level_diff < ceiling: + raise ValueError( + "moderation_tail requires the body's level at x_cut " + "strictly between the pessimist and optimist lines: need " + "0 < level_diff < slope_limit*hEx, got level_diff=" + f"{float(level_diff)!r} vs ceiling={float(ceiling)!r}" + ) + mEx_cut = self.x_cut - x_min + omega_cut = float(level_diff) / ceiling + # chi and its mu-slope at the cut, from the body's own level and + # slope: chip = -mEx*g'/(g*(1-omega)) with g' = slope_limit - body' + slope_diff = self.slope_limit - slope_at_cut + self.decay_hEx = hEx + self.decay_gap_ceiling = ceiling + self.decay_mEx_cut = mEx_cut + self.decay_chi_cut = float(np.log((1.0 - omega_cut) / omega_cut)) + self.decay_chip_cut = float( + -mEx_cut * slope_diff / (float(level_diff) * (1.0 - omega_cut)) + ) + self.decay_extrap_Q = Q + self.decay_extrap_Q_source = "explicit" + self.decay_extrap = True + + def _body_y(self, x): + return np.asarray(self.interp(x), dtype=float) + + def _body_der(self, x): + der = getattr(self.interp, "derivative", None) + if der is not None: + return np.asarray(der(x), dtype=float) + # finite-difference fallback on the BODY, mirroring the default + # HARKinterpolator1D._der (only reachable for derivative-less bodies, + # which require explicit-Q tails) + eps = 1e-8 + return ( + np.asarray(self.interp(x + eps), dtype=float) + - np.asarray(self.interp(x), dtype=float) + ) / eps + + def _body_both(self, x): + ewd = getattr(self.interp, "eval_with_derivative", None) + if ewd is not None: + y, dydx = ewd(x) + return np.asarray(y, dtype=float), np.asarray(dydx, dtype=float) + return self._body_y(x), self._body_der(x) + + def _tail_y_der(self, x_above, want_y, want_der): + """Tail level/derivative above ``x_cut``: the same formulas, in the + same numerically stable arrangement, as + ``LinearInterp._apply_upper_decay``.""" + x_temp = x_above - self.x_cut + if self.decay_extrap_form == "moderation_tail": + # chi(mu) = chi_cut + Q*u + (chip_cut - Q)*(1 - e^-u): stable + # omega recovery via expm1 (chi clipped at 700, where omega has + # already underflowed to ~1e-304 and the tail IS the line); + # e^-u computed as the exact coordinate ratio mEx_cut/mEx. + mEx = x_above - self.decay_x_min + u = np.log1p(x_temp / self.decay_mEx_cut) + eu = self.decay_mEx_cut / mEx + chi = ( + self.decay_chi_cut + + self.decay_extrap_Q * u + + (self.decay_chip_cut - self.decay_extrap_Q) * (1.0 - eu) + ) + omega = 1.0 / (2.0 + np.expm1(np.minimum(chi, 700.0))) + y = ( + self.intercept_limit + self.slope_limit * x_above + - self.decay_gap_ceiling * omega + if want_y + else None + ) + dydx = ( + self.slope_limit + + self.decay_gap_ceiling * omega * (1.0 - omega) + * ( + self.decay_extrap_Q + + (self.decay_chip_cut - self.decay_extrap_Q) * eu + ) / mEx + if want_der + else None + ) + return y, dydx + if self.decay_extrap_form == "powerlaw": + if getattr(self, "decay_extrap_terms", 1) == 2: + # C1 two-term attachment (F11); expressions identical to + # LinearInterp._apply_upper_decay's two-term branch + lw = np.log1p(x_temp / self.decay_extrap_pivot) + w1 = np.exp(-self.decay_extrap_Q * lw) + w2 = np.exp(-(self.decay_extrap_Q + 1.0) * lw) + decay = self.decay_extrap_A * w1 + self.decay_extrap_A2 * w2 + y = ( + self.intercept_limit + self.slope_limit * x_above - decay + if want_y + else None + ) + dydx = ( + self.slope_limit + ( + self.decay_extrap_Q * self.decay_extrap_A * w1 + + (self.decay_extrap_Q + 1.0) * self.decay_extrap_A2 * w2 + ) / (x_temp + self.decay_extrap_pivot) + if want_der + else None + ) + return y, dydx + decay = self.decay_extrap_A * np.exp( + -self.decay_extrap_Q * np.log1p(x_temp / self.decay_extrap_pivot) + ) + y = ( + self.intercept_limit + self.slope_limit * x_above - decay + if want_y + else None + ) + dydx = ( + self.slope_limit + + self.decay_extrap_Q / (x_temp + self.decay_extrap_pivot) * decay + if want_der + else None + ) + return y, dydx + decay = self.decay_extrap_A * np.exp(-self.decay_extrap_B * x_temp) + y = ( + self.intercept_limit + self.slope_limit * x_above - decay + if want_y + else None + ) + dydx = self.slope_limit + self.decay_extrap_B * decay if want_der else None + return y, dydx + + def _evaluate(self, x): + x = np.asarray(x, dtype=float) + if not self.decay_extrap: + return self._body_y(x) + above = x > self.x_cut + if not np.any(above): + return self._body_y(x) + y = np.empty(x.shape, dtype=float) + body = ~above + if np.any(body): + y[body] = self._body_y(x[body]) + y[above], _ = self._tail_y_der(x[above], True, False) + return y + + def _der(self, x): + x = np.asarray(x, dtype=float) + if not self.decay_extrap: + return self._body_der(x) + above = x > self.x_cut + if not np.any(above): + return self._body_der(x) + dydx = np.empty(x.shape, dtype=float) + body = ~above + if np.any(body): + dydx[body] = self._body_der(x[body]) + _, dydx[above] = self._tail_y_der(x[above], False, True) + return dydx + + def _evalAndDer(self, x): + x = np.asarray(x, dtype=float) + if not self.decay_extrap: + return self._body_both(x) + above = x > self.x_cut + if not np.any(above): + return self._body_both(x) + y = np.empty(x.shape, dtype=float) + dydx = np.empty(x.shape, dtype=float) + body = ~above + if np.any(body): + yb, db = self._body_both(x[body]) + y[body] = yb + dydx[body] = db + ya, da = self._tail_y_der(x[above], True, True) + y[above] = ya + dydx[above] = da + return y, dydx + + +class KappaBarTailInterp(HARKinterpolator1D): + """ + Constraint-end (maximal-MPC) tail wrapper over ANY 1D interpolant -- the + bottom-end member of the ``DecayTailInterp`` family. At and above a knot + ``x_knot`` every query is delegated, unchanged, to the wrapped + interpolant; below the knot (and above the binding minimum ``mNrmMin``) + the function is the Theorem CE constraint-end form + + c(m) = MPCmax*me - K*me**(1.0 + CRRA), me = m - mNrmMin, + K = (MPCmax*me_knot - y_knot)/me_knot**(1.0 + CRRA) (value-matching), + + so the composed function is continuous at the knot by construction (the + amplitude is always the knot's own gap below the ``MPCmax`` line -- the + same level-continuity invariant as ``DecayTailInterp``). Queries at or + below ``mNrmMin`` return 0.0 (consumption is zero at the constraint and + undefined below it). + + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Theorem CE: the constraint-end approach exponent is the CRRA itself + # (q_down = rho): c = kap_bar*me - K*me**(1+rho)*(1+o(1)) as me -> 0, + # with NO log-periodic prefactor (the binding one-step map is the + # deterministic worst-branch contraction lambda = wp**(1/rho)*Thorn_Gamma + # < 1). No eigenvalue problem at this end: the exponent needs no + # root-finder, unlike the high-wealth min(1, q*). + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE-psi :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Theorem CE-psi (regime I): with permanent shocks the same q_down = rho + # law holds under the uniform-contraction criterion + # p_eff**(1/rho)*Thorn_Gamma < psi_min, with p_eff the worst-JOINT-atom + # mass (exactly HARK's WorstIncPrb accounting); outside it (regime II) + # q_down = min(rho, s*_+) and this form is NOT theorem-backed -- callers + # should gate on ``HARK.ConsumptionSaving.pf_decay.ce_psi_regime``. + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-prop-C1-psi :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # kap_bar = 1 - p_eff**(1/rho)*Thorn_R (infinite horizon), psi-invariant + # with p_eff the worst-JOINT-atom mass -- the psi==1 Prop C1 formula + # read with the joint worst mass. + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-prop-C2 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Prop C2 (the finite-horizon kap_bar_t recursion, terminal anchor + # kap_bar_T = 1): kap_bar_{T-n}**-1 = 1 + + # p_eff**(1/rho)*Thorn_R*kap_bar_{T-n+1}**-1. HARK's ``calc_mpc_max`` + # computes exactly this step, so passing the solver's ``MPCmaxUnc`` / + # ``solution.MPCmax`` is exact at ANY horizon (an infinite-horizon + # solve iterates it to the Prop C1 fixed point). + # THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-cor-C4 :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] + # Knot placement rule (the guard message below): the knot reads the + # constraint asymptote to relative tolerance tol iff + # me_knot <= (tol*kap_bar/K)**(1/rho) -- push the grid bottom below that + # scale (``pf_decay.aXtraMin_from_tail_tol`` inverts it). + + The MPC behavior is the theorem's content: c'(m) = MPCmax - (1 + + CRRA)*K*me**CRRA rises to MPCmax as me -> 0. This replaces (a) the EGM + bottom SECANT from the constraint corner to the first gridpoint, whose + slope understates MPCmax by exactly K*me_knot**CRRA, and (b) fitted- + tangent lower extrapolations, whose MPC can diverge (measured + Method-of-Moderation counterexample: MPC 3.56 at me = 1e-8). + + Modes (``strict``) and the in-solve constructor (``try_make``) + --------------------------------------------------------------- + ``strict=True`` (default; post-solve attachment, where a violation is a + real diagnosis): requires the Theorem CE regime at the knot, ``K >= 0`` + (knot at or below the MPCmax line) and ``K*me_knot**CRRA < MPCmax`` + (positivity of c below the knot; monotone in me, so holding at the knot + certifies the whole tail). Violation raises ``ValueError`` with the + st-cor-C4 grid-rule message. ``strict=False`` admits the two-sided + bootstrap CORRIDOR ``|K|*me_knot**CRRA < MPCmax`` (a knot ABOVE the + MPCmax line -- K < 0 -- from a contaminated iterate or solve; the tail + then approaches the MPCmax line from above and the inherited bias decays + like (me/me_knot)**CRRA). ``in_regime`` records ``K >= 0``; + ``knot_rel_deficit = K*me_knot**CRRA/MPCmax`` is the SIGNED st-cor-C4 + 'tol' realized at the knot. + + ``try_make`` is the guarded constructor for IN-SOLVE use (re-anchoring + each backward step): it returns ``None`` -- the caller keeps its default + assembly for that step -- unless the corridor holds AND the tail's MPC + over the exposed segment (0, me_knot] lies in ``(0, MPCmax]``: + + MPC <= MPCmax <=> K >= 0, + MPC > 0 at the knot <=> (1 + CRRA)*K*me_knot**CRRA < MPCmax. + + The MPC-range exposure gate closes the corridor's two recorded leaks (a + large-K corridor tail turns its MPC negative near the knot; a K < 0 + corridor tail carries MPC above MPCmax throughout), so in-solve exposure + is always a theorem-shaped tail. Sign self-correction across iterations + is preserved by the FALLBACK, not by exposing K < 0 tails: a refused + iterate keeps HARK's bottom secant, which lies BELOW the true concave + consumption function and therefore biases the next iterate's knot back + below the MPCmax line (K > 0), re-activating the tail. (This differs + from the reference stack's crude ``c = m`` rail, whose MPC-1 bias has the + opposite sign and made a strict in-solve gate deadlock there.) + + Parameters + ---------- + interp : callable + The wrapped interpolant (any object mapping numpy query arrays to + value arrays; all HARK 1D interpolants qualify). Queries at or above + ``x_knot`` delegate to it; a ``derivative`` method is delegated to + where present. + MPCmax : float + The maximal MPC at the constraint end: the solver's ``MPCmaxUnc`` / + ``solution.MPCmax`` (the analytic Prop C2 recursion, exact at any + horizon; infinite-horizon fixed point 1 - wp**(1/CRRA)*Thorn_R). + CRRA : float + Relative risk aversion; ALSO the tail exponent (Theorem CE). + mNrmMin : float + The binding minimum of market resources (``solution.mNrmMin``); 0 for + zero-income-atom calibrations. Must lie strictly below ``x_knot``. + Attach ONLY when the NATURAL borrowing constraint binds: with an + artificially-constrained kink the constraint end has MPC 1 and no + kap_bar asymptote (HARK's ``MPCmaxNow = 1.0`` override branch). + x_knot : float + Market resources at the attachment knot -- the first EGM gridpoint + above the constraint corner (``m_for_interpolation[1]`` at the + solver's assembly site). + y_knot : float or None (default) + Consumption at the knot. ``None`` reads the wrapped interpolant at + ``x_knot`` (must be finite); the solver passes its own exact node + value ``c_for_interpolation[1]``. + strict : bool (default True) + See Modes above. + """ + + distance_criteria = ["interp"] + + def __init__(self, interp, MPCmax, CRRA, mNrmMin, x_knot, y_knot=None, + strict=True): + self.interp = interp + self.MPCmax = float(MPCmax) + self.CRRA = float(CRRA) + self.mNrmMin = float(mNrmMin) + self.x_knot = float(x_knot) + if not (np.isfinite(self.MPCmax) and self.MPCmax > 0.0): + raise ValueError( + "KappaBarTailInterp: MPCmax must be a positive finite float " + "(the solver's MPCmaxUnc / solution.MPCmax), got " + + repr(MPCmax) + ) + if not (np.isfinite(self.CRRA) and self.CRRA > 0.0): + raise ValueError( + "KappaBarTailInterp: CRRA must be a positive finite float, " + "got " + repr(CRRA) + ) + me_knot = self.x_knot - self.mNrmMin + if not (np.isfinite(me_knot) and me_knot > 0.0): + raise ValueError( + "KappaBarTailInterp: the knot must lie strictly above " + f"mNrmMin (x_knot={self.x_knot!r}, mNrmMin={self.mNrmMin!r})" + ) + if y_knot is None: + y_knot = float( + np.asarray(self.interp(np.array([self.x_knot]))).ravel()[0] + ) + self.y_knot = float(y_knot) + if not np.isfinite(self.y_knot): + raise ValueError( + "KappaBarTailInterp: non-finite consumption at the knot " + f"({self.y_knot!r} at x_knot={self.x_knot!r})" + ) + # Value-matching amplitude (level continuity at the knot, the class + # invariant): K = (MPCmax*me - c)/me**(1+CRRA). + self.K = (self.MPCmax * me_knot - self.y_knot) / me_knot ** ( + 1.0 + self.CRRA + ) + deficit = self.K * me_knot**self.CRRA # signed st-cor-C4 'tol' + if strict: + if not (self.K >= 0.0 and deficit < self.MPCmax): + raise ValueError( + "KappaBarTailInterp: knot outside the constraint-end " + "regime -- enlarge the grid bottom (st-cor-C4: the knot " + "obeys the asymptote to relative tolerance tol only for " + "me_knot <= (tol*MPCmax/K)**(1/CRRA); K >= 0 requires " + "y_knot <= MPCmax*me_knot, positivity requires " + "K*me_knot**CRRA < MPCmax i.e. y_knot > 0; " + "pf_decay.aXtraMin_from_tail_tol inverts the rule). " + f"Got K={self.K:.6g}, me_knot={me_knot:.6g}, " + f"MPCmax={self.MPCmax:.6g}. With permanent shocks or a " + "positive worst atom also re-check MPCmax and mNrmMin " + "(regime gate: pf_decay.ce_psi_regime, st-rem-CE-regime)." + ) + else: + if not abs(deficit) < self.MPCmax: + raise ValueError( + "KappaBarTailInterp(strict=False): knot outside even the " + "bootstrap corridor |K|*me_knot**CRRA < MPCmax (relative " + "distance of y_knot from the MPCmax line >= 100%) -- use " + "try_make, which falls back instead of constructing this." + ) + self.strict = bool(strict) + self.in_regime = bool(self.K >= 0.0) + self.knot_rel_deficit = float(deficit / self.MPCmax) + self.mpc_at_knot = float(self.MPCmax - (1.0 + self.CRRA) * deficit) + + @classmethod + def try_make(cls, interp, MPCmax, CRRA, mNrmMin, x_knot, y_knot=None): + """Guarded constructor for IN-SOLVE use: returns ``None`` (the caller + keeps its default bottom assembly for that backward step) unless the + knot admits a corridor tail whose MPC over the exposed segment lies + in (0, MPCmax] -- the exposure gate described in the class docstring. + Never raises on out-of-regime knots; malformed scalar inputs + (non-positive MPCmax/CRRA, knot at or below mNrmMin, non-finite + y_knot) also return None so a transient broken iterate cannot abort + a solve.""" + if MPCmax is None or not np.isfinite(float(MPCmax)) or float(MPCmax) <= 0.0: + return None + if not np.isfinite(float(CRRA)) or float(CRRA) <= 0.0: + return None + me_knot = float(x_knot) - float(mNrmMin) + if not (np.isfinite(me_knot) and me_knot > 0.0): + return None + if y_knot is None: + y_knot = float(np.asarray(interp(np.array([float(x_knot)]))).ravel()[0]) + y_knot = float(y_knot) + if not np.isfinite(y_knot): + return None + K = (float(MPCmax) * me_knot - y_knot) / me_knot ** (1.0 + float(CRRA)) + deficit = K * me_knot ** float(CRRA) + # bootstrap corridor + the MPC-in-(0, MPCmax] exposure gate + if not abs(deficit) < float(MPCmax): + return None + if not (K >= 0.0 and (1.0 + float(CRRA)) * deficit < float(MPCmax)): + return None + return cls(interp, MPCmax, CRRA, mNrmMin, x_knot, y_knot=y_knot, + strict=True) + + def _body_y(self, x): + return np.asarray(self.interp(x), dtype=float) + + def _body_der(self, x): + der = getattr(self.interp, "derivative", None) + if der is not None: + return np.asarray(der(x), dtype=float) + eps = 1e-8 + return ( + np.asarray(self.interp(x + eps), dtype=float) + - np.asarray(self.interp(x), dtype=float) + ) / eps + + def _tail_y(self, x_below): + me = np.maximum(x_below - self.mNrmMin, 0.0) + return self.MPCmax * me - self.K * me ** (1.0 + self.CRRA) + + def _tail_der(self, x_below): + # MPC of the tail: -> MPCmax as me -> 0 (Theorem CE's content); the + # me <= 0 clip returns the limit MPCmax at/below the constraint. + me = np.maximum(x_below - self.mNrmMin, 0.0) + return self.MPCmax - (1.0 + self.CRRA) * self.K * me**self.CRRA + + def _evaluate(self, x): + x = np.asarray(x, dtype=float) + below = x < self.x_knot + if not np.any(below): + return self._body_y(x) + y = np.empty(x.shape, dtype=float) + body = ~below + if np.any(body): + y[body] = self._body_y(x[body]) + y[below] = self._tail_y(x[below]) + return y + + def _der(self, x): + x = np.asarray(x, dtype=float) + below = x < self.x_knot + if not np.any(below): + return self._body_der(x) + dydx = np.empty(x.shape, dtype=float) + body = ~below + if np.any(body): + dydx[body] = self._body_der(x[body]) + dydx[below] = self._tail_der(x[below]) + return dydx + + def _evalAndDer(self, x): + return self._evaluate(x), self._der(x) + + class CubicInterp(HARKinterpolator1D): """ An interpolating function using piecewise cubic splines. Matches level and diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e165772e3..f2199621a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -24,6 +24,28 @@ Release Date: TBD #### Minor Changes +- Adds `decay_extrap_form='powerlaw'` to `LinearInterp`: the gap below the limiting line decays as a power law in `(x + intercept_limit/slope_limit)` instead of exponentially, matching the level and slope of the interpolant at the top gridpoint just as the exponential form does. This is the asymptotically correct tail for buffer-stock consumption functions approaching their perfect-foresight asymptote; the default remains `'exp'` (byte-for-byte unchanged). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds opt-in perfect-foresight decay extrapolation machinery to the aggregate-shock consumption solvers (`ConsAggShockModel`): `pf_mpc_min` / `pf_human_wealth_markov` compute the PF asymptote slope and per-Markov-state joint human wealth at a caller-chosen reference return, and `make_cFunc_slice` attaches the (power-law) decay toward `MPCmin*(m + hNrm)` to each per-`Mgrid` cFunc slice, with a Carroll-Kimball concavity guard that raises on theoretically-impossible inputs. Default off: with the new solver arguments `MPCmin`/`hNrm` at their `None` defaults, behavior is byte-for-byte unchanged. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds keyword-only `decay_extrap_Q` to `LinearInterp`: `None` (default) is byte-for-byte the existing behavior (fitted power-law exponent `Q = B*(x_top + h)`); a positive float (requires `decay_extrap_form='powerlaw'`) attaches the power-law tail with that EXPLICIT exponent, level-matched at the top knot (amplitude = the level gap), at the cost of a small C1 kink `(Q_fit - Q)*A/pivot` in the derivative there under `decay_extrap_terms=1` (the default two-term attachment, added later in this PR, removes the kink). The slope-tangency validity guard is relaxed accordingly (only `slope_limit > 0`, a below-line top knot, and a positive pivot are required), so an explicit exponent can attach a decaying tail in the fitted form's `B <= 0` fallback cases. `decay_extrap_Q_source` records `'explicit'` vs `'fitted'`. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds keyword-only `q_diagnostics` to `LinearInterp`, an opaque measurement-diagnostics rider for measured-Q callers (the two-secant-over-the-top-three-knots workflow): stashed verbatim as `self.local_q_diag` (convention: the tuple `(Q1, Q2, drift)` of the two log-log secant exponents of the gap in the shifted abscissa `x + h`, with `decay_extrap_Q=Q2`, the most local secant), never consulted by evaluation, present -- default `None` -- on every construction path including guard-disable. Both spellings (keyword and attribute) mirror the downstream reference implementation (HAFiscal's `PowerLawDecayLinearInterp`) as a drop-in contract, so post-solve tools read the measurement off converged slices via `getattr(slice, "local_q_diag", None)`. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.powerlaw_tail_diagnostic`, a cheap post-solve wrong-exponent detector (one cFunc sweep, no re-solve): compensate the measured gap below the PF asymptote by trial exponents around `min(1, q_star)` and fit windowed log-log slopes (>= 20 points; never the 2-knot top-slope estimator); verdicts `CONFIRMED` / `PRE_ASYMPTOTIC` (the theorem-backed transient side, expected at near-resonance) / `INCONSISTENT` (flat point far from `min(1, q_star)` or steeper than the impossibility floor -- broken grid, wrong `MPCmin`/`hNrm` reference, or non-converged solve) / `UNMEASURABLE` (float-cancellation guard), with pre-registered thresholds in the docstring. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Threads theory-informed tail policy through the aggregate-shock solvers: `make_cFunc_slice` (and both `solveConsAggShock` / `solve_ConsAggMarkov`, via `time_inv_` exactly like `MPCmin`/`hNrm`) gains `decay_theory` (a `pf_decay.PowerLawDecayParams`, default `None` = byte-for-byte prior behavior) and `decay_Q`. With theory supplied, the default tail exponent is the theory value `min(1, q_star)` (level-matched; the fitted exponent is a noisy 2-knot estimator that can exceed the theoretically impossible 1); `decay_Q=None` keeps the fitted exponent but clamps it to the `min(1, q_star)` ceiling (warning when the clamp bites); a float is an explicit exponent. An explicit exponent also rescues the fallback branches where the fitted form had to disable decay (naive-linear extrapolation that never rejoins the PF line). Level continuity at the top knot is an invariant of the decay machinery: the former `('amplitude', B)` closed-form-amplitude mode, whose guarded level jump violated it, was removed before release (the tuple form raises with guidance toward the level-matched `decay_Q=1.0`; the boundary value `B_psi` keeps its diagnostic role in the amplitude-ratio log). Slices carry a `decay_theory` metadata dict, and at `q* > 1` the fitted-vs-closed-form amplitude ratio is logged once per params object. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `HARK.ConsumptionSaving.pf_decay`, the theory-quantities module behind the power-law decay form: `powerlaw_decay_params` computes, from model primitives (numpy-only, no solve), the decay exponent root `q_star` of `E[psi^(1+q)] = Rcal*Thorn_Gamma^q` and the realized exponent `min(1, q_star)`, the closed-form tail amplitude `B_psi` (at `q_star > 1`), the near-resonance multiplier `lambda_B`, the dual (Kesten) wealth-tail root `zeta_star`, and the GIC/RIC/FHWC condition flags, refusing cleanly (nan/None + a categorized, filterable warning — e.g. `NearResonanceWarning`) when a condition fails. Accepts HARK `DiscreteDistribution`s, `(atoms, probs)` tuples, or a joint `IncShkDstn`; `powerlaw_decay_params_from_agent` reads the primitives off an `IndShockConsumerType`-family agent. Human wealth is computed from primitives in the solver-side convention (`h = 1/(Rcal-1)`, EXCLUDES current income); it never consumes `solution.hNrm` (tolerance-truncated) or `bilt['hNrm']` (the other convention). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `tools/update_theorem_refs.py` and its CI wrapper `tests/test_theorem_refs.py`: the power-law-decay code cites its theorem program via pinned `THEOREM-REF[repo @ sha :: file :: section [:: label]]` comment tags (each with a standalone paraphrase); the tool scans them, verifies syntax always, verifies that every tag's file/section/label resolves at its pinned commit when a theorem-repo checkout is available (`--check`, `--repo`/`$THEOREM_REF_REPO`), and re-pins tags to a new commit (`--new-sha [--write]`, flagging section-body changes for paraphrase review). On public CI (no private theorem repo) the check degrades gracefully to syntax-only and passes. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds a certified grid-EXTENT criterion to `pf_decay`: `rel_gap_at` measures the relative consumption gap below the PF asymptote, and `aXtraMax_from_tail_tol` inverts the theorem's relative-gap decay law `gap/c ∝ (m+h)^-(1+q)` to the m at which that gap reaches a caller-chosen `tail_tol` — i.e. where the asset grid may stop and hand the tail to the power-law extrapolation with a provable ex-post error bound (both the true c and any level-matched monotone below-line tail live within `gap(m_top)` of each other beyond the top, by Carroll-Kimball concavity). Callers should invert with `q_eff = min(q, Q_local)` (the fitted local exponent of a coarse solve) because the pre-asymptotic local exponent approaches `min(1, q_star)` from below at `q_star < 1`; `tail_tol` is clamped at the float64-certifiable floor 1e-6. Pure function of primitives plus one measured reference gap — no solve, no new dependencies. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.qstar_probe` — the operator eigen-probe: measures the decay-exponent root q* NUMERICALLY from the model's own one-period backward step applied to power-law trial gaps (normalized at the probe point; two-epsilon differencing cancels the one-period premium), root-finding the unit multiplier. Grid-free (the trial is analytic, probed at end-of-period assets 1e6-1e8), so immune to sparse-knot contamination and pre-asymptotic drift — matches the analytic eigen-equation root to 5.6e-6/8.6e-6/5.0e-5 on the anchor calibrations with ~1e-6 depth-consistency, where estimation from solved values is 15-40% off. Portable to models WITHOUT a closed-form eigen-equation via a caller-supplied `one_step` + PF limits (anything a time-iteration solver already has); fails closed (nan + reason). Outside the GIC the probe and the idealized (E)-root can disagree — report alongside the condition flags. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.mNrm_stable_points` (+ `StablePoints`): the two classical stable points of a solved consumption function — mNrmTrg (E[m']=m, needs GIC-Mod) and mNrmStE (balanced level growth E[psi m']=m, GIC-Raw) — PLUS their mortality-adjusted twins with R -> LivPrb*R, which exist under the weaker GIC-Mod-Liv/GIC-Raw-Liv conditions even when the unadjusted target does not (the pure-GIC case): with perpetual-youth replacement and newborns at a=0, mortality factors exactly as that return shave in the cross-sectional mean dynamics (measured: the adjusted balanced-growth root lands on the GIC-cap atom's Harmenberg-neutral ergodic mean to 2.5%). Returns both m-roots and their end-of-period-asset images a = m - c(m) (the grid-relevant coordinates); duck-typed on any cFunc callable; nan (never raises) when a locus does not cross. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.aXtraMax_from_wealth_mass` (+ `WealthMassInfo`) and a `LivPrb` argument on `dual_root`: the asset-grid top as a MEASURED stationary wealth quantile — the smallest `aXtraMax` leaving at most `eps_wealth` of the chosen mass (wealth-weighted by default; agent-mass offered) above the grid, computed from the deterministic neutral-measure stationary distribution implied by a solved cFunc (mortality + newborn reset; no RNG). The mortality-augmented dual root `zeta_L` (solving `LivPrb*E[(Thorn_Gamma/psi)^zeta] = 1`) supplies existence and patience-sensitivity — it exists at the GIC patience cap where the classical Kesten root does not, and `zeta_L <= 1` (aggregate wealth not finite) makes the function refuse rather than grid away an economic property; the closed-form Pareto inversion serves only as the covering-grid top for the measurement (it is measured to undershoot unsafely at large zeta_L and overshoot by orders of magnitude near zeta_L = 1). Protocol: call on a coarse solve, then one certificate re-call on a cFunc re-solved with the returned top (load-bearing for patient types; certified quantiles land within 0.1-3.4% of dense-truth values on the anchor calibrations). Apply per type and take the max for a common grid (the most patient type binds). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `DecayTailInterp` to `HARK.interpolation`: the decay tail as a COMPOSABLE wrapper over ANY 1D interpolant, decoupling the tail law from the in-grid representation. At and below a handoff point `x_cut` (default: the wrapped interpolant's top knot) every query delegates to the wrapped body unchanged; above it, the function is the limiting line minus a decaying gap — the same tail family, validity guards, and numerically stable evaluation as `LinearInterp`'s built-in machinery, but sourced from the wrapped function's level (and, for the fitted forms, slope) at `x_cut`, so it byte-for-byte reproduces the baked-in tails over a bare linear body while also composing with `CubicInterp` (which has only the legacy exponential decay and no power-law option), econforge interpolants, or any bare callable (the one-term explicit-exponent mode needs no derivative; the default two-term mode reads the body's slope at the cut). `x_cut` need not be a knot: a cut below the body's top truncates it there and hands the rest to the tail law. Level continuity at `x_cut` is an invariant of the class: every attachable tail is level-matched there (there is deliberately no amplitude-override hook). The `'exp'` form is included for parity but DEPRECATED at birth in this class (selecting it warns; it is asymptotically impossible as a consumption-gap tail); `LinearInterp`'s own `'exp'` default is untouched. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds keyword-only `decay_extrap_terms` (default 2) to the explicit-exponent decay tails of `LinearInterp` and `DecayTailInterp`, threaded through the aggregate-shock solvers as `decay_terms`: `2` attaches the C1 TWO-TERM tail `gap = A*z**(-Q) + A2*z**(-(Q+1))`, level- AND slope-matched at the attachment point with the theory exponent leading (closed forms `A2 = gap*(Q_fit - Q)`, `A = gap - A2`; collapses exactly to the one-term tail when the locally fitted rate equals the theory exponent; warns and falls back to one term when `Q_fit >= Q + 1`); `1` is the level-matched one-term tail with the documented derivative kink. The two-term DEFAULT guards against Jacobian problems in SSJ-type (sequence-space Jacobian) approaches: policy derivatives are primitive inputs to SSJ Jacobian/fake-news construction and to differentiation through the solution, and a C1 kink at the attachment point makes them discontinuous for queries crossing it. The second exponent is `Q + 1` -- an attachment (boundary-layer) term absorbing exactly the one-term kink, deliberately NOT the theory-subleading exponent pair, whose spacing `|q* - 1|` vanishes at near-resonance calibrations and would blow up the amplitudes. Adversarially hardened before release: a non-finite fitted rate (degenerate top segment, infinite pivot) falls back to the warned one-term tail instead of attaching a silent NaN tail, and an EXACT collapse stores the one-term representation so it is byte-identical to `decay_extrap_terms=1` including derivatives. Note on pickles: an instance carrying a genuine two-term tail needs a `decay_extrap_terms`-aware HARK to evaluate as constructed (pre-two-term code would evaluate its leading term only). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `decay_extrap_form='moderation_tail'` to `DecayTailInterp` (with new keyword `x_min`, required by this form): a third tail family that borrows the Method-of-Moderation COORDINATES for the extrapolation region only (tail-only; NOT the full Method of Moderation solution representation). Writing the gap as a position `omega` in `(0, 1)` between the limiting ("optimist") line and the gap ceiling anchored at `x_min` (the "pessimist" bound; `mNrmMin` for consumption functions), and working through `chi = log((1 - omega)/omega)` against `mu = log(x - x_min)`, the tail `chi(mu) = chi_cut + Q*u + (chip_cut - Q)*(1 - exp(-u))` is level- AND slope-matched (C1) at ANY cut with NO guard: the bounded `exp(-u)` correction absorbs an arbitrarily steep local slope — exactly the `Q_fit >= Q + 1` region where the two-term power-law attachment must warn and fall back to the kinked one-term tail, which is the GENERIC configuration at human-wealth-dominated grid tops (conventional `aXtraMax`). The decay law is the same power law `gap ~ (x - x_min)**(-Q)` (the logistic link is only the coordinate system carrying it — this is not exponential/logistic decay of the gap); the gap stays strictly inside `(0, slope_limit*hEx)` by construction, `hEx = intercept_limit/slope_limit + x_min`; and the form collapses exactly to the pinned-slope chi-line when the body's chi-slope at the cut equals `Q`. Requires an explicit `decay_extrap_Q` (there is deliberately no fitted mode) and the body's derivative at the cut; moderation-premise violations (body not strictly between the pessimist and optimist lines at the cut, `hEx <= 0`, `x_min >= x_cut`) raise instead of warn-and-disable. The two-term guard-trip warnings in `LinearInterp` and `DecayTailInterp` now name the cause and the remedies — extend the grid top toward `Q*hNrm` (the human-wealth-scale rule) or use `'moderation_tail'` — and, when `x_min` is supplied to `DecayTailInterp`, print the exact steepness decomposition `Q_fit = s_mu*(1 + hEx/mEx)` with the guard-safe boundary `mEx > Q*hEx`. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds optional theory-pinned tail extrapolation to `IndShockConsumerType`, in the `decay_extrap_form` naming family (implemented in the new `solve_one_period_ConsIndShock_with_tails`, which `pre_solve` swaps in only when an option is enabled -- the public `solve_one_period_ConsIndShock` keeps its exact legacy signature and delegates with the options off, so agents and internal re-solve paths that skip `pre_solve`, e.g. the HANK Jacobian's `solve(presolve=False)`, are untouched): agent-level `decay_extrap_form in {None, 'powerlaw'}` (TOP end: the gap below the perfect-foresight asymptote decays as the theorem-backed power law with explicit exponent `min(1, q_star)`, auto-computed in `pre_solve` from `pf_decay.powerlaw_decay_params_from_agent`, validity-gated and refreshed per solve; threaded through `LinearInterp`'s explicit-Q machinery on the linear path and a `DecayTailInterp` wrap with `x_cut` at the top EGM knot on the `CubicBool` path) and `decay_extrap_form_lower in {None, 'kappabar'}` (BOTTOM end: below the first EGM gridpoint consumption follows Theorem CE's `c = MPCmax*me - K*me**(1+CRRA)` with the solver's own analytic `MPCmaxUnc` — the Prop C2 recursion, exact at any horizon — via the new `KappaBarTailInterp`). Because each backward step's assembled `cFunc` is what the NEXT step's Euler expectation evaluates (`vPfunc = u'(cFunc(m'))` through `calc_vp_next`), the tails act in BOTH roles at once: inside the solution recursion and on the returned policy — measured on the ported nested-grid fidelity battery (`tests/ConsumptionSaving/test_powerlaw_extrap.py`), the in-solve tails beat post-hoc attachment by ~350x at the constraint end (CE calibration) and ~2.4x at the top (HS calibration), with removed-window sup relative errors 4.8e-11 (bottom) / 2.3e-8 (top) against nested-grid truth. Bottom-tail guards: the natural constraint must be the binding one (`BoroCnstArt is None` or `BoroCnstNat >= BoroCnstArt` — a slack artificial constraint does NOT block the tail; one that strictly binds does, its constraint end being an MPC-1 kink with no kappa_bar asymptote), the Theorem CE-psi regime gate (regime II or undetermined warns `ConstraintEndRegimeWarning` naming st-rem-CE-regime and keeps the default secant), and a per-step corridor + MPC-range exposure gate. Defaults `None` = byte-for-byte the existing behavior (regression-pinned against the parent commit in the new test file). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `KappaBarTailInterp` to `HARK.interpolation`: the constraint-end (maximal-MPC) member of the `DecayTailInterp` wrapper family. At and above a knot `x_knot` queries delegate to the wrapped interpolant; below it consumption follows the Theorem CE form `MPCmax*me - K*me**(1+CRRA)` (`me = m - mNrmMin`), value-matched at the knot (level continuity is the class invariant), with MPC rising to `MPCmax` at the constraint — replacing the EGM bottom secant (slope biased by exactly `K*me_knot**CRRA`) and fitted-tangent lower extrapolations (whose MPC can diverge; measured Method-of-Moderation counterexample MPC 3.56 at `me = 1e-8`). `strict=True` (default) enforces the theorem regime at the knot (`K >= 0`, positivity below) and raises with the st-cor-C4 grid-rule message; `strict=False` admits the two-sided bootstrap corridor `|K|*me**CRRA < MPCmax`; classmethod `try_make` is the in-solve guarded constructor, layering an MPC-range exposure gate (tail MPC in `(0, MPCmax]`) over the corridor and returning `None` (caller keeps its default assembly) instead of raising. Queries at or below `mNrmMin` return 0. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds the constraint-end theory helpers to `pf_decay`: `ce_psi_regime(IncShkDstn, CRRA, Thorn_Gamma)` classifies a calibration's constraint end per Theorem CE-psi — regime I (uniform contraction `lambda(psi_min) = p_eff**(1/CRRA)*Thorn_Gamma/psi_min < 1`, where the approach exponent is the CRRA itself) versus regime II (`q_down = min(CRRA, s*_+)`, st-rem-CE-regime, where the kappa_bar tail is NOT theorem-backed), with `p_eff` the worst-JOINT-atom mass (exactly HARK's `WorstIncPrb` accounting) — plus the filterable `ConstraintEndRegimeWarning`; and `aXtraMin_from_tail_tol`, the bottom-end mirror of `aXtraMax_from_tail_tol` (st-cor-C4): inverts the Theorem CE deviation law `(kap_bar - c/me)/kap_bar = (K/kap_bar)*me**CRRA` from one measured reference node to the `aXtraMin` at which the bottom knot reads the constraint asymptote to a target relative tolerance (measured route primary — the amplitude `K` is explicit only modulo boundary data; ex-post certificate documented and tested), converting the knot rule from m-space to a-space via `aXtraMin = (1 - kap_bar)*me_target`. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.powerlaw_validity_threshold` (+ `ValidityThreshold`): the proofs' EXPLICIT guaranteed-validity floor `wbar0` for the top gap law, computed from primitives, in the total-wealth coordinate `wbar = m + hNrm`. Returns both the K̂-free `wbar0_hat_free` (display (5.0a): `max{ h+mbar, (8(rho+1)*gbar/kappa + C0)/Thorn_Gamma, 8(rho+1)*C0/Thorn_Gamma, (h+1+C0)/Thorn_Gamma, 2*zeta }`) and the full `wbar0` (display (5.0): `max{wbar0_hat_free, 2*K̂}`, `K̂ := 2(K_L+K_R)`, Cor. 5.2), with every constituent constant (`gbar=kappa*h`, `mbar=kappa*h/Thorn_R`, `C0`, `zeta`, `K_L`, `K_R`, `c2`, `tbar`) taken verbatim from `stage_A_proof.md` §5 and the binding term reported. This is a DIAGNOSTIC, never a refusal gate: the constants are deliberately CRUDE (statement.md Remark 7) — `wbar0` certifies "the tail is provably valid beyond here" with explicit constants, NOT where extrapolation first becomes accurate (empirically much earlier, near `m ~ hNrm`; the operative ex-post quality rule stays `aXtraMax_from_tail_tol`). Fails closed to `nan` + a diagnosis under GIC/FHWC/RIC violations (never raises). THEOREM-REF-pinned to (5.0a)/(5.0); hand-computed unit test. [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Adds `pf_decay.ergodic_grid_diagnostics` / `ergodic_grid_diagnostics_from_agent` (the ex-ante patience screen) and `pf_decay.ergodic_grid_report` (the postmortem coverage certificate) for grid design, with `ErgodicGridDiagnostics` / `ErgodicGridReport` / `ErgodicCoverageWarning`. The *shape* of the ergodic distribution of the market-resources ratio `m` is pinned ex ante by patience primitives (a Kesten random-growth process), so the tail exponent — survivor / agent-counting / Harmenberg, computed on the model's OWN discretized `psi` atoms via the mortality-EFFECTIVE patience ladder (`beta_eff = DiscFac*LivPrb`; the raw-`beta` `conditions` machinery can disagree — College-TOP has raw GIC-Nrm failing while effective holds) — is available WITHOUT a solve; only the distribution's location needs one. The numeric core is NOT reimplemented in HARK: it is soft-IMPORTED (lazily, at call time, never at HARK import time; `API_LEVEL` drift-guarded) from BufferStockTheory-Latest's `ergodic_coverage_lib` (the single source of truth), so the diagnostics — and their tests/notebook cells — are dormant/skipped wherever that checkout is absent (public CI unaffected). The report is HARK-native: a fixed-seed `agent.simulate()` on a `deepcopy` (it never mutates, re-grids, or re-solves the user's agent), the imported Hill estimator, and a coverage-vs-accuracy pairing with `rel_gap_at` at the top knot. Both are PASSIVE diagnostics — advisory `UserWarning` only, never an action; defaults byte-zero. Theory of record: BST-Latest `ergodic_coverage.md` (cited, not restated). [#1782](https://github.com/econ-ark/HARK/pull/1782) +- Documents and regression-tests the constraint-end tail's binding-constraint gate: the `decay_extrap_form_lower='kappabar'` bottom tail attaches whenever the natural constraint is the binding one — `BoroCnstArt is None` OR `BoroCnstNat >= BoroCnstArt` (evaluated per backward step, so a lifecycle agent's gate can flip across ages) — and refuses only when an artificial constraint STRICTLY binds. New tests cover an artificial-but-slack constraint (tail built, nested-grid fidelity vs the rails secant) and a lifecycle whose per-age natural constraint crosses a fixed artificial one (per-period wrap types flip). The gate wording ("natural-constraint branch only") is corrected accordingly. [#1782](https://github.com/econ-ark/HARK/pull/1782) +||||||| a25d3ae0 - Excludes scipy 1.18.0, whose `PPoly`-family objects (e.g. `CubicHermiteSpline`) cannot be `deepcopy`-ed (`TypeError: cannot pickle 'module' object`), breaking `ValueFuncCRRA` construction and the existing test suite wherever that scipy version is resolved. [#1788](https://github.com/econ-ark/HARK/pull/1788) - future item - future item diff --git a/examples/ConsIndShockModel/PowerlawExtrapolation.ipynb b/examples/ConsIndShockModel/PowerlawExtrapolation.ipynb new file mode 100644 index 000000000..81c3be622 --- /dev/null +++ b/examples/ConsIndShockModel/PowerlawExtrapolation.ipynb @@ -0,0 +1,904 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e8ae1e91", + "metadata": {}, + "source": [ + "# Power-law tail extrapolation for buffer-stock consumption functions\n", + "\n", + "## What problem is this solving?\n", + "\n", + "HARK solves a consumption-saving problem by computing the optimal consumption\n", + "function $c(m)$ — how much a household consumes as a function of its *normalized\n", + "market resources* $m$ (cash-on-hand measured in units of permanent income) — on a\n", + "**finite grid** of $m$ values. But a solved policy has to return an answer for\n", + "*any* $m$, including values **off the grid**:\n", + "\n", + "* **above the top gridpoint** — a household that is temporarily very rich (a big\n", + " income draw, or simply a wealthy type), and\n", + "* **below the bottom gridpoint** — a household right up against its borrowing\n", + " constraint.\n", + "\n", + "For those regions the solver **extrapolates**. The choice of extrapolation rule is\n", + "usually treated as a harmless technical detail. It is not: because the solver is a\n", + "backward recursion, *each period's consumption function is evaluated inside the\n", + "next period's expectation*, so a poor extrapolation rule feeds back and corrupts\n", + "the **interior** (on-grid) solution too — the part we actually use.\n", + "\n", + "This notebook shows two extrapolation rules that come with a **theorem** attached\n", + "— one for each end — and measures how much they matter, both off the grid and,\n", + "crucially, *on* it. They are opt-in on `IndShockConsumerType`\n", + "(`decay_extrap_form='powerlaw'` for the top, `decay_extrap_form_lower='kappabar'`\n", + "for the bottom); leaving them at their defaults (`None`) reproduces HARK's current\n", + "behavior byte-for-byte.\n", + "\n", + "The theory is developed in the [buffer-stock power-law-decay synthesis paper](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)\n", + "and its [formal statement page](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement); the in-practice experiments this notebook\n", + "mirrors are on [the extrapolators-in-practice page](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/extrapolators-in-practice). Specific claims below\n", + "link to the relevant result inline.\n" + ] + }, + { + "cell_type": "markdown", + "id": "dc7623fd", + "metadata": {}, + "source": [ + "## The vocabulary, in plain terms\n", + "\n", + "A few objects recur throughout. Definitions first, then the two laws.\n", + "\n", + "* **The perfect-foresight (PF) rule** $\\bar c(m)$. If income risk vanished, a\n", + " household with resources $m$ and *human wealth* $\\texttt{hNrm}$ (the present\n", + " value of expected future labor income, in the same normalized units) would\n", + " consume a constant fraction of its *total* wealth:\n", + " $$\\bar c(m) = \\underline{\\kappa}\\,(m + \\texttt{hNrm}).$$\n", + " This straight line is the **asymptote** the true $c(m)$ approaches as the\n", + " household gets rich: with a big enough buffer, precautionary saving stops\n", + " mattering and behavior tends to the PF rule.\n", + "* **MPCmin** $=\\underline{\\kappa}$ (kappa-underbar), the slope of that line: the\n", + " *limiting marginal propensity to consume* of a very rich household. The true MPC\n", + " $c'(m)$ falls toward $\\underline{\\kappa}$ from above as $m\\to\\infty$.\n", + "* **The gap** $g(m) = \\bar c(m) - c(m) \\ge 0$: how far actual consumption sits\n", + " *below* the PF line. Precaution makes households consume less than the PF rule,\n", + " so $g>0$; the gap shrinks to zero as $m\\to\\infty$. **How fast it shrinks is the\n", + " whole question.**\n", + "* **MPCmax** $=\\bar\\kappa$ (kappa-bar), the MPC at the *other* end — a household\n", + " right at its borrowing constraint, which spends nearly everything.\n", + "\n", + "**The two theorem-backed laws** (each links to its source):\n", + "\n", + "1. **Top end — a power law.** The gap decays as a *power* of total wealth,\n", + " $g \\propto \\bar w^{-\\min(1,\\,q^\\uparrow)}$ where $\\bar w = m + \\texttt{hNrm}$\n", + " and $q^\\uparrow$ is an eigenvalue computed from the model's primitives. A key\n", + " consequence: the gap can **never** decay faster than $1/\\bar w$ — a prudent\n", + " household always keeps at least a first-order precautionary premium — so\n", + " *exponential* decay of the gap is asymptotically **impossible**\n", + " ([synthesis paper, \"What is $q^\\star$?\"](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)). This matters because HARK's\n", + " *default* top extrapolation uses exponential decay.\n", + "2. **Bottom end — the maximal-MPC law.** Approaching the constraint,\n", + " $c(m) = \\bar\\kappa\\, m^e - K\\,(m^e)^{1+\\rho}$ with $m^e = m - \\texttt{mNrmMin}$\n", + " and $\\rho$ the coefficient of relative risk aversion; the MPC rises to\n", + " $\\bar\\kappa$ at the constraint. This is **Theorem CE** and, with permanent\n", + " shocks, **Theorem CE-$\\psi$** (valid in \"regime I\")\n", + " ([statement page, Thm CE / CE-$\\psi$](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement#st-thm-ce)).\n", + "\n", + "**One convention trap, stated once.** HARK's `hNrm` **excludes** the current\n", + "period's income, whereas the theory pages' human wealth $h_{\\rm BST}$ **includes**\n", + "it: $h_{\\rm BST} = \\texttt{hNrm} + 1$. All PF-rule arithmetic below uses HARK's\n", + "`hNrm`; when we say \"grid reaches human wealth\" we mean $m \\approx \\texttt{hNrm}$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "201cd178", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import warnings\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from HARK.ConsumptionSaving.ConsIndShockModel import IndShockConsumerType\n", + "from HARK.ConsumptionSaving.pf_decay import (\n", + " powerlaw_decay_params_from_agent,\n", + " ce_psi_regime,\n", + " powerlaw_validity_threshold,\n", + " powerlaw_tail_diagnostic,\n", + " rel_gap_at,\n", + " aXtraMax_from_tail_tol,\n", + ")\n", + "from HARK.interpolation import KappaBarTailInterp, DecayTailInterp\n", + "\n", + "# --- pinned calibration: HAFiscal \"College, top discount-factor type\"\n", + "# preferences, with a zero-income unemployment atom (IncUnemp=0 => the borrowing\n", + "# constraint sits at mNrmMin=0, giving a clean Theorem-CE bottom). The quarterly\n", + "# income volatilities are HAFiscal's; HARK's 0.1/0.1 defaults would change q*.\n", + "CRRA = 2.0 # rho, relative risk aversion\n", + "Rfree = 1.01 # gross quarterly return\n", + "PermGroFac = 1.0 + 0.01958 / 4 # permanent income growth (quarterly)\n", + "DiscFac = 0.995714 # quarterly discount factor\n", + "LivPrb = 1.0 - 1.0 / 160.0 # survival probability\n", + "PARS = dict(\n", + " cycles=0, T_cycle=1, CRRA=CRRA, Rfree=[Rfree], DiscFac=DiscFac,\n", + " LivPrb=[LivPrb], PermGroFac=[PermGroFac], BoroCnstArt=None,\n", + " vFuncBool=False, CubicBool=False,\n", + " UnempPrb=0.027, IncUnemp=0.0,\n", + " TranShkStd=[0.12 ** 0.5], TranShkCount=7,\n", + " PermShkStd=[0.003 ** 0.5], PermShkCount=7,\n", + " T_retire=0, UnempPrbRet=0.0, IncUnempRet=0.0,\n", + ")\n", + "# the option pair featured here: power-law top tail + kappabar bottom tail\n", + "TAILS = dict(decay_extrap_form=\"powerlaw\", decay_extrap_form_lower=\"kappabar\")\n", + "\n", + "\n", + "def log_grid(aMin, aMax, N):\n", + " return np.exp(np.linspace(np.log(aMin), np.log(aMax), N))\n", + "\n", + "\n", + "def solve(grid, tol=1e-9, **opts):\n", + " '''Solve the pinned agent on a given end-of-period asset grid. `opts` selects\n", + " the extrapolation: pass **TAILS for the theorem tails, nothing for HARK's\n", + " defaults (exponential top decay + a secant at the bottom).'''\n", + " pars = dict(PARS); pars.update(opts)\n", + " a = IndShockConsumerType(**pars); a.verbose = 0\n", + " a.aXtraGrid = np.asarray(grid, float); a.tolerance = tol\n", + " with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\")\n", + " a.solve()\n", + " return a.solution[0]\n", + "\n", + "\n", + "def m_nodes(cf):\n", + " '''The endogenous m-gridpoints of an assembled consumption function, seen\n", + " through any tail wrappers and the lower-envelope.'''\n", + " f = cf\n", + " while True:\n", + " if isinstance(f, (KappaBarTailInterp, DecayTailInterp)):\n", + " f = f.interp\n", + " elif hasattr(f, \"functions\"):\n", + " f = f.functions[0]\n", + " elif hasattr(f, \"x_list\"):\n", + " return np.asarray(f.x_list, float)\n", + "\n", + "\n", + "def sup_rel(cfunc, truth_cfunc, m):\n", + " '''sup relative error in consumption vs the dense-truth solve, over m:\n", + " max_m |c(m) - c_truth(m)| / c_truth(m).'''\n", + " v = np.asarray(cfunc(m), float); t = np.asarray(truth_cfunc(m), float)\n", + " return float(np.max(np.abs(v - t) / t))\n" + ] + }, + { + "cell_type": "markdown", + "id": "c47bbf3d", + "metadata": {}, + "source": [ + "## The model's theory quantities\n", + "\n", + "`pf_decay` computes the theorem quantities directly from primitives — no solve\n", + "required. A short gloss on each, then the numbers.\n", + "\n", + "* **$q^\\star$** and **$\\min(1,q^\\star)$** — the eigenvalue governing the top decay\n", + " and the *realized* exponent ([what $q^\\star$ is](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)). Here $q^\\star<1$, so\n", + " the top gap decays like $\\bar w^{-q^\\star}$.\n", + "* **$\\underline{\\kappa}$ / $\\bar\\kappa$** — the limiting (rich) and maximal\n", + " (constrained) MPCs.\n", + "* **regime** — Theorem CE-$\\psi$ holds in \"regime I\" (a uniform-contraction\n", + " condition on the worst income draw); outside it the bottom law is not\n", + " theorem-backed ([Thm CE-$\\psi$, regime remark](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement#st-rem-ce-regime)).\n", + "* **$\\bar w_0$** — the proofs' *explicit* validity floor: the top law is\n", + " **proven** for $\\bar w \\ge \\bar w_0$. Its constants are deliberately crude, so\n", + " $\\bar w_0$ says \"provably valid beyond here\", **not** \"the grid must reach here\"\n", + " — in practice accuracy arrives far earlier, as the experiments below show\n", + " ([synthesis paper](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)).\n", + "\n", + "A `NearResonanceWarning` is *expected*: this calibration sits within ~1% of the\n", + "$q^\\star=1$ knife-edge, where the pre-asymptotic window is unusually long." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c2e6f5f9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "q* = 0.6727 (realized top exponent min(1,q*) = 0.6727 < 1)\n", + "MPCmin (kappa_underbar) = 0.01021 slope of the PF line\n", + "MPCmax (kappa_bar) = 0.83736 MPC at the borrowing constraint\n", + "human wealth hNrm = 196.85 (theory-page h_BST = hNrm+1 = 197.85)\n", + "constraint-end regime = I (Theorem CE-psi applies)\n", + "validity floor wbar0 = 4752 = 24.1 x hNrm (crude 'provably valid beyond here')\n", + "near-resonance (|lam_B-1|<1%)= True (expected; long pre-asymptotic window)\n" + ] + } + ], + "source": [ + "agent0 = IndShockConsumerType(**PARS); agent0.verbose = 0\n", + "with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\")\n", + " agent0.update_income_process()\n", + "\n", + "with warnings.catch_warnings(record=True) as wrec:\n", + " warnings.simplefilter(\"always\")\n", + " th = powerlaw_decay_params_from_agent(agent0)\n", + "near_resonance = any(\"NEAR-RESONANCE\" in str(w.message) for w in wrec)\n", + "\n", + "h = th.h # HARK hNrm (excludes current income)\n", + "q_eff = min(1.0, th.q_star)\n", + "\n", + "# MPCmax (kappa_bar) comes from a solve; do one and reuse it below\n", + "sol_h = solve(log_grid(1e-3, h, 48), **TAILS)\n", + "kap_bar = sol_h.MPCmax\n", + "\n", + "reg = ce_psi_regime(agent0.IncShkDstn[0], CRRA, th.Thorn_Gamma)\n", + "vt = powerlaw_validity_threshold(Rfree, PermGroFac, DiscFac, CRRA,\n", + " LivPrb=LivPrb, IncShkDstn=agent0.IncShkDstn[0],\n", + " warn=False)\n", + "\n", + "print(f\"q* = {th.q_star:.4f} (realized top exponent min(1,q*) = {q_eff:.4f} < 1)\")\n", + "print(f\"MPCmin (kappa_underbar) = {th.kappa:.5f} slope of the PF line\")\n", + "print(f\"MPCmax (kappa_bar) = {kap_bar:.5f} MPC at the borrowing constraint\")\n", + "print(f\"human wealth hNrm = {h:.2f} (theory-page h_BST = hNrm+1 = {h+1:.2f})\")\n", + "print(f\"constraint-end regime = {reg['regime']} (Theorem CE-psi applies)\")\n", + "print(f\"validity floor wbar0 = {vt.wbar0:.0f} = {vt.wbar0/h:.1f} x hNrm (crude 'provably valid beyond here')\")\n", + "print(f\"near-resonance (|lam_B-1|<1%)= {near_resonance} (expected; long pre-asymptotic window)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "2cc3fc11", + "metadata": {}, + "source": [ + "## Why the *shape* of the top extrapolation matters\n", + "\n", + "Off the top of the grid, HARK must guess how $c(m)$ behaves. Two continuous rules\n", + "are on the table, and both are **level- and slope-matched** at the top gridpoint,\n", + "so they agree there and differ only in how the gap closes further out:\n", + "\n", + "* **HARK's default — exponential decay.** The gap $g$ is sent to zero\n", + " *exponentially* in $\\bar w$. Convenient, but the theory says the gap can never\n", + " fall faster than $1/\\bar w$, so exponential decay is asymptotically **wrong** in\n", + " shape ([synthesis paper, the $1/\\bar w$ floor](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)).\n", + "* **The power-law tail** (`decay_extrap_form='powerlaw'`) — the gap decays as\n", + " $\\bar w^{-\\min(1,q^\\star)}$, the theorem-backed shape\n", + " ([statement page, the gap law](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement)).\n", + "\n", + "We are **not** comparing against a \"use the PF line above a cutoff\" rule — a rule\n", + "with a kink or jump at the cutoff that nobody actually uses. The honest question\n", + "is: *between the two real, smooth rules — exponential vs power-law — how much does\n", + "the shape matter?* The measure throughout is the **sup relative error in\n", + "consumption** against a dense reference (\"truth\") solve:\n", + "$$\\text{err} = \\max_{m} \\frac{|c_{\\rm grid}(m) - c_{\\rm truth}(m)|}{c_{\\rm truth}(m)}.$$\n" + ] + }, + { + "cell_type": "markdown", + "id": "ac743d34", + "metadata": {}, + "source": [ + "## How big is the gap the extrapolation must represent?\n", + "\n", + "Before comparing rules, it helps to see how large the gap actually is at plausible\n", + "grid tops. The quantity `rel_gap_at` returns the *relative* gap\n", + "$(\\bar c - c)/c$ at a given $m$ — i.e. how far, in percent of consumption, the true\n", + "policy sits below the PF line there. That is the size of the thing any\n", + "extrapolation has to get right." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9ec80e90", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " grid top m | in units of hNrm | relative gap (cbar-c)/c\n", + "------------------------------------------------------------\n", + " 98.4 | 0.5x | 31.9%\n", + " 196.8 | 1.0x | 18.6% <- human wealth\n", + " 393.7 | 2.0x | 9.4%\n", + " 787.4 | 4.0x | 4.3%\n", + " 4724.3 | 24.0x | 0.4% <- validity floor scale\n" + ] + } + ], + "source": [ + "truth = solve(log_grid(1e-6, 1e5, 1500)) # dense reference (\"truth\")\n", + "print(f\"{'grid top m':>12} | {'in units of hNrm':>16} | {'relative gap (cbar-c)/c':>24}\")\n", + "print(\"-\" * 60)\n", + "for k in (0.5, 1.0, 2.0, 4.0, 24.0):\n", + " m = k * h\n", + " rg = float(np.atleast_1d(rel_gap_at(truth.cFunc, np.array([m]), th.kappa, h))[0])\n", + " note = \" <- human wealth\" if k == 1.0 else (\" <- validity floor scale\" if k == 24.0 else \"\")\n", + " print(f\"{m:12.1f} | {k:14.1f}x | {rg:>22.1%}{note}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "8ed2b2c9", + "metadata": {}, + "source": [ + "Read this as: if you stop the grid at $m = \\texttt{hNrm}$ (one times human\n", + "wealth), the true policy is still ~19% below the PF line there — a substantial gap\n", + "the extrapolation must shape correctly. Stop much earlier and the gap is huge;\n", + "stop far past human wealth and it is small. The rest of the notebook asks what that\n", + "does to accuracy." + ] + }, + { + "cell_type": "markdown", + "id": "c3d364dd", + "metadata": {}, + "source": [ + "## Four grids, one truth\n", + "\n", + "We solve the same model on four grids that differ in **how far out the top\n", + "gridpoint reaches**, at a *constant node density* (so differences reflect grid\n", + "*extent*, not how finely each grid is spaced):\n", + "\n", + "* **G1 — too short.** Top at $m\\approx5$, far below human wealth\n", + " ($\\approx{}$ 0.03 of `hNrm`). A deliberately pre-asymptotic anchor.\n", + "* **G2 — the human-wealth rule.** Top at $m\\approx\\texttt{hNrm}$. The simple\n", + " practical rule \"extend the grid to human wealth.\"\n", + "* **G3 — four times human wealth.** Top at $m\\approx4\\,\\texttt{hNrm}$. This grid\n", + " answers the practical question: **is pushing the grid past human wealth to\n", + " $4\\times$ meaningfully better than stopping at $1\\times$?**\n", + "* **G4 — the guaranteed regime.** Top past the proven validity floor\n", + " $\\bar w_0 - \\texttt{hNrm}$ ($\\approx 23\\times$ human wealth).\n", + "\n", + "\"Truth\" is a dense solve on a very wide grid, used as the reference." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e9440d45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "G1 ( too short): 32 nodes, top gridpoint m = 5.0 ( 0.03 x hNrm)\n", + "G2 ( = hNrm): 48 nodes, top gridpoint m = 200.3 ( 1.02 x hNrm)\n", + "G3 ( = 4 hNrm): 53 nodes, top gridpoint m = 797.1 ( 4.05 x hNrm)\n", + "G4 (guaranteed): 60 nodes, top gridpoint m = 4603.5 ( 23.39 x hNrm)\n", + "\n", + "G1 diagnostic (no truth needed): verdict 'INCONSISTENT'; gap at top knot 100%; aXtraMax_from_tail_tol says extend to m~3849 for 1% tolerance\n" + ] + } + ], + "source": [ + "DENS = 9.0 # nodes per log10-decade, held constant across grids\n", + "\n", + "def grid_to(aMax, aMin=1e-3):\n", + " n = max(12, int(round(DENS * np.log10(aMax / aMin))))\n", + " return log_grid(aMin, aMax, n)\n", + "\n", + "grids = {\"G1\": grid_to(4.0), \"G2\": grid_to(h), \"G3\": grid_to(4 * h),\n", + " \"G4\": grid_to(vt.wbar0 - h)}\n", + "sols = {}\n", + "for name, g in grids.items():\n", + " sols[name] = dict(pl=solve(g, **TAILS), exp=solve(g)) # power-law vs default\n", + "\n", + "lbl = {\"G1\": \"too short\", \"G2\": \"= hNrm\", \"G3\": \"= 4 hNrm\", \"G4\": \"guaranteed\"}\n", + "for name in (\"G1\", \"G2\", \"G3\", \"G4\"):\n", + " top = m_nodes(sols[name][\"pl\"].cFunc)[-1]\n", + " print(f\"{name} ({lbl[name]:>10}): {len(grids[name]):2d} nodes, \"\n", + " f\"top gridpoint m = {top:8.1f} ({top/h:6.2f} x hNrm)\")\n", + "\n", + "# HARK flags the too-short grid WITHOUT any truth comparison:\n", + "g1 = sols[\"G1\"][\"pl\"]\n", + "diag = powerlaw_tail_diagnostic(g1.cFunc, th.kappa, h, th, m_lo=1.0, m_hi=4.0)\n", + "g1top = m_nodes(g1.cFunc)[-1]\n", + "rg1 = float(np.atleast_1d(rel_gap_at(g1.cFunc, np.array([g1top]), th.kappa, h))[0])\n", + "print(f\"\\nG1 diagnostic (no truth needed): verdict '{diag.verdict}'; \"\n", + " f\"gap at top knot {rg1:.0%}; aXtraMax_from_tail_tol says extend to \"\n", + " f\"m~{aXtraMax_from_tail_tol(g1top, rg1, q_eff, h, 1e-2):.0f} for 1% tolerance\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "cf7f01ab", + "metadata": {}, + "source": [ + "## Off-grid accuracy: exponential vs power-law\n", + "\n", + "For each grid, sup relative error above the top gridpoint, exponential (HARK\n", + "default) vs power-law tail, against truth." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "21d7d3b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "grid | top / hNrm | power-law | exp-decay | exp/power-law\n", + "--------------------------------------------------------------\n", + " G1 | 0.03x | 6.99e-02 | 3.09e-01 | 4.4x\n", + " G2 | 1.02x | 5.77e-03 | 1.77e-02 | 3.1x\n", + " G3 | 4.05x | 3.54e-03 | 4.08e-03 | 1.2x\n", + " G4 | 23.39x | 1.18e-03 | 1.06e-03 | 0.9x\n", + "\n", + "bottom end (near the constraint): kappabar 2.63e-08 vs default secant 6.28e-06 (238x)\n" + ] + } + ], + "source": [ + "print(f\"{'grid':>4} | {'top / hNrm':>10} | {'power-law':>10} | {'exp-decay':>10} | {'exp/power-law':>13}\")\n", + "print(\"-\" * 62)\n", + "fid = {}\n", + "for name in (\"G1\", \"G2\", \"G3\", \"G4\"):\n", + " top = m_nodes(sols[name][\"pl\"].cFunc)[-1]\n", + " win = log_grid(top * 1.05, min(top * 10, 3.0e4), 60)\n", + " e_pl = sup_rel(sols[name][\"pl\"].cFunc, truth.cFunc, win)\n", + " e_ex = sup_rel(sols[name][\"exp\"].cFunc, truth.cFunc, win)\n", + " fid[name] = dict(top=top, win=win, e_pl=e_pl, e_ex=e_ex)\n", + " print(f\"{name:>4} | {top/h:9.2f}x | {e_pl:10.2e} | {e_ex:10.2e} | {e_ex/e_pl:11.1f}x\")\n", + "\n", + "# bottom end: the kappabar tail vs HARK's default secant, near the constraint\n", + "tbot = m_nodes(truth.cFunc)[1]\n", + "g2 = sols[\"G2\"]; bk = m_nodes(g2[\"pl\"].cFunc)[1]\n", + "wb = log_grid(max(tbot * 1.2, 1e-5), bk * 0.9, 50)\n", + "eb_k = sup_rel(g2[\"pl\"].cFunc, truth.cFunc, wb)\n", + "eb_s = sup_rel(g2[\"exp\"].cFunc, truth.cFunc, wb)\n", + "print(f\"\\nbottom end (near the constraint): kappabar {eb_k:.2e} vs default secant \"\n", + " f\"{eb_s:.2e} ({eb_s/eb_k:.0f}x)\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "22fa2a5d", + "metadata": {}, + "source": [ + "**What this says.**\n", + "\n", + "* At **G2** (grid to human wealth) the power-law tail is about **3× more accurate**\n", + " than exponential decay above the top gridpoint — the shape genuinely matters at a\n", + " realistic grid top.\n", + "* By **G3** (four times human wealth) the two rules have nearly converged, and at\n", + " **G4** (deep in the asymptotic regime) they are indistinguishable: once you are\n", + " well past human wealth the gap is tiny and its shape barely matters. So the\n", + " power-law tail earns its keep precisely at the *short* grid you would want to use.\n", + "* At **G1** (far too short) both are poor, but power-law is still ~4× better.\n", + "* At the **bottom**, the `kappabar` tail beats HARK's default secant by **~200×**\n", + " near the constraint: a much larger effect, because the secant systematically\n", + " mis-slopes as the MPC rises to $\\bar\\kappa$ ([Theorem CE](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement#st-thm-ce)).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "899b9e8d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))\n", + "\n", + "# (left) rel error above the top gridpoint, power-law vs exp, at hNrm and 4hNrm:\n", + "# the shape matters at hNrm (solid well below dotted) but not at 4hNrm (they meet)\n", + "for name, c, lab in [(\"G2\", \"C0\", \"G2 (hNrm)\"), (\"G3\", \"C1\", \"G3 (4 hNrm)\")]:\n", + " win = fid[name][\"win\"]; ct = np.asarray(truth.cFunc(win), float)\n", + " ax[0].loglog(win / h, np.abs(np.asarray(sols[name][\"pl\"].cFunc(win), float) - ct) / ct,\n", + " c + \"-\", lw=1.8, label=f\"{lab} power-law\")\n", + " ax[0].loglog(win / h, np.abs(np.asarray(sols[name][\"exp\"].cFunc(win), float) - ct) / ct,\n", + " c + \":\", lw=1.8, label=f\"{lab} exp-decay\")\n", + "ax[0].axvline(1.0, color=\"0.6\", ls=\"--\", lw=1)\n", + "ax[0].set_xlabel(\"m / hNrm (above the top gridpoint)\")\n", + "ax[0].set_ylabel(\"relative error in c vs truth\")\n", + "ax[0].set_title(\"(A1) top: power-law (solid) vs exp (dotted)\")\n", + "ax[0].legend(fontsize=8)\n", + "\n", + "# (right) MPC across the whole range: -> kappa_bar at the constraint, -> MPCmin far out\n", + "mm = log_grid(1e-4, 3.0e4, 400)\n", + "ax[1].semilogx(mm, np.asarray(g2[\"pl\"].cFunc.derivative(mm), float), \"C0-\",\n", + " label=\"MPC (power-law + kappabar tails)\")\n", + "ax[1].axhline(th.kappa, color=\"C2\", ls=\"--\", label=r\"$\\underline{\\kappa}$ = MPCmin\")\n", + "ax[1].axhline(kap_bar, color=\"C3\", ls=\"--\", label=r\"$\\bar{\\kappa}$ = MPCmax\")\n", + "ax[1].set_xlabel(\"m\"); ax[1].set_ylabel(\"MPC = c'(m)\")\n", + "ax[1].set_title(r\"(A2) MPC spans $\\bar{\\kappa}$ (constraint) to $\\underline{\\kappa}$ (rich)\")\n", + "ax[1].legend(fontsize=8)\n", + "fig.tight_layout(); plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "859f5b09", + "metadata": {}, + "source": [ + "## The part we actually use: the *in-grid* solution\n", + "\n", + "Extrapolation accuracy off the ends is not the real prize. What we use for\n", + "simulation and policy is the consumption function **on the grid** — the interior.\n", + "And here is the subtle point: because the solver is a backward recursion, *the\n", + "consumption function built at each step is evaluated inside the previous step's\n", + "expectation over next period's resources*. A big income draw can throw next\n", + "period's resources past the current top gridpoint, so **the extrapolation rule\n", + "feeds back into the interior solution** ([two-roles experiment](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/extrapolators-in-practice)).\n", + "\n", + "So the grid's **extent** — not just its density — changes the interior policy.\n", + "To see it cleanly we evaluate each grid's solution **at its own gridpoints** (where\n", + "interpolation error is exactly zero), so any difference from truth is pure\n", + "feedback contamination, and we compare grid *extents* at matched density." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0951ce7d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "G1-region interior: G1 max rel err 2.67e-02\n", + "G1-region interior: G2 max rel err 1.30e-02\n", + "G2-region interior: G2 max rel err 1.43e-02\n", + "G2-region interior: G3 max rel err 1.44e-02\n", + "G2-region interior: G4 max rel err 1.43e-02\n" + ] + }, + { + "data": { + "image/png": "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keOoVPEqLy8Pb29vgDI9RTIyMkq31xamaXLxxRfTunVrHnzwQfz8/MoV3UROxJaQQEb8POzJyXi2aolHaCj5P68GwBIQQNh99xJ09dUYFcz59nzCDqCkcHWsFsG+3HpOm+qOLiIiIu6W/D28fTWmo4ClzbsAtjK7TdOgReOW7skmIiJSB1SqeNWvXz8WLFjA+PHjS4tXxcXFPProo5x//vmnfD3TNCkqKsLLywtLBfMDHeVyuf5xf0VWrlzJwYMH+emnn/DwqPT89NKA2RISODA+rrRbv33nLuw7dwEQeMUVhD1wPx5Nm5Y7r9jp4p01e0k8dKTC6+7JzK/W3CIiIlILJH0H71xDsaOA6dFd+NQsKVz9OdUVpmlQlDKCvDz1uhIRETmeSlVznn32WS688EISEhIwTZM77riDFStWkJWVxY8//njS10lPT+e///0vr7zyCrt372blypVccMEFZY6x2+3cf//9LFiwgPz8fM466yzmz59P586dAdi0aRN33313hdf/7rvv2L59O+eccw4PPPAA27dv5+KLL2bChAmaW0hOWkb8vNLC1bE8W7Wi2awnKzxn5fZDPP75Fnal51W43zAgOtS/yrOKiIhILZL0LbxzLQXOQh6I7sT3pq20WOXIa4/FKwOXPQSLM4ioED93pxUREam1KlW86tGjB+vXr2fu3Lnk5uaybds2hg4dyn333UdUVNRJX+fll18mLy+PN99887g9th566CE++eQTfvjhB1q1asU999zD4MGD2b59O/7+/rRu3ZqnnnqqwnOtViv+/v58//33PPfccwwaNIipU6cSHBzM6NGjK3Pr0sAUpx2iKDGxXOEKwJGaWm5bYlouj3+xle92pAMQ7OfJ4C4RLPplH8csKIRpQtyA9tWeX0RERNzkz8JVtquIcVFnsNHMw3R5UHDgevpHnseqnRk48wOxGgZPjuhCZKCvuxOLiIjUWpUqXk2bNo1p06bx/PPPn9aDT5kyBYD9+/dXuD8vL49XX32VF154gW7dugHw0ksvER4ezuLFixk1ahQBAQH079//uI/Rp08fIiMjue6667BYLHz77bfs3r37tHJL/ecqKiLrjf+R8eqr4HKVP8Aw8IqOLv02K8/O7GU7eHvNXpwuE0+rwa39ovi/i84g0NeTC9qHMmd5IknpeUSH+hM3oD2xXSJq8I5ERESkxiR9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lV7b0pDp9V9/6LmsH/Q81g96HusPPZcVU8+resLb25upU6fi7e3t7igiVUava6mv9NqWhkyv//pDz2X9oOexftDzWH/ouayYel6JiIiIiIiIiEitpZ5XIiIiIiIiIiJSa6l4JSIiIiIiIiIitZaHuwOIiFQF0zTZtWsXAI0aNSIiIsLNiUREROSovXv3Yrfb8fT0pHXr1u6OI9KgFBQUcODAAQBCQkIICgpybyCRSlDPqwYoIyODG2+8kUaNGtGmTRsWLVrk7kgip62goIDY2FjOO+88xowZ4+44IpU2f/58IiMjCQ8PZ/bs2e6OI+JWBw8e5KqrrsLPz48zzjiDJUuWuDuSVNKoUaMYOHAg559/vrujyClITk6mX79++Pv7ExsbS0ZGhrsjSSWsX7+e2NhY+vbty/z5890dRyrJ4XAwffp0wsLCaNq0KRMnTnR3pBql4lUDtGTJEoYMGUJaWhqvvfYao0ePpri42N2xRE6Ln58fO3fu5PXXX3d3FJFK27VrF9OnT+fbb79l9erVvPDCC/zxxx/ujiXiNh9++CE33HADmZmZPP3004waNcrdkaSSli9fzs8//+zuGHKK7rnnHoYNG8ahQ4do3749U6dOdXckqYR+/fqxc+dOxo8f7+4ocho2btyIYRhs2bKFtWvX8s477/DDDz+4O1aN0bDBWmrr1q3s2bOHc889F39//3L7HQ4H69atw26307t3b3x9fUv3maaJ0+ksd45hGFitVkaPHl26rXfv3jRp0gQPD70UpPp99913bN26lWHDhtG8efNy+48cOcKyZcvIycnhrLPOokOHDqX7ioqK2LdvX4XXbdu2LYZhVFtukVORmprK77//TpcuXWjRokWFx2zZsoVDhw4RExNDWFhY6fYVK1YwbNiw0tf+1VdfzTfffEPnzp1rJLtIZWzcuJGDBw9y0UUX4eXlVW6/3W7nt99+wzRNevfuXWbp7xO1We65557SbX379qVp06bVcxPCpk2b+OmnnzjrrLPo3r17uf0Oh4MVK1Zw8OBBOnXqxL/+9a/SfccO3f+7li1barl3N3I6nfz0009YLBbOOeecCo/Jyspi06ZNBAcH061btzL7VqxYwQcffICPjw8TJkzg4osvronY8jemafLNN9+QlJTEyJEjK/xdmJ2dzbJlyygoKKB///60adPGDUnlRGw2G6tXr6Z169Zl/q9zrN27d5OcnEx0dHSZYda9evWiV69eQMnwz/DwcJo0aVIjuWsFU2qVpUuXmueee64ZGRlpAubWrVvLHfPHH3+Ybdq0MVu3bm3GxMSYTZs2NZctW1a6/7333jOtVmu5P82bNy9znYKCAnPYsGHmkiVLqv2+pGH7/PPPzU6dOpm9e/c2AfObb74pd8y2bdvMZs2amb169TIvv/xy08/Pz3z66adL969fv95s27ZthX+KiopKj/vqq6/Myy67rCZuS6SMLVu2mFdffbXZvHlz02KxmK+99lq5Y3Jzc80BAwaYwcHB5plnnmn6+PiYzzzzTOn+J5980nzkkUdKv3/66afNyZMn10h+kVP10UcfmX379i1ts6SkpJQ7Zu3atWbz5s3Ntm3bmu3btzcjIiLMn376qXT/f//73wrbLB06dChzndzcXHPgwIHmypUrq/u2GpwNGzaY/fv3Nzt37mz6+vqazz77bLljDh8+bPbu3duMjo42R44caTZt2tS88cYbTZfLZZqmaRYVFR33PXr9+vWl10lJSTFbt25dQ3cmM2fONKOiosywsDCzc+fOFR7z2muvmX5+fmavXr3M0NBQs2/fvmZGRoZpmqZps9nMgICA0mPz8vLMwMDAmogux3j33XfNtm3bmr169TKBMj9TR61du9Zs2rSpefbZZ5tDhw41fX19zVdffbXccVOnTjVnzZpVA6nl71JSUsw777zTjIiIMP39/c3777+/3DFOp9McNWqU6efnZ/bt29f09fU177rrrtLftceaNm2aOXHixJqIXmto2GAts2/fPmbOnPmPczpcf/319OjRg+TkZP744w9GjRrFtddey5EjR4CST+odDke5P/v37y+9xuHDh7nsssu48847GTZsWLXflzRsXl5efPDBB3z++efHPeauu+6iW7durF27lo8//pj//e9/TJo0ia1btwLQo0cPdu7cWeGfij7pF6lp+/btY8SIESQnJ+Pp6VnhMZMnTyY5OZnExETWrl3LBx98wEMPPcTq1asBCA0NJS0trfT4tLQ0QkNDayS/yKk6ePAgc+bMYcGCBRXudzgcXHPNNVx88cXs3LmT7du3M3ToUK699lrsdjsAo0ePrrDNsm3bttLrpKWlMWzYMB555BEuuOCCmri1BsXpdPLEE0+wefNmAgICKjxm2rRp5OTksH79ehYvXsz3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8uijjxIXF0eLFi3cG15KHT58mPfeew8o6Rm3evVq5s+fT+vWrbnkkkuAklXrzjrrLC699FLOP/983n33XTp27Mjtt9/uzuhyAj///DM5OTkkJydz5MgRli5dCpQUlj09Pbnyyivp27cvQ4cOZcKECWzdupU33nijTMFZ3G/Tpk38+OOPpSNvPvjgA1avXs0555xD165dsVqt/Pvf/+baa6/FZrMREhJCfHw8cXFx+v9hLVJcXMzy5cuBkmHYu3fvZunSpQQGBnL22WcD8Mgjj9C7d2+uu+46Lr/8chYvXozNZuOhhx5yZ/RawzAb0tqKdcDixYsrnNBy/PjxXHrppaXff/DBB3z44YfY7XYGDRrE7bffjtVqrcmoIiftl19+qfB1femll5b51Hbbtm28++672Gw2zj77bEaOHKnx3VJn/PHHH9x///3ltg8YMIAHH3yw9PvffvuN+fPnc+jQIbp3786ECRPKTYQsUhe88cYbpUWPY02aNInzzz+/9PuFCxeW9rgaMmQIt9xyi3631yIHDx5kxowZ5bZ369aNsWPHln6fmprKm2++ycGDB+nUqRO33norPj4+NRlVTtH48ePZsWNHue2LFy8mICAAgLy8PGbPns3atWsJDg7mtttu0weHtcyKFStYvHhxue1XX301F110Uen3v//+O++//z4FBQVceOGFDBs2rCZjygnYbDauvvrqctvbt2/PSy+9VPp9UlISc+bMITk5mbZt23LffffRqlWrmoxaa6l4JSIiIiIiIiIitZbmvBIRERERERERkVpLxSsREREREREREam1VLwSEREREREREZFaS8UrERERERERERGptVS8EhERERERERGRWkvFKxERERERERERqbVUvBIRERERERERkVpLxSsRKbV3715WrFgBQHZ2NosWLcJut7stT23I0NCczL/5kSNHWLRoEQUFBTX+2KcrJSWFpUuXVtv1RUTE/b777juSk5MBWL9+PT/88INb89SGDA3Nyfybb9q0iW+//dYtj326fvrpJ7Zv316tjyFS26h4JSKlbr31VpKSkgDYvXs31113HTab7aTPP3z4MIsWLaK4uLhK8lQmQ3Wq6vurjU7m3zw1NZXrrruOzMzMGn/svzvV5yQoKIjbb7+dNWvWVDamiIjUYomJiYwcOZKAgAAA3nzzTZ5++ulTusa6detYtWpV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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def at_own_nodes(sol, m_hi):\n", + " m = m_nodes(sol.cFunc); mm = m[(m > m[1] * 1.05) & (m < m_hi)]\n", + " ct = np.asarray(truth.cFunc(mm), float); cv = np.asarray(sol.cFunc(mm), float)\n", + " return mm, np.abs(cv - ct) / ct\n", + "\n", + "g1top = m_nodes(sols[\"G1\"][\"pl\"].cFunc)[-1]\n", + "g2top = m_nodes(sols[\"G2\"][\"pl\"].cFunc)[-1]\n", + "\n", + "fig, ax = plt.subplots(1, 2, figsize=(12, 4.3), sharey=True)\n", + "# (left) G1 vs G2, on G1's interior\n", + "for name, c in [(\"G1\", \"C3\"), (\"G2\", \"C0\")]:\n", + " mm, err = at_own_nodes(sols[name][\"pl\"], g1top * 0.95)\n", + " ax[0].loglog(mm, err, c + \"o-\", ms=4, label=f\"{name} (top {m_nodes(sols[name]['pl'].cFunc)[-1]/h:.2f} hNrm)\")\n", + " print(f\"G1-region interior: {name} max rel err {err.max():.2e}\")\n", + "ax[0].set_title(\"(B1) in-grid error: too-short G1 vs human-wealth G2\")\n", + "ax[0].set_xlabel(\"m (interior, at own gridpoints)\")\n", + "ax[0].set_ylabel(\"relative error in c vs truth\"); ax[0].legend(fontsize=8)\n", + "\n", + "# (right) G2 vs G3 vs G4 (hNrm, 4hNrm, guaranteed) on G2's interior -- they overlap\n", + "for name, c in [(\"G2\", \"C0\"), (\"G3\", \"C1\"), (\"G4\", \"C2\")]:\n", + " mm, err = at_own_nodes(sols[name][\"pl\"], g2top * 0.95)\n", + " ax[1].loglog(mm, err, c + \"o-\", ms=3,\n", + " label=f\"{name} (top {m_nodes(sols[name]['pl'].cFunc)[-1]/h:.0f}x hNrm)\")\n", + " print(f\"G2-region interior: {name} max rel err {err.max():.2e}\")\n", + "ax[1].set_title(\"(B2) in-grid: hNrm vs 4hNrm vs guaranteed -- essentially identical\")\n", + "ax[1].set_xlabel(\"m (interior, at own gridpoints)\"); ax[1].legend(fontsize=8)\n", + "fig.tight_layout(); plt.show()\n" + ] + }, + { + "cell_type": "markdown", + "id": "7f8dde24", + "metadata": {}, + "source": [ + "**The payoff, and it is the whole point.**\n", + "\n", + "* **G1 vs G2 (left).** The too-short grid's *interior* consumption is visibly\n", + " contaminated near the top of its range — its worst on-grid error is ~2× G2's on\n", + " the same region — even though both grids interpolate there. Stopping the grid\n", + " short does not merely mis-extrapolate off the end; it degrades the policy you\n", + " actually use.\n", + "* **G2 vs G3 vs G4 (right).** The human-wealth grid (`hNrm`), the four-times grid\n", + " ($4\\,\\texttt{hNrm}$), and the far-longer guaranteed-regime grid ($\\approx23\\,\n", + " \\texttt{hNrm}$) give the **same interior to three digits** — their curves lie\n", + " exactly on top of one another. So the direct answer to *\"is four times human\n", + " wealth meaningfully better than one times?\"* is **no**, not for the interior\n", + " policy, which is what we use. Pushing the grid past human wealth only tightens\n", + " the far-off-grid tail, a region you rarely evaluate.\n", + "\n", + "Together: **extend the grid to about human wealth and stop.** That keeps the\n", + "interior clean; four times human wealth adds nothing on the inside, and the crude\n", + "proven floor $\\bar w_0 \\approx 24\\times$ human wealth is far more than needed —\n", + "\"guaranteed\" and \"necessary\" are very different numbers." + ] + }, + { + "cell_type": "markdown", + "id": "1b90272f", + "metadata": {}, + "source": [ + "## Appendix: when the bottom tail attaches\n", + "\n", + "The `kappabar` bottom tail is built when the **natural** borrowing constraint is\n", + "the binding one — i.e. `BoroCnstArt is None` *or* `BoroCnstNat >= BoroCnstArt`. An\n", + "*artificial* constraint that binds only *slackly* does not block it; one that binds\n", + "*strictly* does (its constraint end is a kink with MPC $=1$, where Theorem CE's\n", + "$\\text{MPC}\\to\\bar\\kappa$ mechanism does not operate). For this calibration the\n", + "natural constraint sits at $\\texttt{BoroCnstNat} = 0$." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "656dc6a4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BoroCnstArt= None | mNrmMin= 0.00 | kappabar tail BUILT | no artificial constraint\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BoroCnstArt= -0.5 | mNrmMin= 0.00 | kappabar tail BUILT | artificial, slack (-0.5 < 0)\n", + "BoroCnstArt= 0.2 | mNrmMin= 0.20 | refused (default secant kept) | artificial, strictly binds (0.2 > 0)\n" + ] + } + ], + "source": [ + "for label, boro in [(\"no artificial constraint\", None),\n", + " (\"artificial, slack (-0.5 < 0)\", -0.5),\n", + " (\"artificial, strictly binds (0.2 > 0)\", 0.2)]:\n", + " s = solve(log_grid(1e-4, 1e3, 120), tol=1e-8,\n", + " BoroCnstArt=boro, decay_extrap_form_lower=\"kappabar\")\n", + " built = isinstance(s.cFunc, KappaBarTailInterp)\n", + " print(f\"BoroCnstArt={str(boro):>5} | mNrmMin={s.mNrmMin:5.2f} | \"\n", + " f\"{'kappabar tail BUILT' if built else 'refused (default secant kept)':32} | {label}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "89fe7c54", + "metadata": {}, + "source": [ + "## Grid coverage vs grid accuracy: the ex-ante patience screen\n", + "\n", + "Everything above was about **accuracy** — how faithfully `c(m)` is computed. There\n", + "is a second, independent question: does the grid **cover** the region where the\n", + "population actually lives? A grid could be perfectly accurate over a range the\n", + "simulated agents never visit, or (worse) too short to reach the wealth levels a\n", + "patient calibration produces.\n", + "\n", + "Remarkably, the *shape* of the ergodic distribution of `m` is pinned down **ex\n", + "ante**, from patience primitives alone: the high-`m` dynamics are a Kesten\n", + "random-growth process, so the **tail exponent** is available without solving —\n", + "only the distribution's *location* needs a solve\n", + "([theory of record: ergodic coverage](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/)). `pf_decay` exposes this as an\n", + "ex-ante screen (`ergodic_grid_diagnostics_from_agent`) plus a postmortem\n", + "(`ergodic_grid_report`).\n", + "\n", + "The numeric core is **imported** from a BufferStockTheory-Latest checkout (a single\n", + "source of truth, not duplicated in HARK); the cells below run only where that\n", + "checkout is present and otherwise print a dormancy note." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8c097c1b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "regime: powerlaw\n", + "GIC-Nrm raw 1.00075 (FAILS) effective 0.99762 (holds)\n", + " -> raw and effective disagree (mortality-financed tameness): True\n", + "tail exponent alpha (ex ante, on the model's OWN psi atoms):\n", + " survivor 2.708 counting 3.871 Harmenberg 5.529\n", + " no-mortality shadow (raw beta): 0.467\n", + " infinite mean? False infinite variance? False\n" + ] + } + ], + "source": [ + "import os, sys\n", + "# locate a local BufferStockTheory-Latest checkout (dev-only; dormant in public CI)\n", + "for _cand in [os.environ.get(\"BST_POWERLAW_DECAY_DIR\"),\n", + " \"/home/shared/github/llorracc/BufferStockTheory-Latest/theory/powerlaw-decay\",\n", + " os.path.expanduser(\"~/github/llorracc/BufferStockTheory-Latest/theory/powerlaw-decay\")]:\n", + " if _cand and os.path.isdir(_cand):\n", + " if _cand not in sys.path:\n", + " sys.path.insert(0, _cand)\n", + " break\n", + "try:\n", + " from HARK.ConsumptionSaving.pf_decay import (\n", + " ergodic_grid_diagnostics_from_agent, ergodic_grid_report)\n", + " with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\")\n", + " d = ergodic_grid_diagnostics_from_agent(agent0)\n", + " ERGODIC = True\n", + "except Exception as e:\n", + " ERGODIC = False\n", + " print(\"Ergodic screen DORMANT (needs a local BufferStockTheory-Latest checkout).\")\n", + " print(\" \", str(e).split(\".\")[0])\n", + "\n", + "if ERGODIC:\n", + " print(f\"regime: {d.regime}\")\n", + " print(f\"GIC-Nrm raw {d.GICNrm_raw:.5f} \"\n", + " f\"({'FAILS' if not d.GICNrm_raw_holds else 'holds'}) \"\n", + " f\"effective {d.GICNrm_eff:.5f} \"\n", + " f\"({'holds' if d.GICNrm_eff_holds else 'FAILS'})\")\n", + " print(f\" -> raw and effective disagree (mortality-financed tameness): \"\n", + " f\"{d.gicnrm_split}\")\n", + " print(f\"tail exponent alpha (ex ante, on the model's OWN psi atoms):\")\n", + " print(f\" survivor {d.alpha_survivor:.3f} counting {d.alpha_counting:.3f} \"\n", + " f\"Harmenberg {d.alpha_harmenberg:.3f}\")\n", + " print(f\" no-mortality shadow (raw beta): {d.alpha_survivor_rawbeta:.3f}\")\n", + " print(f\" infinite mean? {d.infinite_mean} infinite variance? \"\n", + " f\"{d.infinite_variance}\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d2300ed", + "metadata": {}, + "source": [ + "**What the screen says (this calibration).** The normalized GIC condition\n", + "**fails on raw β** (`GICNrm_raw` > 1) but **holds on the mortality-effective**\n", + "`β·LivPrb` — and the solved model has a finite target, so the *effective* ladder is\n", + "the one that governs. Tameness here is entirely **mortality-financed**: the\n", + "no-mortality shadow exponent is ≈ 0.47, and an exponent below 1 means an\n", + "**infinite-mean** ratio distribution — without the survival haircut this\n", + "calibration would not have a well-behaved wealth distribution at all.\n", + "\n", + "The tail exponents come in three flavours (survivor / agent-counting / Harmenberg\n", + "measures) and must be read on the model's **own discretized ψ atoms** — swapping the\n", + "income menu moves them materially, so the lognormal closed form is only a compass.\n", + "With α ≈ 3.9 (counting) the mean and variance are finite (α > 2), so simulated\n", + "moments converge." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e86bdc2e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "where the population lives (simulated m): median 3.93, 99th 10.50, 99.9th 14.76, max 32\n", + "Hill(1%) = 6.73 (>= ex-ante counting alpha 3.87: pre-asymptotic thinning, expected)\n", + "\n", + "take m = 20 (~ HARK's default aXtraMax):\n", + " COVERAGE : P(m > 20) = 7.3e-05 -> a grid top of 20 already covers the population\n", + " ACCURACY : rel_gap at m = 20 = 0.70 -> but stopping the grid at 20 would hand off with a ~70% error\n", + "\n", + "coverage is satisfied by m ~ 20; ACCURACY needs the grid to reach human wealth ~ 197. Different questions, different scales -- accuracy binds.\n" + ] + } + ], + "source": [ + "if ERGODIC:\n", + " # postmortem on a solved agent at the human-wealth grid (G2 = hNrm)\n", + " ra = IndShockConsumerType(**PARS); ra.verbose = 0\n", + " ra.aXtraGrid = grid_to(h)\n", + " with warnings.catch_warnings():\n", + " warnings.simplefilter(\"ignore\")\n", + " ra.solve()\n", + " rep = ergodic_grid_report(ra, seed=0, thresholds=(20.0,))\n", + " rg20 = float(np.atleast_1d(\n", + " rel_gap_at(ra.solution[0].cFunc, np.array([20.0]), d.MPCmin_eff, h))[0])\n", + " print(f\"where the population lives (simulated m): median {rep.quantiles[0.5]:.2f}, \"\n", + " f\"99th {rep.quantiles[0.99]:.2f}, 99.9th {rep.quantiles[0.999]:.2f}, \"\n", + " f\"max {rep.quantiles['max']:.0f}\")\n", + " print(f\"Hill(1%) = {rep.hill[0.01]:.2f} (>= ex-ante counting alpha \"\n", + " f\"{rep.alpha_counting_exante:.2f}: pre-asymptotic thinning, expected)\")\n", + " print()\n", + " print(f\"take m = 20 (~ HARK's default aXtraMax):\")\n", + " print(f\" COVERAGE : P(m > 20) = {rep.mass_above_thresholds[20.0]:.1e} \"\n", + " f\"-> a grid top of 20 already covers the population\")\n", + " print(f\" ACCURACY : rel_gap at m = 20 = {rg20:.2f} \"\n", + " f\"-> but stopping the grid at 20 would hand off with a ~{100*rg20:.0f}% error\")\n", + " print()\n", + " print(f\"coverage is satisfied by m ~ 20; ACCURACY needs the grid to reach human \"\n", + " f\"wealth ~ {h:.0f}. Different questions, different scales -- accuracy binds.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "2fe90630", + "metadata": {}, + "source": [ + "**Coverage vs accuracy — the payoff.** A grid top at `m = 20` would *cover*\n", + "the ergodic distribution (only ~10⁻⁵ of the population sits above it) yet be badly\n", + "*inaccurate* (a ~60% handoff error, because 20 is deeply pre-asymptotic). The two\n", + "criteria live at different scales — coverage ~ 20, accuracy ~ human wealth (~197) —\n", + "and the **accuracy** criterion binds. So the notebook's earlier rule, *\"reach human\n", + "wealth, then stop,\"* is the accuracy-driven one; the ex-ante screen then certifies\n", + "coverage almost for free, and flags pathological calibrations (raw-GIC failure,\n", + "infinite-mean/variance tails) from primitives alone.\n", + "\n", + "**Discipline (as everywhere in this notebook):** these are *passive* diagnostics.\n", + "Like `aXtraMax_from_tail_tol`, they **report**; they never re-grid or re-solve.\n", + "HARK tells you what a wider grid would buy; you decide whether to pay for it." + ] + }, + { + "cell_type": "markdown", + "id": "57a11227", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "* Two opt-in, theorem-backed extrapolation tails\n", + " ([power-law top](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement), [Theorem-CE bottom](https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement#st-thm-ce)); defaults\n", + " leave HARK unchanged.\n", + "* Compared to the **real** default rules (not a strawman PF-line-with-a-kink): the\n", + " **bottom** `kappabar` tail is a large win (~200×) near the constraint; the\n", + " **top** power-law tail is ~3× better than exponential decay at a grid reaching\n", + " human wealth, and ties it far out where the gap is negligible.\n", + "* The decisive effect is **in-grid**: a grid that stops well short of human wealth\n", + " contaminates the interior policy through the solver's own expectations; a grid to\n", + " human wealth fixes it; four times human wealth — or the guaranteed floor — gives\n", + " the same interior. The practical rule is \"reach human wealth, then stop,\" and HARK\n", + " ships the diagnostics (`powerlaw_tail_diagnostic`, `aXtraMax_from_tail_tol`,\n", + " `powerlaw_validity_threshold`) to check it without a truth solve.\n", + "* **Coverage is a separate question from accuracy**, and it is answerable *ex ante*:\n", + " the ergodic distribution's tail exponent is pinned by patience primitives (a\n", + " Kesten process), so `ergodic_grid_diagnostics` flags pathological calibrations and\n", + " `ergodic_grid_report` certifies coverage — both passive, both advisory. For this\n", + " calibration coverage (~20) is satisfied far below the accuracy scale (~human\n", + " wealth), so accuracy binds.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/ConsumptionSaving/test_AggShock_pf_decay.py b/tests/ConsumptionSaving/test_AggShock_pf_decay.py new file mode 100644 index 000000000..693dfdc2a --- /dev/null +++ b/tests/ConsumptionSaving/test_AggShock_pf_decay.py @@ -0,0 +1,559 @@ +"""Tests for the opt-in perfect-foresight decay extrapolation machinery in +ConsAggShockModel (pf_mpc_min, pf_human_wealth_markov, make_cFunc_slice) and a +real-solve truth test showing the power-law tail beats the exponential one. +""" + +import unittest +import warnings + +import numpy as np + +from HARK.ConsumptionSaving.ConsAggShockModel import ( + AggShockConsumerType, + CobbDouglasEconomy, + make_cFunc_slice, + pf_human_wealth_markov, + pf_mpc_min, +) +from HARK.ConsumptionSaving.ConsIndShockModel import IndShockConsumerType +from HARK.ConsumptionSaving.pf_decay import PFDecayGridWarning, powerlaw_decay_params +from HARK.interpolation import LinearInterp +from tests.ConsumptionSaving.test_pf_decay import _params + + +class TestPFBoundHelpers(unittest.TestCase): + def test_pf_mpc_min_closed_form(self): + R, beta, rho, L = 1.03, 0.96, 2.0, 0.98 + expect = 1.0 - (R * beta * L) ** (1.0 / rho) / R + self.assertAlmostEqual(pf_mpc_min(R, beta, rho, L), expect, places=14) + self.assertGreater(pf_mpc_min(R, beta, rho, L), 0.0) + + def test_pf_mpc_min_warns_when_return_impatience_fails(self): + with self.assertWarns(UserWarning): + val = pf_mpc_min(1.0, 1.01, 2.0) + self.assertLessEqual(val, 0.0) + + def test_human_wealth_scalar_matches_analytic(self): + # Single state: h = (G/R)*E / (1 - G/R) = G*E/(R - G) + R, G, E = 1.03, 1.01, 1.0 + h = pf_human_wealth_markov(np.array([[1.0]]), R, np.array([E]), np.array([G])) + self.assertAlmostEqual(h[0], G * E / (R - G), places=10) + + def test_human_wealth_markov_joint_fixed_point(self): + # Two states, the second with ZERO income (deep unemployment): the + # joint solve must give it strictly positive human wealth (future + # re-employment), where an own-state recursion would degenerate to 0. + M = np.array([[0.9, 0.1], [0.2, 0.8]]) + R = 1.03 + E = np.array([1.0, 0.0]) + G = np.array([1.01, 1.01]) + h = pf_human_wealth_markov(M, R, E, G) + self.assertTrue(np.all(np.isfinite(h))) + self.assertGreater(h[1], 0.0) + self.assertGreater(h[0], h[1]) + # Non-circular check: h satisfies the defining fixed point + # h_i = sum_j M[i,j] * (G_j/R) * (E_j + h_j) + resid = h - (M * (G / R)[None, :]) @ (E + h) + np.testing.assert_allclose(resid, 0.0, atol=1e-12) + + def test_human_wealth_nan_when_fhwc_fails(self): + # G >= R in every state: infinite human wealth -> all-NaN + warning + M = np.array([[0.9, 0.1], [0.2, 0.8]]) + with self.assertWarns(UserWarning): + h = pf_human_wealth_markov( + M, 1.03, np.array([1.0, 1.0]), np.array([1.04, 1.04]) + ) + self.assertTrue(np.all(np.isnan(h))) + + +class TestMakeCFuncSlice(unittest.TestCase): + MPCmin = 0.05 + hNrm = 20.0 # PF line 0.05*(m + 20) = 1 + 0.05*m + + def line(self, m): + return self.MPCmin * (m + self.hNrm) + + def concave_below(self, m): + # strictly below the line, slope falling toward MPCmin from above + return self.line(m) - 2.0 * (m + self.hNrm) ** (-0.8) + + def test_legacy_when_bounds_missing(self): + m = np.linspace(0.0, 30.0, 40) + c = self.concave_below(m) + f = make_cFunc_slice(m, c) + self.assertIsInstance(f, LinearInterp) + self.assertFalse(f.decay_extrap) + f2 = make_cFunc_slice(m, c, 0.05, None) + self.assertFalse(f2.decay_extrap) + + def test_decay_attaches_powerlaw_by_default(self): + m = np.linspace(0.0, 30.0, 40) + c = self.concave_below(m) + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_form, "powerlaw") + self.assertEqual(f.slope_limit, self.MPCmin) + self.assertAlmostEqual(f.intercept_limit, self.MPCmin * self.hNrm, places=14) + # pivot = m_top + intercept/slope = m_top + hNrm + self.assertAlmostEqual(f.decay_extrap_pivot, 30.0 + self.hNrm, places=10) + + def test_decay_form_exp_override(self): + m = np.linspace(0.0, 30.0, 40) + c = self.concave_below(m) + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, decay_form="exp") + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_form, "exp") + + def test_concavity_guard_raises_on_impossible_input(self): + # Top knot ABOVE the PF line while the top slope has already fallen to + # MPCmin: impossible for a converged concave consumption function. + m = np.linspace(0.0, 30.0, 40) + c = self.line(m) + 0.1 + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm) + + def test_above_line_transient_falls_back_to_legacy(self): + # Above the line but with slope still well above MPCmin: an ordinary + # pre-asymptotic backward-induction transient -> legacy, no raise. + m = np.linspace(0.0, 30.0, 40) + c = self.line(m) + 0.1 + 0.2 * (m / 30.0) ** 2 + slope_top = (c[-1] - c[-2]) / (m[-1] - m[-2]) + self.assertGreater(slope_top, self.MPCmin + 1e-6) + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm) + self.assertFalse(f.decay_extrap) + + +class TestTruncatedGridTruthIndShock(unittest.TestCase): + """Solve a default-calibration infinite-horizon IndShock model on a DEEP + grid (the truth), truncate its consumption function at m=40, and compare + exponential vs power-law decay extrapolation against the truth above the + truncated grid. Measured baseline (this exact setup): the exponential has + destroyed ~100% of the true PF gap by m=2000 (it returns the PF line while + the truth is still 0.25% of c below it); the power law keeps ~82% of the + gap; max error vs truth is ~9x smaller (1.8e-3 vs 1.6e-2 relative). The + assertions below use ~2x safety margins on those measurements. + """ + + @classmethod + def setUpClass(cls): + agent = IndShockConsumerType( + cycles=0, aXtraMax=1.0e5, aXtraCount=96, aXtraNestFac=3 + ) + agent.solve() + sol = agent.solution[0] + cls.cT = sol.cFunc + cls.MPCmin = sol.MPCmin + cls.hNrm = sol.hNrm + cls.mMin = sol.mNrmMin + m_knots = np.linspace(cls.mMin + 0.05, 40.0, 300) + c_knots = cls.cT(m_knots) + cls.f_exp = LinearInterp( + m_knots, c_knots, cls.MPCmin * cls.hNrm, cls.MPCmin + ) + cls.f_pl = LinearInterp( + m_knots, + c_knots, + cls.MPCmin * cls.hNrm, + cls.MPCmin, + decay_extrap_form="powerlaw", + ) + cls.lad = np.geomspace(50.0, 2000.0, 25) + cls.c_true = cls.cT(cls.lad) + cls.gap_true = cls.MPCmin * (cls.lad + cls.hNrm) - cls.c_true + cls.err_exp = np.abs(cls.f_exp(cls.lad) - cls.c_true) + cls.err_pl = np.abs(cls.f_pl(cls.lad) - cls.c_true) + + def test_setup_is_in_the_decaying_configuration(self): + self.assertTrue(self.f_exp.decay_extrap) + self.assertTrue(self.f_pl.decay_extrap) + # the true gap at the ladder top is economically real, not float noise + self.assertGreater(self.gap_true[-1], 0.1) + # implied power-law exponent is a sane buffer-stock value + self.assertGreater(self.f_pl.decay_extrap_Q, 0.0) + self.assertLess(self.f_pl.decay_extrap_Q, 2.0) + + def test_powerlaw_beats_exponential_against_truth(self): + # max relative error: measured pl 1.8e-3 vs exp 1.6e-2 (9x) + self.assertLess( + np.max(self.err_pl / self.c_true), + 0.5 * np.max(self.err_exp / self.c_true), + ) + # pointwise: measured median ratio ~8.6x + self.assertGreater(np.median(self.err_exp / self.err_pl), 3.0) + + def test_exponential_destroys_the_gap_powerlaw_keeps_it(self): + # measured: err_exp/gap_true = 1.000 at m=2000 (exp -> the PF line), + # err_pl/gap_true = 0.18 + self.assertGreater(self.err_exp[-1] / self.gap_true[-1], 0.9) + self.assertLess(self.err_pl[-1] / self.gap_true[-1], 0.5) + + +class TestMakeCFuncSliceTheoryModes(unittest.TestCase): + """The decay_theory/decay_Q policy of make_cFunc_slice, mode by mode. + + Pre-registered semantics (the orchestrated default policy): + (1) decay_theory=None -> byte-identical prior PR behavior; + (2) decay_theory + default -> theory exponent min(1, q*), explicit, + level-matched; + (3) decay_theory + decay_Q=None -> fitted exponent clamped to min(1, q*) + (PFDecayGridWarning when the clamp + bites; inert otherwise); + (4) decay_Q=('amplitude', B) -> REMOVED (ValueError): level + continuity at the top knot is an + invariant (design ruling 2026-07-11); + (5) theory rescues the fitted form's no-decay fallback (slope_top <= + MPCmin with the knot below the line). + """ + + MPCmin = 0.05 + hNrm = 20.0 # PF line 0.05*(m + 20) = 1 + 0.05*m + + @classmethod + def setUpClass(cls): + cls.theory_lo = _params("HS") # q* = 0.3813 < 1 + cls.theory_hi = _params("CCAP") # q* = 1.4735 > 1, B_psi = 356.63 + assert 0.0 < cls.theory_lo.q_star < 1.0 + assert cls.theory_hi.q_star > 1.0 and cls.theory_hi.B_psi is not None + + def line(self, m): + return self.MPCmin * (m + self.hNrm) + + def concave_below(self, m, expo=0.8): + # strictly below the line, slope falling toward MPCmin from above; + # fitted exponent at the top knot is ~expo + return self.line(m) - 2.0 * (m + self.hNrm) ** (-expo) + + def knots(self, expo=0.8): + m = np.linspace(0.0, 30.0, 40) + return m, self.concave_below(m, expo) + + # ---- (1) decay_theory=None: byte-identical prior behavior ------------- + def test_no_theory_is_byte_identical_slicewise(self): + probe = np.concatenate([np.linspace(0.5, 29.5, 30), + np.geomspace(31.0, 3000.0, 25)]) + # healthy knot: prior behavior is the fitted powerlaw attach + m, c = self.knots() + f_new = make_cFunc_slice(m, c, self.MPCmin, self.hNrm) + f_old = LinearInterp(m, c, self.MPCmin * self.hNrm, self.MPCmin, + decay_extrap_form="powerlaw") + np.testing.assert_array_equal(f_new(probe), f_old(probe)) + self.assertFalse(hasattr(f_new, "decay_theory")) + # above-line transient: prior behavior is the bare fallback + c_above = self.line(m) + 0.1 + 0.2 * (m / 30.0) ** 2 + f_new = make_cFunc_slice(m, c_above, self.MPCmin, self.hNrm) + f_old = LinearInterp(m, c_above) + self.assertFalse(f_new.decay_extrap) + np.testing.assert_array_equal(f_new(probe), f_old(probe)) + # Carroll-Kimball impossible knot: still raises + with self.assertRaises(ValueError): + make_cFunc_slice(m, self.line(m) + 0.1, self.MPCmin, self.hNrm) + + # ---- (2) theory default: explicit min(1, q*) -------------------------- + def test_theory_default_attaches_theory_exponent(self): + m, c = self.knots() + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + self.assertEqual(f.decay_extrap_Q, self.theory_lo.q) + # level-matched at the top knot + eps = 1e-9 + lvl = float(f(np.array([m[-1] + eps]))[0]) + self.assertAlmostEqual(lvl, c[-1], places=7) + # metadata dict attached + meta = f.decay_theory + self.assertEqual(meta["Q_used"], self.theory_lo.q) + self.assertEqual(meta["q_star"], self.theory_lo.q_star) + self.assertAlmostEqual(meta["Q_fit"], 0.8, delta=0.05) + self.assertIsNone(meta["B_psi"]) + # q* > 1: the realized exponent is capped at 1 + f_hi = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_hi) + self.assertEqual(f_hi.decay_extrap_Q, 1.0) + + # ---- (3) guarded fit -------------------------------------------------- + def test_guarded_fit_inert_when_fit_below_ceiling(self): + # fitted exponent ~0.8 < ceiling 1.0 (q* > 1): the clamp must be inert + # and the attach identical to the plain fitted attach + m, c = self.knots() + probe = np.geomspace(31.0, 3000.0, 25) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_hi, decay_Q=None) + self.assertFalse( + any(issubclass(w.category, PFDecayGridWarning) for w in caught) + ) + self.assertEqual(f.decay_extrap_Q_source, "fitted") + f_plain = LinearInterp(m, c, self.MPCmin * self.hNrm, self.MPCmin, + decay_extrap_form="powerlaw") + np.testing.assert_array_equal(f(probe), f_plain(probe)) + self.assertAlmostEqual(f.decay_theory["Q_used"], f_plain.decay_extrap_Q, + places=14) + + def test_guarded_fit_clamps_and_warns(self): + m, c = self.knots() + # (a) ceiling = q* < 1 bites on a fitted exponent ~0.8 + with self.assertWarns(PFDecayGridWarning): + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo, decay_Q=None) + self.assertEqual(f.decay_extrap_Q, self.theory_lo.q) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + # (b) synthetic Q_fit ~1.5 > 1 knot: the Prop-A0 hard cap (ceiling 1.0 + # from the q* > 1 theory) bites + m2, c2 = self.knots(expo=1.5) + with self.assertWarns(PFDecayGridWarning): + f2 = make_cFunc_slice(m2, c2, self.MPCmin, self.hNrm, + decay_theory=self.theory_hi, decay_Q=None) + self.assertEqual(f2.decay_extrap_Q, 1.0) + + # ---- explicit float ---------------------------------------------------- + def test_explicit_float_passthrough(self): + m, c = self.knots() + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, decay_Q=0.6) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q, 0.6) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + self.assertFalse(hasattr(f, "decay_theory")) # no theory metadata + + # ---- (5) rescue of the no-decay fallback ------------------------------- + def test_rescue_where_fit_disables_decay(self): + # below the line but top slope BELOW MPCmin: the fitted form must fall + # back to naive-linear (no decay); theory attaches the explicit tail + m = np.linspace(0.0, 30.0, 40) + c = self.line(m) - 0.1 * (m + self.hNrm) ** 0.5 + slope_top = (c[-1] - c[-2]) / (m[-1] - m[-2]) + self.assertLess(slope_top, self.MPCmin) + f_legacy = make_cFunc_slice(m, c, self.MPCmin, self.hNrm) + self.assertFalse(f_legacy.decay_extrap) + f_theory = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo) + self.assertTrue(f_theory.decay_extrap) + self.assertEqual(f_theory.decay_extrap_Q, self.theory_lo.q) + # default rescue is the C1 two-term attachment: it extends the body + # SMOOTHLY (matching its still-widening gap at the knot: an interior + # gap maximum, not immediate decay) and only then decays to the line + self.assertEqual(f_theory.decay_extrap_terms, 2) + self.assertEqual(f_theory.decay_theory["terms"], 2) + slope_top = (c[-1] - c[-2]) / (m[-1] - m[-2]) + d_above = float(f_theory.derivative(np.array([m[-1] + 1e-11]))[0]) + self.assertLess(abs(d_above - slope_top), 1e-9) # C1, no kink + lad = np.geomspace(50.0, 5000.0, 20) + gap = self.line(lad) - f_theory(lad) + gap_top = self.line(m[-1]) - c[-1] + self.assertTrue(np.all(gap > 0.0)) + self.assertLess(gap[-1], gap_top) # eventually decays below the knot gap + gap_legacy = self.line(lad) - f_legacy(lad) + self.assertGreater(gap_legacy[-1], gap[-1]) + # the ONE-TERM rescue keeps its original registrations: immediate + # monotone decay from a level-matched knot (with the documented kink) + f_one = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo, decay_terms=1) + gap1 = self.line(lad) - f_one(lad) + self.assertTrue(np.all(gap1 > 0.0)) + self.assertTrue(np.all(np.diff(gap1) < 0.0)) + self.assertLess(gap1[0], gap_top) + # guarded fit rescues with the same ceiling exponent + f_guard = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo, decay_Q=None) + self.assertEqual(f_guard.decay_extrap_Q, self.theory_lo.q) + + # ---- (4) amplitude mode: REMOVED (continuity invariant) ----------------- + def test_amplitude_mode_removed_raises(self): + # Design ruling 2026-07-11: level continuity at the top knot is an + # INVARIANT of the decay machinery -- the former ('amplitude', B) + # mode's guarded level jump is never attachable. The tuple form now + # raises, pointing callers at decay_Q=1.0; that level-matched + # exponent-1 tail remains available and jump-free. + B0 = 25.0 + m = np.linspace(0.0, 30.0, 40) + c = self.line(m) - B0 / (m + self.hNrm) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_hi, + decay_Q=("amplitude", B0)) + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_hi, decay_Q=1.0) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + self.assertEqual(f.decay_extrap_Q, 1.0) + # level-matched (no jump): the tail limit at the top knot is the + # solved value there + lvl = float(f(np.array([m[-1] + 1e-9]))[0]) + self.assertAlmostEqual(lvl, c[-1], places=7) + + # ---- validation and refusal paths -------------------------------------- + def test_invalid_decay_Q_raises(self): + m, c = self.knots() + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, decay_Q="bogus") + # refuter finding (B3): decay_terms must be validated even on the + # legacy early-return path (MPCmin/hNrm None) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, None, None, decay_terms=37) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, decay_terms=True) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_Q=("amplitude", -1.0)) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_Q=("amplitude",)) + with self.assertRaises(ValueError): + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=self.theory_lo, decay_form="exp") + + def test_theory_with_nan_qstar_falls_back_to_guarded_fit(self): + # FHWC-violated theory: q* is nan, so the theory default degrades to + # the guarded fit with the GIC-free Prop-A0 ceiling 1.0 (inert here) + theory_nan = powerlaw_decay_params( + 1.0, 1.005, 0.98, 2.0, LivPrb=0.99, + PermShkDstn=None, TranShkDstn=(np.array([0.7, 1.3]), + np.array([0.5, 0.5])), + warn=False, + ) + self.assertTrue(np.isnan(theory_nan.q_star)) + m, c = self.knots() + f = make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=theory_nan) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q_source, "fitted") + self.assertTrue(np.isnan(f.decay_theory["q_star"])) + + def test_amplitude_ratio_logged_once_per_params_at_qstar_gt_1(self): + theory_fresh = _params("CCAP") # fresh object -> fresh log dedup key + m, c = self.knots() + with self.assertLogs("HARK.ConsumptionSaving.ConsAggShockModel", + level="INFO") as cm: + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=theory_fresh) + # second slice with the SAME params object: no second log line + make_cFunc_slice(m, c, self.MPCmin, self.hNrm, + decay_theory=theory_fresh) + ratio_lines = [r for r in cm.output if "amplitude ratio" in r] + self.assertEqual(len(ratio_lines), 1) + + +class TestSolverThreading(unittest.TestCase): + """decay_theory/decay_Q threaded through solveConsAggShock via time_inv_, + exactly like the PR threads MPCmin/hNrm. Includes an actual-solve identity + check: explicit decay_theory=None reproduces the default solve array-equal + (the cross-commit byte-identity vs the pre-change HEAD was verified on the + same probes during development; the healthy/fallback slice paths are pinned + byte-for-byte in TestMakeCFuncSliceTheoryModes).""" + + @classmethod + def _fresh_agent(cls, **attrs): + agent = AggShockConsumerType(seed=0, AgentCount=100, cycles=0) + economy = CobbDouglasEconomy(agents=[agent], seed=0, act_T=20, + max_loops=1) + economy.give_agent_params() + for k, v in attrs.items(): + setattr(agent, k, v) + agent.tolerance = 1e-4 + agent.solve() + return agent, economy + + @classmethod + def setUpClass(cls): + cls.probe_m = np.concatenate( + [np.linspace(0.1, 20.0, 30), np.geomspace(25.0, 5000.0, 30)] + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cls.agent_default, cls.eco = cls._fresh_agent() + cls.agent_explicit, _ = cls._fresh_agent( + decay_theory=None, decay_Q="theory" + ) + # PR opt-in bounds at the steady-state reference return + R = float(cls.eco.Rfunc(cls.eco.kSS)) + w = float(cls.eco.wFunc(cls.eco.kSS)) + d = cls.agent_default.IncShkDstn[0] + pmv = np.asarray(d.pmv) + E_inc = float(np.sum(pmv * np.asarray(d.atoms[0]) + * np.asarray(d.atoms[1]))) + G_tot = (float(np.asarray(cls.agent_default.PermGroFac).flat[0]) + * float(cls.agent_default.PermGroFacAgg)) + bounds = dict( + MPCmin=pf_mpc_min( + R, + float(cls.agent_default.DiscFac), + float(cls.agent_default.CRRA), + float(np.asarray(cls.agent_default.LivPrb).flat[0]), + ), + hNrm=float(pf_human_wealth_markov( + np.array([[1.0]]), R, np.array([w * E_inc]), + np.array([G_tot]))[0]), + ) + cls.agent_optin, _ = cls._fresh_agent(**bounds) + # theory params from the agent's idiosyncratic shock marginals + def row_marginal(row): + vals, inv = np.unique(np.asarray(d.atoms[row]), + return_inverse=True) + p = np.zeros(len(vals)) + np.add.at(p, inv, pmv) + return vals, p + + cls.theory = powerlaw_decay_params( + R, G_tot, + float(cls.agent_default.DiscFac), + float(cls.agent_default.CRRA), + LivPrb=float(np.asarray(cls.agent_default.LivPrb).flat[0]), + PermShkDstn=row_marginal(0), TranShkDstn=row_marginal(1), + warn=False, + ) + cls.agent_theory, _ = cls._fresh_agent( + decay_theory=cls.theory, **bounds + ) + + def _probe(self, agent): + M = np.full_like(self.probe_m, self.eco.MSS) + return agent.solution[0].cFunc(self.probe_m, M) + + def test_instance_defaults_exist(self): + agent = AggShockConsumerType(seed=0, AgentCount=10, cycles=0) + self.assertIsNone(agent.decay_theory) + self.assertEqual(agent.decay_Q, "theory") + self.assertIn("decay_theory", agent.time_inv_) + self.assertIn("decay_Q", agent.time_inv_) + + def test_explicit_none_identical_to_default(self): + np.testing.assert_array_equal( + self._probe(self.agent_default), self._probe(self.agent_explicit) + ) + + @staticmethod + def _slices(agent): + # LowerEnvelope2D -> VariableLowerBoundFunc2D -> LinearInterpOnInterp1D + return agent.solution[0].cFunc.functions[0].func.xInterpolators + + def test_theory_threading_reaches_solved_slices(self): + # theory is well-posed at this calibration (R_SS ~ 1.042 makes the + # default AggShock economy a q* > 1 calibration, so q = min(1, q*) = 1) + self.assertTrue(self.theory.valid) + self.assertGreater(self.theory.q_star, 1.0) + self.assertEqual(self.theory.q, 1.0) + self.assertIsNotNone(self.theory.B_psi) + # decay_theory travelled through time_inv_ into every solved slice: + # the metadata dict is attached with this exact params object's numbers + for s in self._slices(self.agent_theory): + self.assertTrue(hasattr(s, "decay_theory")) + self.assertEqual(s.decay_theory["q_star"], self.theory.q_star) + for s in self._slices(self.agent_optin): + self.assertFalse(hasattr(s, "decay_theory")) + + def test_theory_inert_when_slices_are_above_line_transients(self): + # At this GE calibration the kSS-reference PF line lies BELOW the + # solved top knots (above-line transients on the default shallow + # grid), so the tail policy must stay INERT: every slice falls back to + # the legacy bare extrapolation in BOTH agents and the solves agree + # exactly. (An engaged theory tail on a real solve is pinned at slice + # level in TestMakeCFuncSliceTheoryModes; picking a GE reference that + # puts the line above the knots is deliberately the caller's problem.) + for agent in (self.agent_optin, self.agent_theory): + for s in self._slices(agent): + self.assertFalse(s.decay_extrap) + c_optin = self._probe(self.agent_optin) + c_theory = self._probe(self.agent_theory) + self.assertTrue(np.all(np.isfinite(c_theory))) + np.testing.assert_array_equal(c_theory, c_optin) diff --git a/tests/ConsumptionSaving/test_pf_decay.py b/tests/ConsumptionSaving/test_pf_decay.py new file mode 100644 index 000000000..37976490e --- /dev/null +++ b/tests/ConsumptionSaving/test_pf_decay.py @@ -0,0 +1,1145 @@ +"""Tests for the power-law-decay theory utilities (HARK.ConsumptionSaving.pf_decay). + +Pre-registered tolerances (declared here BEFORE the assertions were first run; +never weakened). The absolute anchors are the theorem program's calibration +table (theory/powerlaw-decay/figures/FIGURES.md at HAFiscal-Latest @ 71ca7c61), +built from the estimated HAFiscal calibrations HS / CTOP (College top +discount-factor atom) / CCAP (College GIC-cap atom): + + q* = 0.3813 / 0.6942 / 1.4735 abs tol 5e-4 + B_psi = 356.63 (CCAP only) rel tol 5e-4 (0.05%) + lambda_B = 1.0096 / 1.0026 / 0.9978 abs tol 1e-3 (all near-resonance) + E[psi^2] = 1.002492 abs tol 1e-5 + zeta* = 9.19 / 3.17 / None rel tol 1% + E[ln A] = -0.0113 / -0.0039 / +0.0009 abs tol 1e-4 + +This makes the HAFiscal calibration table a permanent HARK regression anchor +for the theory utility. + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/figures/FIGURES.md :: Calibrations] +# Anchor provenance: the theorem program's calibration table (CAL-HS / +# CAL-CTOP / CAL-CCAP, built from the estimated HAFiscal parameters) is the +# source of the q*, B_psi, lambda_B and E[psi^2] targets above. + +# THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §6.1 How rarely is the tail visited? The dual (Kesten) root, for economists :: The reachability taxonomy :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] +# The zeta* and E[ln A] targets above are the measured reachability table: +# dual root 9.19 (HS) / 3.17 (College top atom) / none (GIC-cap atom, whose +# positive log-drift leaves mortality-with-replacement to truncate its tail). +""" + +import pickle +import unittest +import warnings + +import numpy as np +from scipy.stats import norm + +from HARK.ConsumptionSaving.ConsIndShockModel import ( + IndShockConsumerType, + PerfForesightConsumerType, +) +from HARK.ConsumptionSaving.pf_decay import ( + NearResonanceWarning, + NoDualRootWarning, + PFDecayConditionWarning, + ShockCorrelationWarning, + aXtraMax_from_tail_tol, + aXtraMax_from_wealth_mass, + dual_root, + mNrm_stable_points, + powerlaw_decay_params, + powerlaw_decay_params_from_agent, + powerlaw_tail_diagnostic, + powerlaw_validity_threshold, + qstar_probe, + rel_gap_at, + resonance_constants, +) +from HARK.distributions import DiscreteDistribution + +# ------------------------------------------------------------------ fixtures +# Discretizers are TEST FIXTURES, not shipped API: HARK users bring their own +# discretized distributions. These replicate the theorem program's 7-atom +# construction (equiprobable mean-one lognormal; unemployment atom with the +# employed atoms rescaled so the total mean stays 1). + + +def lognormal_equiprob(sigma_log, N): + if sigma_log <= 0 or N == 1: + return np.array([1.0]), np.array([1.0]) + u = (np.arange(N) + 0.5) / N + z = norm.ppf(u) + a = np.exp(sigma_log * z - 0.5 * sigma_log**2) + p = np.full(N, 1.0 / N) + a = a / (p * a).sum() + return a, p + + +def with_unemp_atom(sigma_log, N, unemp_prob, inc_unemp=0.0): + th_e, p_e = lognormal_equiprob(sigma_log, N) + scale = (1.0 - unemp_prob * inc_unemp) / (1.0 - unemp_prob) + th = np.concatenate(([inc_unemp], th_e * scale)) + p = np.concatenate(([unemp_prob], p_e * (1.0 - unemp_prob))) + return th, p + + +R0, RHO, LIV = 1.01, 2.0, 1.0 - 1.0 / 160.0 +PSI, PP = lognormal_equiprob(np.sqrt(0.003), 7) +TH_H, TP_H = with_unemp_atom(np.sqrt(0.12), 7, 0.044, 0.7) +TH_C, TP_C = with_unemp_atom(np.sqrt(0.12), 7, 0.027, 0.7) +G_H = 1.0 + 0.01812 / 4 +G_C = 1.0 + 0.01958 / 4 +BETA_HS = 0.98051 +BETA_CTOP = 0.98680 + (0.99640 - 0.98680) * 13.0 / 14.0 +BETA_CCAP = 1.005375 + +CALS = { + "HS": dict(G=G_H, beta=BETA_HS, th=TH_H, tp=TP_H), + "CTOP": dict(G=G_C, beta=BETA_CTOP, th=TH_C, tp=TP_C), + "CCAP": dict(G=G_C, beta=BETA_CCAP, th=TH_C, tp=TP_C), +} +TARGETS = { + "HS": dict(q=0.3813, lamB=1.0096, zeta=9.19, ElnA=-0.0113), + "CTOP": dict(q=0.6942, lamB=1.0026, zeta=3.17, ElnA=-0.0039), + "CCAP": dict(q=1.4735, lamB=0.9978, zeta=None, ElnA=+0.0009), +} +B_PSI_TARGET = 356.63 +E_PSI2_TARGET = 1.002492 + + +def _params(tag, warn=False): + c = CALS[tag] + return powerlaw_decay_params( + R0, c["G"], c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"]), warn=warn, + ) + + +class TestCalibrationAnchors(unittest.TestCase): + """FIGURES.md calibration table as a permanent regression anchor + (tolerances pre-registered in the module docstring above).""" + + @classmethod + def setUpClass(cls): + cls.res = {tag: _params(tag) for tag in CALS} + + def test_q_star(self): + for tag in CALS: + self.assertLessEqual( + abs(self.res[tag].q_star - TARGETS[tag]["q"]), 5e-4, tag + ) + + def test_realized_exponent_is_min_one_qstar(self): + self.assertAlmostEqual(self.res["HS"].q, self.res["HS"].q_star, places=14) + self.assertEqual(self.res["CCAP"].q, 1.0) + + def test_B_psi(self): + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/alt_proof_compactified.md :: Theorem γ-B (Stage-B boundary value) :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/alt-proof-compactified/] + # The closed-form boundary amplitude B_psi exists only at q* > 1; at the + # GIC-cap calibration B_psi = 356.63. At q* < 1 the Gordon denominator + # Rcal*Thorn_Gamma - E[psi^2] is negative and B_psi must be refused. + self.assertIsNotNone(self.res["CCAP"].B_psi) + self.assertLessEqual(abs(self.res["CCAP"].B_psi / B_PSI_TARGET - 1.0), 5e-4) + self.assertIsNone(self.res["HS"].B_psi) + self.assertIsNone(self.res["CTOP"].B_psi) + + def test_lambda_B_and_near_resonance(self): + for tag in CALS: + r = self.res[tag] + self.assertLessEqual(abs(r.lambda_B - TARGETS[tag]["lamB"]), 1e-3, tag) + self.assertTrue(r.near_resonance, tag) + self.assertTrue(any("NEAR-RESONANCE" in w for w in r.warnings), tag) + + def test_E_psi2(self): + for tag in CALS: + self.assertLessEqual( + abs(self.res[tag].E_psi2 - E_PSI2_TARGET), 1e-5, tag + ) + + def test_zeta_star(self): + for tag in ("HS", "CTOP"): + r = self.res[tag] + self.assertIsNotNone(r.zeta_star, tag) + self.assertLessEqual( + abs(r.zeta_star / TARGETS[tag]["zeta"] - 1.0), 0.01, tag + ) + self.assertEqual(r.dual_diagnosis, "ok", tag) + rK = self.res["CCAP"] + self.assertIsNone(rK.zeta_star) + self.assertIn("positive log-drift", rK.dual_diagnosis) + + def test_E_ln_A(self): + for tag in CALS: + self.assertLessEqual( + abs(self.res[tag].E_ln_A - TARGETS[tag]["ElnA"]), 1e-4, tag + ) + + def test_valid_flags(self): + # All three estimated calibrations satisfy GIC/RIC/FHWC — the GIC cap + # shaves beta to sit just BELOW the GIC boundary (Lambda > 0 but tiny; + # it is the DUAL root that fails at CCAP via positive log-drift, not GIC) + for tag in CALS: + self.assertTrue(self.res[tag].valid, tag) + self.assertGreater(self.res["CCAP"].Lambda, 0.0) + self.assertLess(self.res["CCAP"].Lambda, 1e-3) + + +class TestHConvention(unittest.TestCase): + """The two-human-wealth-conventions fact, pinned as tripwires. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/ADVERSARIAL_TESTING_GUIDE.md :: 5. LANDMINES — documented evaluation traps and silent-pass hazards :: The `h` human-wealth convention] + # Theorem h = 1/(Rcal-1) EXCLUDES current income = h_BST - 1 with + # h_BST = R/(R-Gamma). HARK's SOLVER-side hNrm (calc_human_wealth) matches + # the theorem convention (h*E_inc); bilt['hNrm'] from calc_limiting_values + # is BST-convention. These tests pin BOTH sides so a future "unification" + # of the two conventions trips loudly instead of silently moving one. + """ + + def test_h_equals_hBST_minus_one(self): + # pre-registered tol: rel 1e-14 (pure algebra in longdouble) + LD = np.longdouble + for tag, c in CALS.items(): + r = _params(tag) + Rcal = LD(R0) / LD(c["G"]) + h_BST = Rcal / (Rcal - 1) # BST convention: INCLUDES current income + dev = abs(float(LD(r.h) / (h_BST - 1)) - 1.0) + self.assertLessEqual(dev, 1e-14, tag) + + def test_solver_hNrm_excludes_current_income(self): + # cycles=1: exactly one period of future income, so the solver-side + # convention is directly observable: hNrm == (G/R)*E_inc (rel 1e-10). + agent = IndShockConsumerType(cycles=1) + agent.solve() + R = float(np.asarray(agent.Rfree).flat[0]) + G = float(np.asarray(agent.PermGroFac).flat[0]) + d0 = agent.IncShkDstn[0] + E_inc = float( + np.sum(np.asarray(d0.pmv) * np.asarray(d0.atoms[0]) * np.asarray(d0.atoms[1])) + ) + h1 = float(agent.solution[0].hNrm) + self.assertLessEqual(abs(h1 - (G / R) * E_inc), 1e-10 * max(1.0, h1)) + + def test_bilt_hNrm_is_the_OTHER_convention_tripwire(self): + # calc_limiting_values' bilt['hNrm'] must equal R/(R-G) — the BST + # convention that INCLUDES current income (PF E_inc = 1). If a future + # refactor "unifies" it with solution.hNrm (which excludes current + # income), this assertion trips and the theory utility's compute-from- + # primitives rule must be re-audited. + agent = PerfForesightConsumerType(cycles=0) + if not hasattr(agent, "bilt"): + agent.bilt = {} + agent.calc_limiting_values() + R = float(np.asarray(agent.Rfree).flat[0]) + G = float(np.asarray(agent.PermGroFac).flat[0]) + h_bilt = float(agent.bilt["hNrm"]) + h_bst = R / (R - G) + self.assertLessEqual(abs(h_bilt - h_bst), 1e-10 * h_bst) + # and it exceeds the solver/theorem convention by exactly E_inc = 1 + self.assertGreater(h_bilt, h_bst - 1.0 + 0.5) + + +class TestDegenerateAndConditions(unittest.TestCase): + def test_psi_equals_one_closed_form(self): + # Stage A: q* = ln(Rcal)/Lambda exactly (tol 1e-12 relative) + r = powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, + PermShkDstn=None, TranShkDstn=(TH_H, TP_H), warn=False, + ) + q_closed = np.log(r.Rcal) / r.Lambda + self.assertLessEqual(abs(r.q_star - q_closed), 1e-12 * max(1.0, q_closed)) + self.assertEqual(r.E_psi2, 1.0) + self.assertLessEqual(abs(r.sigma_B2 / r.Var_theta - 1.0), 1e-12) + self.assertIsNone(r.zeta_star) + self.assertEqual(r.P_A_gt_1, 0.0) + self.assertIn("compact", r.dual_diagnosis) + + def test_fhwc_refusal(self): + # FHWC violated (R <= G): clean refusal, no exception + with self.assertWarns(PFDecayConditionWarning): + r = powerlaw_decay_params( + 1.0, G_C, 0.98, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), + ) + self.assertFalse(r.FHWC) + self.assertFalse(r.valid) + self.assertTrue(np.isnan(r.h)) + self.assertIsNone(r.sigma_B2) + self.assertIsNone(r.B_psi) + self.assertIsNone(r.c_J) + self.assertTrue(np.isnan(r.q_star)) + self.assertIn("FHWC", r.diagnosis) + self.assertTrue(any("FHWC" in w for w in r.warnings)) + + def test_gic_refusal_psi_one(self): + # GIC violated with psi == 1: L(q) non-increasing, no (E)-root + with self.assertWarns(PFDecayConditionWarning): + r = powerlaw_decay_params( + R0, G_C, 1.008, RHO, LivPrb=LIV, + PermShkDstn=None, TranShkDstn=(TH_C, TP_C), + ) + self.assertFalse(r.GIC) + self.assertFalse(r.valid) + self.assertTrue(r.RIC) + self.assertTrue(np.isnan(r.q_star)) + self.assertNotEqual(r.diagnosis, "") + self.assertTrue(any("GIC" in w for w in r.warnings)) + + def test_gic_violated_wide_psi_still_reports_root(self): + # With a psi spread the convex L(q) can still cross zero even though + # GIC fails; the root is reported (with valid=False) and satisfies (E) + # to 1e-10. + r = powerlaw_decay_params( + R0, G_C, 1.008, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), warn=False, + ) + self.assertTrue(np.isfinite(r.q_star)) + self.assertGreater(r.q_star, 0.0) + psf, ppf = np.asarray(PSI, float), np.asarray(PP, float) + resid = abs( + float(np.dot(ppf, psf ** (1.0 + r.q_star))) + - r.Rcal * r.Thorn_Gamma**r.q_star + ) + self.assertLessEqual(resid, 1e-10) + self.assertFalse(r.valid) + + def test_ric_violation_warns(self): + with self.assertWarns(PFDecayConditionWarning): + r = powerlaw_decay_params( + 1.01, 1.0, 1.03, RHO, LivPrb=1.0, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), + ) + self.assertFalse(r.RIC) + self.assertLessEqual(r.kappa, 0.0) + self.assertFalse(r.valid) + self.assertTrue(any("RIC" in w for w in r.warnings)) + + def test_qstar_beyond_bracket_cap_is_finite_not_false_gic(self): + # Regression: a LEGITIMATE calibration (GIC/RIC/FHWC all hold) whose + # (E)-root exceeds the bracket-expansion cap. psi == 1 with Lambda ~ 1e-6 + # gives analytic q* = ln(Rcal)/Lambda ~ 4963 >> the historical 1024 cap. + # The old fixed cap returned nan + a FALSE 'GIC violated' diagnosis, which + # poisoned the realized exponent (should be min(1, q*) = 1) and dropped B_psi. + import math + R, G, rho = 1.01, 1.005, 1.0 # rho=1 => Thorn = R*beta_eff + Thorn_Gamma = math.exp(-1e-6) # Lambda = 1e-6, deep near-resonance + beta = (Thorn_Gamma * G) / R + r = powerlaw_decay_params( + R, G, beta, rho, LivPrb=1.0, + PermShkDstn=None, TranShkDstn=(TH_C, TP_C), warn=False, + ) + self.assertTrue(r.valid) # GIC, RIC, FHWC all hold + self.assertTrue(np.isfinite(r.q_star)) + self.assertAlmostEqual(r.q_star, math.log(R / G) / 1e-6, delta=1e-2) + self.assertGreater(r.q_star, 1024.0) # genuinely beyond the historical cap + self.assertEqual(r.q, 1.0) # realized exponent min(1, q*) = 1 + self.assertIsNotNone(r.B_psi) # q* > 1 and denom > 0 => B_psi defined + self.assertEqual(r.diagnosis, "") # no false condition claim + + def test_dual_root_beyond_bracket_cap_is_finite(self): + # Regression: a genuine Kesten root zeta* beyond the bracket cap. A single + # marginally-expanding psi atom (A = Thorn_Gamma/psi just above 1) with + # E[ln A] < 0 has a real but enormous Pareto exponent; the old fixed 1024 + # cap returned a false None ('bracket cap hit') for a tail that exists, and + # the inline 'not reachable for finite atoms' comment was factually wrong. + R, G, rho = 1.01, 1.0, 2.0 + Thorn_Gamma = 0.9999 + beta = (Thorn_Gamma * G) ** rho / R # Thorn = (R*beta)^(1/rho) = Thorn_Gamma*G + psi = np.array([0.9995, 1.0000102]) + pr = np.array([0.02, 0.98]) + psi = psi / float((pr * psi).sum()) # exact mean 1 + r = powerlaw_decay_params( + R, G, beta, rho, LivPrb=1.0, + PermShkDstn=(psi, pr), TranShkDstn=(TH_C, TP_C), warn=False, + ) + self.assertIsNotNone(r.zeta_star) + self.assertGreater(r.zeta_star, 1024.0) # genuinely beyond the historical cap + self.assertLess(r.E_ln_A, 0.0) # contracts on average + self.assertGreater(r.P_A_gt_1, 0.0) # occasionally expands + A = Thorn_Gamma / psi # satisfies E[(Thorn_Gamma/psi)^zeta] = 1 + self.assertAlmostEqual(float(np.dot(pr, A ** r.zeta_star)), 1.0, places=6) + + +class TestInputAcceptance(unittest.TestCase): + def test_hark_discrete_distribution_objects(self): + r_tuple = _params("HS") + psi_dd = DiscreteDistribution(np.asarray(PP, float), np.asarray(PSI, float)) + th_dd = DiscreteDistribution(np.asarray(TP_H, float), np.asarray(TH_H, float)) + r_dd = powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, + PermShkDstn=psi_dd, TranShkDstn=th_dd, warn=False, + ) + self.assertLessEqual(abs(r_dd.q_star / r_tuple.q_star - 1.0), 1e-12) + self.assertLessEqual(abs(r_dd.sigma_B2 / r_tuple.sigma_B2 - 1.0), 1e-12) + + def test_joint_equals_marginals(self): + r_tuple = _params("HS") + PSIJ, THJ = np.meshgrid( + np.asarray(PSI, float), np.asarray(TH_H, float), indexing="ij" + ) + PJ = np.outer(np.asarray(PP, float), np.asarray(TP_H, float)).ravel() + joint = (np.vstack([PSIJ.ravel(), THJ.ravel()]), PJ) + r_joint = powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, IncShkDstn=joint, warn=False + ) + self.assertLessEqual(abs(r_joint.q_star / r_tuple.q_star - 1.0), 1e-10) + self.assertLessEqual(abs(r_joint.sigma_B2 / r_tuple.sigma_B2 - 1.0), 1e-10) + + def test_time_varying_one_element_lists(self): + r_tuple = _params("HS") + r_list = powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, + PermShkDstn=[(PSI, PP)], TranShkDstn=[(TH_H, TP_H)], warn=False, + ) + self.assertEqual(r_list.q_star, r_tuple.q_star) + + def test_error_taxonomy(self): + joint = (np.vstack([PSI, PSI]), PP) + with self.assertRaises(ValueError): # both joint and marginals + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, PermShkDstn=(PSI, PP), IncShkDstn=joint + ) + with self.assertRaises(ValueError): # probs don't sum to 1 + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, PermShkDstn=(PSI, PP * 0.5), + TranShkDstn=(TH_H, TP_H), + ) + with self.assertRaises(ValueError): # negative probability + bad_p = PP.copy() + bad_p[0], bad_p[1] = -bad_p[1], bad_p[0] + 2 * bad_p[1] + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, PermShkDstn=(PSI, bad_p), + TranShkDstn=(TH_H, TP_H), + ) + with self.assertRaises(ValueError): # psi atom <= 0 + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, + PermShkDstn=(np.array([0.0, 2.0]), np.array([0.5, 0.5])), + TranShkDstn=(TH_H, TP_H), + ) + with self.assertRaises(ValueError): # joint atoms wrong shape + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, + IncShkDstn=(np.vstack([PSI, PSI, PSI]), PP), + ) + with self.assertRaises(ValueError): # non-positive scalar + powerlaw_decay_params(-1.0, G_H, BETA_HS, RHO) + + def test_correlated_joint_warns_independent_does_not(self): + # perfectly anticorrelated 2-atom joint: E[psi]=E[theta]=1 but + # E[psi*theta] = 0.99 != 1 + atoms = np.array([[0.9, 1.1], [1.1, 0.9]]) + with self.assertWarns(ShockCorrelationWarning): + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, + IncShkDstn=(atoms, np.array([0.5, 0.5])), + ) + # independent outer product must NOT warn about correlation + PSIJ, THJ = np.meshgrid( + np.asarray(PSI, float), np.asarray(TH_H, float), indexing="ij" + ) + PJ = np.outer(np.asarray(PP, float), np.asarray(TP_H, float)).ravel() + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + powerlaw_decay_params( + R0, G_H, BETA_HS, RHO, LivPrb=LIV, + IncShkDstn=(np.vstack([PSIJ.ravel(), THJ.ravel()]), PJ), + ) + self.assertFalse( + any(issubclass(w.category, ShockCorrelationWarning) for w in caught) + ) + + def test_frozen_and_picklable(self): + r = _params("HS") + with self.assertRaises(Exception): # frozen dataclass + r.q_star = 0.5 + r2 = pickle.loads(pickle.dumps(r)) + self.assertEqual(r2.q_star, r.q_star) + self.assertEqual(r2.warnings, r.warnings) + + +class TestWarningFilters(unittest.TestCase): + def test_near_resonance_warning_fires_and_is_filterable(self): + with self.assertWarns(NearResonanceWarning): + _params("HS", warn=True) + # production log hygiene: filtering the dedicated category silences it + # WITHOUT touching other UserWarnings + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + warnings.simplefilter("ignore", category=NearResonanceWarning) + r = powerlaw_decay_params( + R0, G_C, BETA_CCAP, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), + ) + cats = [w.category for w in caught] + self.assertFalse(any(issubclass(c, NearResonanceWarning) for c in cats)) + # CCAP's no-dual-root warning (a different category) still comes through + self.assertTrue(any(issubclass(c, NoDualRootWarning) for c in cats)) + # and the record on the result object is complete regardless + self.assertTrue(any("NEAR-RESONANCE" in w for w in r.warnings)) + + def test_warn_false_emits_nothing_but_records(self): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + r = _params("CCAP", warn=False) + self.assertEqual(len(caught), 0) + # CCAP records near-resonance + no-dual-root even with warn=False + self.assertTrue(any("NEAR-RESONANCE" in w for w in r.warnings)) + self.assertTrue(any("NO DUAL ROOT" in w for w in r.warnings)) + + +class TestFromAgent(unittest.TestCase): + def test_matches_direct_call(self): + agent = IndShockConsumerType(cycles=0) + r_agent = powerlaw_decay_params_from_agent(agent, warn=False) + d_perm, d_tran = agent.PermShkDstn[0], agent.TranShkDstn[0] + r_direct = powerlaw_decay_params( + np.asarray(agent.Rfree).flat[0], + np.asarray(agent.PermGroFac).flat[0], + agent.DiscFac, + agent.CRRA, + LivPrb=np.asarray(agent.LivPrb).flat[0], + PermShkDstn=d_perm, + TranShkDstn=d_tran, + warn=False, + ) + self.assertEqual(r_agent.q_star, r_direct.q_star) + self.assertEqual(r_agent.sigma_B2, r_direct.sigma_B2) + + def test_joint_fallback(self): + # an agent-like object with only a joint IncShkDstn (e.g. a hand-built + # income process) exercises the fallback path + PSIJ, THJ = np.meshgrid( + np.asarray(PSI, float), np.asarray(TH_H, float), indexing="ij" + ) + PJ = np.outer(np.asarray(PP, float), np.asarray(TP_H, float)).ravel() + + class _Stub: + Rfree = [R0] + PermGroFac = [G_H] + DiscFac = BETA_HS + CRRA = RHO + LivPrb = [LIV] + IncShkDstn = [(np.vstack([PSIJ.ravel(), THJ.ravel()]), PJ)] + + r_stub = powerlaw_decay_params_from_agent(_Stub(), warn=False) + r_marg = _params("HS") + self.assertLessEqual(abs(r_stub.q_star / r_marg.q_star - 1.0), 1e-10) + + +class TestResonanceHelper(unittest.TestCase): + def test_resonance_constants_at_root_found_beta(self): + """Root-find beta so q* = 1 exactly on the College fundamentals, then + check the theorem-program targets: C_B = 176.37 and cJ/Rcal = 1.1128 + (both rel tol 0.5%), plus the exact-resonance identity + C_B * Lprime(1) == cJ/Rcal (rel tol 1e-10).""" + + def qstar_of(beta): + return powerlaw_decay_params( + R0, G_C, beta, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), warn=False, + ).q_star + + lo, hi = 0.99, 1.01 + for _ in range(200): + mid = 0.5 * (lo + hi) + if mid == lo or mid == hi: + break + if qstar_of(mid) < 1.0: + lo = mid + else: + hi = mid + beta_res = 0.5 * (lo + hi) + self.assertLessEqual(abs(qstar_of(beta_res) - 1.0), 1e-8) + rc = resonance_constants( + R0, G_C, beta_res, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), warn=False, + ) + self.assertLessEqual(abs(rc["C_B"] / 176.37 - 1.0), 5e-3) + self.assertLessEqual(abs(rc["cJ_over_Rcal"] / 1.1128 - 1.0), 5e-3) + self.assertLessEqual( + abs(rc["C_B"] * rc["Lprime1"] / rc["cJ_over_Rcal"] - 1.0), 1e-10 + ) + self.assertLessEqual(rc["resonance_residual"], 1e-6) + + +class TestTailDiagnostic(unittest.TestCase): + """powerlaw_tail_diagnostic: the Theorem gamma-T compensated-flatness test + as a post-solve grid diagnostic. + + Pre-registered expectations (measured during development, thresholds from + the function's docstring, never tuned): + * fine real solve (aXtraMax=1e5, aXtraCount=512), window [3e3, 6e4]: + center slope(s=q) measured +0.046 -> CONFIRMED (flat_tol 0.08); + * coarse real solve (aXtraCount=96), window [2e3, 2e4]: interpolation + bias flattens the local exponent (center +0.23) -> PRE_ASYMPTOTIC; + the healthy-solve contract is verdict in {CONFIRMED, PRE_ASYMPTOTIC}, + NEVER INCONSISTENT (no false positive); + * beyond the solved grid the gap collapses below the float-cancellation + guard -> UNMEASURABLE; + * the h-convention trap (+E_inc on hNrm) turns the measured gap into + gap_true + kappa*E_inc. On SYNTHETIC theorem-form data with a window + where the constant dominates, the flat point sits near s=0, far from + q -> INCONSISTENT (the clean signature; pinned on synthetic data + because on a REAL solve at reachable windows the constant only + partially dominates and the poisoning mimics a transient -- + PRE_ASYMPTOTIC with a visibly degraded center, asserted below). + """ + + @classmethod + def setUpClass(cls): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cls.agent_fine = IndShockConsumerType( + cycles=0, aXtraMax=1.0e5, aXtraCount=512, aXtraNestFac=1 + ) + cls.agent_fine.solve() + cls.agent_coarse = IndShockConsumerType( + cycles=0, aXtraMax=1.0e5, aXtraCount=96, aXtraNestFac=3 + ) + cls.agent_coarse.solve() + cls.params = powerlaw_decay_params_from_agent(cls.agent_fine, warn=False) + cls.kappa = cls.params.kappa + cls.hN = cls.params.h * cls.params.E_inc # theorem-convention, primitives + cls.q = cls.params.q + + # ---------------- real-solve verdicts ---------------- + def test_confirmed_on_fine_real_solve(self): + d = powerlaw_tail_diagnostic( + self.agent_fine.solution[0].cFunc, self.kappa, self.hN, self.params, + m_lo=3e3, m_hi=6e4, + ) + self.assertEqual(d.verdict, "CONFIRMED") + self.assertEqual(d.n_points, 40) + # probe drifts carry the gamma-T signs: slope increasing through 0 at q + center = d.slopes[list(d.s_grid).index(self.q)] + self.assertLessEqual(abs(center), 0.08) + self.assertLess(d.slopes[0], d.slopes[-1]) + + def test_no_false_positive_on_coarse_real_solve(self): + d = powerlaw_tail_diagnostic( + self.agent_coarse.solution[0].cFunc, self.kappa, self.hN, + self.params, m_lo=2e3, m_hi=2e4, + ) + self.assertIn(d.verdict, ("CONFIRMED", "PRE_ASYMPTOTIC")) + + def test_unmeasurable_beyond_the_grid(self): + d = powerlaw_tail_diagnostic( + self.agent_fine.solution[0].cFunc, self.kappa, self.hN, self.params, + m_lo=1e6, m_hi=1e8, + ) + self.assertEqual(d.verdict, "UNMEASURABLE") + self.assertLess(d.n_points, 20) + + def test_wrong_hNrm_degrades_the_center_on_a_real_solve(self): + # +E_inc (the h_BST trap): within a reachable window the constant + # poisoning mimics a transient; the verdict must NOT be CONFIRMED and + # the center must move away from flat by the poisoning + cF = self.agent_fine.solution[0].cFunc + d_right = powerlaw_tail_diagnostic(cF, self.kappa, self.hN, self.params, + m_lo=3e3, m_hi=6e4) + d_wrong = powerlaw_tail_diagnostic( + cF, self.kappa, self.hN + self.params.E_inc, self.params, + m_lo=3e3, m_hi=6e4, + ) + self.assertEqual(d_right.verdict, "CONFIRMED") + self.assertNotEqual(d_wrong.verdict, "CONFIRMED") + c_right = d_right.slopes[list(d_right.s_grid).index(self.q)] + c_wrong = d_wrong.slopes[list(d_wrong.s_grid).index(self.q)] + self.assertGreater(c_wrong, c_right + 0.1) + + # ---------------- synthetic theorem-form verdict logic ---------------- + def _synthetic(self, expo, C=2.0): + kappa, hN = self.kappa, self.hN + + def cF(m): + x = m + hN + return kappa * x - C * x ** (-expo) + + return cF + + def test_confirmed_on_exact_theorem_form(self): + d = powerlaw_tail_diagnostic( + self._synthetic(self.q), self.kappa, self.hN, self.params, + m_lo=50.0, m_hi=5000.0, + ) + self.assertEqual(d.verdict, "CONFIRMED") + center = d.slopes[list(d.s_grid).index(self.q)] + self.assertLessEqual(abs(center), 1e-6) + + def test_pre_asymptotic_on_shallow_transient(self): + # local exponent q - 0.3 (below q): the theorem-backed transient side + d = powerlaw_tail_diagnostic( + self._synthetic(self.q - 0.3), self.kappa, self.hN, self.params, + m_lo=50.0, m_hi=5000.0, + ) + self.assertEqual(d.verdict, "PRE_ASYMPTOTIC") + + def test_inconsistent_on_steeper_than_floor(self): + # local exponent q + 0.6: steeper than min(1, q*) -- the Prop-A0 side + d = powerlaw_tail_diagnostic( + self._synthetic(self.q + 0.6), self.kappa, self.hN, self.params, + m_lo=50.0, m_hi=5000.0, + ) + self.assertEqual(d.verdict, "INCONSISTENT") + self.assertIn("STEEPER", d.notes) + + def test_inconsistent_in_steeper_deadband(self): + # Regression (docstring/code alignment): a local exponent in the band + # (q - 2*flat_tol, q - flat_tol), i.e. center in (-0.16, -0.08) at the + # default flat_tol=0.08. The old docstring implied INCONSISTENT only for + # center < -2*flat_tol, leaving this band undocumented; the code (and now + # the docstring) classify ANY steeper-than-CONFIRMED center as INCONSISTENT + # (Prop A0: no true transient decays faster than min(1, q*)). + d = powerlaw_tail_diagnostic( + self._synthetic(self.q + 0.12), self.kappa, self.hN, self.params, + m_lo=50.0, m_hi=5000.0, + ) + center = d.slopes[list(d.s_grid).index(self.q)] + self.assertLess(center, -0.08) # steeper than the CONFIRMED band + self.assertGreater(center, -0.16) # inside the old-docstring dead-band + self.assertEqual(d.verdict, "INCONSISTENT") + self.assertIn("STEEPER", d.notes) + + def test_inconsistent_on_h_convention_trap_synthetic(self): + # exact theorem-form data measured with hNrm + E_inc: the gap becomes + # gap_true + kappa*E_inc; on a window where the constant dominates the + # flat point sits near s = 0, far from q (q = 0.5923 > the 0.5 + # inconsistency_tol) -> INCONSISTENT, the trap detected + self.assertGreater(self.q, 0.5) # precondition for the far-flat-point + d = powerlaw_tail_diagnostic( + self._synthetic(self.q), self.kappa, self.hN + self.params.E_inc, + self.params, m_lo=1e4, m_hi=1e6, + ) + self.assertEqual(d.verdict, "INCONSISTENT") + self.assertIn("wrong-exponent signature", d.notes) + + # ---------------- refusal / plumbing paths ---------------- + def test_unmeasurable_when_qstar_nan(self): + bad = powerlaw_decay_params( + 1.0, G_C, 0.98, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(TH_C, TP_C), warn=False, + ) + d = powerlaw_tail_diagnostic( + self._synthetic(0.5), self.kappa, self.hN, bad, m_lo=50.0, m_hi=5e3 + ) + self.assertEqual(d.verdict, "UNMEASURABLE") + self.assertIn("q_star is nan", d.notes) + + def test_m_hi_discovery_and_requirement(self): + # bare-callable cFunc without x_list: m_hi is required + with self.assertRaises(ValueError): + powerlaw_tail_diagnostic( + self._synthetic(self.q), self.kappa, self.hN, self.params + ) + # a LinearInterp slice carries x_list: m_hi defaults to half its top + from HARK.interpolation import LinearInterp + + m = np.geomspace(1.0, 4.0e4, 400) + f = LinearInterp(m, self._synthetic(self.q)(m)) + d = powerlaw_tail_diagnostic(f, self.kappa, self.hN, self.params) + self.assertEqual(d.verdict, "CONFIRMED") + self.assertLessEqual(d.window[1], 0.5 * 4.0e4) + + def test_trial_offsets_must_include_zero(self): + with self.assertRaises(ValueError): + powerlaw_tail_diagnostic( + self._synthetic(self.q), self.kappa, self.hN, self.params, + m_lo=50.0, m_hi=5e3, trial_offsets=(-0.15, 0.15), + ) + + +class TestExtentCriterion(unittest.TestCase): + """aXtraMax_from_tail_tol + rel_gap_at: the certified grid-extent + criterion (the surviving deliverable of the grid-placement P1 program — + the placement scheme itself was KILLED by its pre-registered gates; see + theory/powerlaw-decay/grid_placement_p1_frontier_of_failure.md in the + HAFiscal-Latest repository).""" + + def test_inversion_reproduces_tolerance_on_synthetic_gap(self): + # pure power law: rel_gap(x) = rel_ref * (x/x_ref)^(-(1+q)) + q, hN, m_ref, rel_ref, tol = 0.4, 150.0, 40.0, 0.5, 1e-4 + m_top = aXtraMax_from_tail_tol(m_ref, rel_ref, q, hN, tol, safety=1.0) + rel_at_top = rel_ref * ((m_top + hN) / (m_ref + hN)) ** (-(1.0 + q)) + self.assertAlmostEqual(rel_at_top / tol, 1.0, places=10) + + def test_safety_extends_the_top(self): + args = (40.0, 0.5, 0.4, 150.0, 1e-4) + self.assertGreater( + aXtraMax_from_tail_tol(*args, safety=1.5), + aXtraMax_from_tail_tol(*args, safety=1.0), + ) + + def test_tol_floor_clamps_below_certifiable(self): + a = aXtraMax_from_tail_tol(40.0, 0.5, 0.4, 150.0, 1e-12) + b = aXtraMax_from_tail_tol(40.0, 0.5, 0.4, 150.0, 1e-6) + self.assertEqual(a, b) + + def test_fails_closed_on_bad_inputs(self): + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(40.0, -0.1, 0.4, 150.0, 1e-4))) + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(40.0, 0.5, 0.0, 150.0, 1e-4))) + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(np.nan, 0.5, 0.4, 150.0, 1e-4))) + + def test_rel_gap_at_measures_the_synthetic_gap(self): + kappa, hN = 0.02, 150.0 + gapfun = lambda m: 0.8 * ((m + hN) / hN) ** (-0.4) + cfun = lambda m: kappa * (np.asarray(m, float) + hN) - gapfun( + np.asarray(m, float)) + m = np.array([10.0, 100.0, 1000.0]) + got = rel_gap_at(cfun, m, kappa, hN) + want = gapfun(m) / cfun(m) + self.assertTrue(np.allclose(got, want, rtol=1e-12)) + # scalar in -> scalar out + self.assertIsInstance(rel_gap_at(cfun, 10.0, kappa, hN), float) + + def test_live_extent_is_conservative_on_the_HS_anchor(self): + # Deeper reference points imply SMALLER certified tops than shallow + # ones inverted with the same q_eff would (the pre-asymptotic local + # exponent rises with depth), and the inversion is monotone in tol. + params = _params("HS") + hN = params.h * params.E_inc + top4 = aXtraMax_from_tail_tol(40.0, 0.75, params.q, hN, 1e-4) + top3 = aXtraMax_from_tail_tol(40.0, 0.75, params.q, hN, 1e-3) + self.assertGreater(top4, top3) + self.assertGreater(top3, 40.0) + + # --- review-hardening additions (2026-07-10 adversarial pass) --- + + def test_rel_gap_at_empty_input_returns_empty(self): + out = rel_gap_at(lambda m: np.asarray(m, float), np.array([]), 0.02, + 150.0) + self.assertIsInstance(out, np.ndarray) + self.assertEqual(out.size, 0) + + def test_bad_safety_fails_closed(self): + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(40.0, 0.5, 0.4, 150.0, 1e-4, safety=-1.5))) + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(40.0, 0.5, 0.4, 150.0, 1e-4, safety=0.0))) + self.assertTrue(np.isnan( + aXtraMax_from_tail_tol(40.0, 0.5, 0.4, 150.0, 1e-4, + safety=np.nan))) + + def test_closed_form_route_guarantees_by_identity(self): + # q* > 1: x_top = sqrt(B_psi/(MPCmin*tol)) makes B_psi/(MPCmin*x^2) + # equal tol exactly; monotone in tol; live CCAP ex-post measured + # 6.9e-5 at tol 1e-4 (see the frontier-of-failure record). + params = _params("CCAP") + hN = params.h * params.E_inc + top = aXtraMax_from_tail_tol(np.nan, np.nan, np.nan, hN, 1e-4, + B_psi=params.B_psi, MPCmin=params.kappa) + x = top + hN + self.assertAlmostEqual(params.B_psi / (params.kappa * x * x) / 1e-4, + 1.0, places=10) + top6 = aXtraMax_from_tail_tol(np.nan, np.nan, np.nan, hN, 1e-6, + B_psi=params.B_psi, MPCmin=params.kappa) + self.assertGreater(top6, top) + # closed-form route with a q* < 1 calibration (B_psi None) -> nan + self.assertTrue(np.isnan(aXtraMax_from_tail_tol( + np.nan, np.nan, np.nan, hN, 1e-4, B_psi=_params("HS").B_psi, + MPCmin=params.kappa)) or _params("HS").B_psi is None) + + +class TestQstarProbe(unittest.TestCase): + """The operator eigen-probe: numerical q* from the model's own one-period + backward step (no eigen-equation). Pre-registered: |q_hat - q*| <= 5e-4 + per anchor (measured 5.6e-6..5.0e-5), depth-consistency <= 1e-4 + (measured ~1e-6).""" + + def test_matches_analytic_root_on_anchors(self): + for tag in ("HS", "CTOP", "CCAP"): + c = CALS[tag] + q_true = _params(tag).q_star + q_hat, cons, diag = qstar_probe( + R0, c["G"], c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"])) + self.assertEqual(diag, "ok", tag) + self.assertLessEqual(abs(q_hat - q_true), 5e-4, tag) + self.assertLessEqual(cons, 1e-4, tag) + + def test_custom_one_step_hook_reproduces_primitives_mode(self): + # portability contract: a caller-supplied backward step + PF limits + c = CALS["HS"] + params = _params("HS") + hN = params.h * params.E_inc + PSIj, THj = np.meshgrid(PSI, c["th"], indexing="ij") + WPj = np.outer(PP, c["tp"]).ravel() + WPj = WPj / WPj.sum() + psi_j, th_j = PSIj.ravel(), THj.ravel() + + def my_step(c_trial, a): + m_img = (R0 / (c["G"] * psi_j))[None, :] * a[:, None] \ + + th_j[None, :] + rhs = c["beta"] * LIV * R0 * ( + WPj[None, :] * (c["G"] * psi_j[None, :]) ** (-RHO) + * c_trial(m_img) ** (-RHO)).sum(1) + return rhs ** (-1.0 / RHO) + + q_a, _, diag_a = qstar_probe( + R0, c["G"], c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"])) + q_b, _, diag_b = qstar_probe( + R0, c["G"], c["beta"], RHO, LivPrb=LIV, + one_step=my_step, MPCmin=params.kappa, hNrm=hN) + self.assertEqual((diag_a, diag_b), ("ok", "ok")) + self.assertLessEqual(abs(q_a - q_b), 1e-6) + + def test_fails_closed_without_pf_asymptote(self): + # FHWC violated: Rcal <= 1 -> h nan -> nan + reason, no exception + c = CALS["HS"] + q_hat, cons, diag = qstar_probe( + 1.0, 1.02, c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"])) + self.assertTrue(np.isnan(q_hat)) + self.assertIn("asymptote unavailable", diag) + + +class TestStablePoints(unittest.TestCase): + """mNrm_stable_points: the two classical loci + the mortality-adjusted + (R -> LivPrb*R) twins, with end-of-period-asset images.""" + + @classmethod + def setUpClass(cls): + cls.c = CALS["HS"] + cls.params = _params("HS") + hE = cls.params.h * cls.params.E_inc + kap = cls.params.kappa + # synthetic concave cFunc below its PF line with a power-law gap + cls.cf = staticmethod( + lambda m: kap * (np.asarray(m, float) + hE) + - 0.9 * (np.asarray(m, float) + hE) ** (-0.38) * hE ** 0.76) + cls.sp = mNrm_stable_points( + cls.cf, R0, cls.c["G"], LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(cls.c["th"], cls.c["tp"])) + + def test_roots_satisfy_their_defining_loci(self): + sp, c = self.sp, self.c + R, G, L = R0, c["G"], LIV + Eth, Eip = sp.E_theta, sp.E_inv_psi + loci = { + sp.mNrmTrg: lambda m: m - (m - Eth) / ((R / G) * Eip), + sp.mNrmStE: lambda m: m - (m - Eth) * G / R, + sp.mNrmTrg_mort: lambda m: m - (m - Eth) / ((L * R / G) * Eip), + sp.mNrmStE_mort: lambda m: m - (m - Eth) * G / (L * R), + } + for m_hat, locus in loci.items(): + self.assertTrue(np.isfinite(m_hat)) + resid = abs(float(self.cf(np.array([m_hat]))[0]) - locus(m_hat)) + self.assertLessEqual(resid, 1e-7 * max(1.0, m_hat)) + + def test_orderings_and_a_images(self): + sp = self.sp + # Jensen (E[1/psi] > 1): StE below Trg; mortality shave lowers both + self.assertLess(sp.mNrmStE, sp.mNrmTrg) + self.assertLess(sp.mNrmTrg_mort, sp.mNrmTrg) + self.assertLess(sp.mNrmStE_mort, sp.mNrmStE) + # a-images are m - c(m) (the grid-relevant coordinates) + self.assertAlmostEqual( + sp.aNrmTrg, + sp.mNrmTrg - float(self.cf(np.array([sp.mNrmTrg]))[0]), places=10) + + def test_LivPrb_one_degenerates_to_unadjusted(self): + sp1 = mNrm_stable_points( + self.cf, R0, self.c["G"], LivPrb=1.0, + PermShkDstn=(PSI, PP), TranShkDstn=(self.c["th"], self.c["tp"])) + self.assertAlmostEqual(sp1.mNrmTrg, sp1.mNrmTrg_mort, places=12) + self.assertAlmostEqual(sp1.mNrmStE, sp1.mNrmStE_mort, places=12) + + def test_nan_when_no_crossing(self): + sp = mNrm_stable_points( + lambda m: np.full_like(np.asarray(m, float), 0.35), + R0, self.c["G"], LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(self.c["th"], self.c["tp"])) + self.assertTrue(np.isnan(sp.mNrmTrg)) + self.assertTrue(np.isnan(sp.mNrmStE)) + + +class TestZetaL(unittest.TestCase): + """dual_root(..., LivPrb): the mortality-augmented dual root.""" + + def test_livprb_one_is_byte_compatible_none_at_cap(self): + c = CALS["CCAP"] + params = _params("CCAP") + z, ElnA, PA, diag = dual_root(PSI, PP, params.Thorn_Gamma) + self.assertIsNone(z) + self.assertIn("positive log-drift", diag) + + def test_mortality_root_exists_at_the_cap(self): + # the pure-GIC case: no Kesten root, but the mortality-augmented one + # exists via the expanding branch (measured ~1.92 at the anchor) + params = _params("CCAP") + z, _, _, diag = dual_root(PSI, PP, params.Thorn_Gamma, LivPrb=LIV) + self.assertIsNone(diag) + self.assertGreater(z, 1.2) + self.assertLess(z, 3.0) + + def test_mortality_thins_the_tail(self): + params = _params("HS") + z1, _, _, _ = dual_root(PSI, PP, params.Thorn_Gamma) + zL, _, _, _ = dual_root(PSI, PP, params.Thorn_Gamma, LivPrb=LIV) + self.assertGreater(zL, z1) + + def test_no_expanding_branch_stays_rootless(self): + # psi == 1 under GIC: compact support; mortality cannot create a tail + params = _params("HS") + z, _, _, diag = dual_root(np.array([1.0]), np.array([1.0]), + params.Thorn_Gamma, LivPrb=LIV) + self.assertIsNone(z) + self.assertIn("no expanding branch", diag) + + +class TestWealthMassRule(unittest.TestCase): + """aXtraMax_from_wealth_mass on a synthetic solved cFunc (solve-free).""" + + @classmethod + def setUpClass(cls): + cls.c = CALS["HS"] + params = _params("HS") + hE = params.h * params.E_inc + kap = params.kappa + # harmonic blend: ~m at small m, ~kappa*(m+hE) at large m; smooth, + # 0 < c < min(m, line) so a = m - c > 0 and the gap is positive + cls.cf = staticmethod( + lambda m: 1.0 / (1.0 / np.maximum(np.asarray(m, float), 1e-12) + + 1.0 / (kap * (np.asarray(m, float) + hE)))) + cls.a_max, cls.info = aXtraMax_from_wealth_mass( + cls.cf, R0, cls.c["G"], cls.c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(cls.c["th"], cls.c["tp"]), + eps_wealth=1e-4, probe_count=384) + + def test_returns_finite_adequate_top(self): + self.assertTrue(np.isfinite(self.a_max)) + self.assertGreater(self.a_max, self.info.anchor_a) + self.assertTrue(self.info.cover_adequate) + self.assertEqual(self.info.diagnosis, "ok") + + def test_dial_monotonicities(self): + t = self.info.quantile_table + for meas in ("agent", "wealth"): + self.assertLessEqual(t[(meas, 1e-2)], t[(meas, 1e-3)]) + self.assertLessEqual(t[(meas, 1e-3)], t[(meas, 1e-4)]) + for e in (1e-2, 1e-3, 1e-4): + self.assertGreaterEqual(t[("wealth", e)], t[("agent", e)]) + + def test_deterministic(self): + a2, info2 = aXtraMax_from_wealth_mass( + self.cf, R0, self.c["G"], self.c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(self.c["th"], self.c["tp"]), + eps_wealth=1e-4, probe_count=384) + self.assertEqual(self.a_max, a2) + + def test_wealth_measure_refuses_when_aggregate_wealth_unbounded(self): + # cap-atom primitives with near-unit survival: zeta_L <= 1 + c = CALS["CCAP"] + a_max, info = aXtraMax_from_wealth_mass( + self.cf, R0, c["G"], c["beta"], RHO, LivPrb=0.9999, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"]), + eps_wealth=1e-4, probe_count=384) + self.assertTrue(np.isnan(a_max)) + self.assertIn("REFUSED", info.diagnosis) + + def test_never_raises_on_degenerate_primitives(self): + a_max, info = aXtraMax_from_wealth_mass( + self.cf, 1.0, 1.02, 0.96, RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(self.c["th"], self.c["tp"]), + probe_count=256) + self.assertTrue(isinstance(info.diagnosis, str) + and len(info.diagnosis) > 0) + + +class TestValidityThreshold(unittest.TestCase): + """pf_decay.powerlaw_validity_threshold: the proofs' explicit + guaranteed-validity floor wbar0 (stage_A_proof.md (5.0a)/(5.0)).""" + + def test_hand_computed_transitory_only(self): + # a clean psi == 1 calibration whose x0^0 is recomputed here from + # scratch (plain float arithmetic), independent of the module internals. + R, G, beta, rho = 1.04, 1.0, 0.90, 2.0 + th = ([0.0, 1.25], [0.2, 0.8]) # E[theta]=1, theta_min=0, theta_max=1.25 + res = powerlaw_validity_threshold(R, G, beta, rho, TranShkDstn=th, + warn=False) + + Rcal = R / G + Thorn = (R * beta) ** (1.0 / rho) + Thorn_R = Thorn / R + Thorn_G = Thorn / G + kappa = 1.0 - Thorn_R + h = 1.0 / (Rcal - 1.0) + gbar = kappa * h + mbar = kappa * h / Thorn_R + theta_min, theta_max = 0.0, 1.25 + C_plus = (theta_max - 1.0) + Rcal * gbar + C0 = max(1.0 - theta_min, C_plus) + zeta = C0 / (1.0 - Thorn_G) + tbar = 1.0 / (8.0 * (rho + 1.0)) + c2 = (rho * (rho + 1.0) / 2.0) * (1.0 - tbar) ** (-(rho + 2.0)) + terms = [ + h + mbar, + (8.0 * (rho + 1.0) * gbar / kappa + C0) / Thorn_G, + 8.0 * (rho + 1.0) * C0 / Thorn_G, + (h + 1.0 + C0) / Thorn_G, + 2.0 * zeta, + ] + x00 = max(terms) + K_L = c2 * Thorn_G * gbar / (rho * Rcal * kappa) + + # the composed constants match the independent recomputation + self.assertAlmostEqual(res.gbar, gbar, places=12) + self.assertAlmostEqual(res.mbar, mbar, places=12) + self.assertAlmostEqual(res.C0, C0, places=12) + self.assertAlmostEqual(res.zeta, zeta, places=10) + self.assertAlmostEqual(res.c2, c2, places=12) + self.assertAlmostEqual(res.K_L, K_L, places=9) + self.assertAlmostEqual(res.wbar0_hat_free, x00, places=6) + # explicit hand-computed anchor (this calibration -> 622.306271...) + self.assertAlmostEqual(res.wbar0_hat_free, 622.306271137, places=6) + # the gbar/kappa term binds (statement.md: "tens of h"), and the full + # floor is never below the K_hat-free one + self.assertEqual( + res.binding_term, "(8(rho+1)*gbar/kappa + C0)/Thorn_Gamma" + ) + self.assertGreaterEqual(res.wbar0, res.wbar0_hat_free) + self.assertTrue(res.valid and res.diagnosis == "") + + def test_full_floor_uses_two_Khat_when_it_dominates(self): + # wbar0 = max(x0^0, 2*K_hat); the binding entry is reported honestly. + res = powerlaw_validity_threshold(1.04, 1.0, 0.90, 2.0, + TranShkDstn=([0.0, 1.25], [0.2, 0.8]), + warn=False) + self.assertAlmostEqual( + res.wbar0, max(res.wbar0_hat_free, 2.0 * res.K_hat), places=9 + ) + self.assertIn(res.binding_term, res.terms) + self.assertAlmostEqual( + res.wbar0, res.terms[res.binding_term], places=9 + ) + + def test_is_a_diagnostic_not_a_refusal_gate(self): + # FHWC violated (Rcal = R/G < 1): returns nan + diagnosis, never raises. + res = powerlaw_validity_threshold(1.0, 1.04, 0.96, 2.0, + TranShkDstn=([0.0, 1.25], [0.2, 0.8]), + warn=False) + self.assertFalse(res.valid) + self.assertTrue(np.isnan(res.wbar0)) + self.assertTrue(np.isnan(res.wbar0_hat_free)) + self.assertIn("FHWC", res.diagnosis) + + def test_psi_general_scale_is_tens_of_h(self): + # on an estimated (psi-general) College calibration the guaranteed floor + # is finite, positive, and of order tens of h, dominated by the + # gbar/kappa term (statement.md Remark 7: crude but explicit). + c = CALS["CTOP"] + th = _params("CTOP") # shares the module's patience objects + res = powerlaw_validity_threshold( + R0, c["G"], c["beta"], RHO, LivPrb=LIV, + PermShkDstn=(PSI, PP), TranShkDstn=(c["th"], c["tp"]), warn=False + ) + self.assertTrue(res.valid) + self.assertTrue(np.isfinite(res.wbar0) and res.wbar0 > 0) + self.assertGreater(res.wbar0_hat_free, 10.0 * th.h) + self.assertLess(res.wbar0_hat_free, 100.0 * th.h) + self.assertEqual( + res.binding_term, "(8(rho+1)*gbar/kappa + C0)/Thorn_Gamma" + ) diff --git a/tests/ConsumptionSaving/test_powerlaw_extrap.py b/tests/ConsumptionSaving/test_powerlaw_extrap.py new file mode 100644 index 000000000..74bc1b53b --- /dev/null +++ b/tests/ConsumptionSaving/test_powerlaw_extrap.py @@ -0,0 +1,1336 @@ +"""Tests for the optional theory-pinned tail extrapolation of IndShockConsumerType +(``decay_extrap_form`` / ``decay_extrap_form_lower``) and its supporting pieces +(``KappaBarTailInterp``, ``pf_decay.ce_psi_regime``, ``pf_decay.aXtraMin_from_tail_tol``). + +This file PORTS the theorem program's extrapolator-fidelity protocol +(``theory/powerlaw-decay/verify_extrap_fidelity_checks.py`` at +BufferStockTheory-Latest @ 12b0b178) into HARK's pytest: + +* NESTED GRIDS: the truth solve uses a wide dense log a-grid; small solves use + the strict subset ``grid[k:-k]`` (subset identity asserted), so the removed + windows at each end measure exactly what the tails must recover. +* TRUTH = the tails-in-solve solve on the pinned grid (the battery's + pre-registered truth definition); NON-CIRCULARITY is audited by rails-only + solves on grids EXTENDED past the comparison window (G-AUD ports), whose + agreement with the truth on the window is both gated and used as the + measurement FLOOR below which no fidelity ratio is gated (the battery-v2 + floor discipline). +* THE TWO ROLES: in-solve tails (the option) versus tails attached to a + default solve's cFunc post hoc (eval-only). The in-solve variant must be + strictly better -- the expectation channel is where the value lives. + +Pre-registered gates (declared BEFORE first CI run; may be tightened, never +loosened). Values were MEASURED at authoring time (2026-07-14, linux/x86-64 +float64). The ERROR gates carry >= 21x headroom above their measured values +and must sit ABOVE the printed cross-solve audit floor; the two-roles FACTOR +gates are minimum improvement factors with smaller margins over their +measured factors (CE bottom: gate 10 vs measured ~353x = 35x margin; HS top: +gate 1.3 vs measured 2.42x = 1.9x margin; HS bottom: gate 3 vs measured +~19.6x = 6.5x margin): + + CE bottom sup rel-err (tails, k=160) gate 1e-8 measured 4.77e-11 (audit floor 2.53e-11) + CE top sup rel-err (tails, k=160) gate 1e-6 measured 2.42e-08 + CE in-grid contamination (tails) gate 1e-9 measured 8.40e-12 (rails 1.64e-08) + CE MPC at smallest removed node gate 1e-6 measured ~9e-12 rel dev from MPCmax + CE two-roles factor (bottom) gate >= 10 measured ~353x + HS top sup rel-err (tails, k=150) gate 1e-6 measured 2.28e-08 (audit floor 2.96e-10) + HS bottom sup rel-err (tails, psi-general regime I) + gate 2e-4 measured 9.51e-06 (rails 1.55e-03) + HS MPC at smallest removed excess node gate 1e-5 measured 4.47e-07 + HS two-roles factor (top) gate >= 1.3 measured ~2.4x + HS two-roles factor (bottom) gate >= 3 measured ~19.6x + ladder monotonicity (both calibrations): shallower trim never worse + +# THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] +# Bottom gates: c = kap_bar*me - K*me**(1+rho), MPC -> kap_bar as me -> 0 +# (q_down = rho, no root-finding at the constraint end). +# THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/statement.md :: st-thm-CE-psi :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/statement/] +# The psi-general bottom (CAL-HS block) is theorem-backed in regime I only: +# lambda(psi_min) = p_eff**(1/rho)*Thorn_Gamma/psi_min < 1; the regime-gate +# tests construct a regime-II process and assert the warning + refusal +# (st-rem-CE-regime). +# THEOREM-REF[BufferStockTheory-Latest @ 12b0b178 :: theory/powerlaw-decay/final_proof_myst.md :: eq-powerlaw :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] +# Top gates: the gap below MPCmin*(m + hNrm) decays as wbar**(-min(1, q*)). +# The two-roles finding and fig7/fig8 evidence: +# https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/extrapolators-in-practice + +Byte-zero regression: with the options at their ``None`` defaults the solve +must be byte-for-byte the parent code path. The pinned probe values below were +generated at the parent commit df508470 (the theory-pin retarget commit, the +tip this PR stacks on). On the pin-generation platform (linux/x86-64 float64) +they must match EXACTLY (float equality, no tolerance); on other platforms +libm last-ulp differences shift the solved values legitimately, so the pin +comparison degrades to ``np.allclose(rtol=1e-9)`` there -- the platform-FREE +byte-zero guarantee is the in-process explicit-None-equals-stock test, which +stays exact everywhere. +""" + +import os +import platform +import sys +import unittest +import warnings + +import numpy as np + +from HARK.ConsumptionSaving.ConsIndShockModel import ( + IndShockConsumerType, + solve_one_period_ConsIndShock, + solve_one_period_ConsIndShock_with_tails, +) +from HARK.ConsumptionSaving.pf_decay import ( + ConstraintEndRegimeWarning, + aXtraMin_from_tail_tol, + ce_psi_regime, + ergodic_grid_diagnostics, + ergodic_grid_diagnostics_from_agent, + ergodic_grid_report, + powerlaw_decay_params_from_agent, +) +from HARK.distributions import DiscreteDistributionLabeled +from HARK.interpolation import ( + DecayTailInterp, + KappaBarTailInterp, + LinearInterp, +) + +# --------------------------------------------------------------- calibrations +# CE-rho2 (bottom-primary; psi == 1 via PermShkStd=0, zero-income unemployment +# atom): the theorem program's verify_constraint_end_checks NB-rho2 calibration +# in HARK parameters. kap_bar = 1 - 0.05**0.5*Thorn_R = 0.785165538... +CE_PARS = dict( + cycles=0, + T_cycle=1, + CRRA=2.0, + Rfree=[1.04], + DiscFac=0.96, + LivPrb=[1.0], + PermGroFac=[1.0], + BoroCnstArt=None, + vFuncBool=False, + CubicBool=False, + UnempPrb=0.05, + IncUnemp=0.0, + TranShkStd=[0.1], + TranShkCount=7, + PermShkStd=[0.0], + PermShkCount=1, + T_retire=0, + UnempPrbRet=0.0, + IncUnempRet=0.0, +) +# CAL-HS (top-primary; psi-general): the estimated high-school calibration of +# the theorem program's figure scripts, on HARK's own discretization (hence +# q_star = 0.3759 here vs 0.3813 on the reference stack's atom grid). Its +# worst JOINT atom is the lowest employed-income atom (theta_min < IncUnemp), +# giving lambda(psi_min) deep in regime I (statement.md st-rem-CE-regime). +HS_PARS = dict( + cycles=0, + T_cycle=1, + CRRA=2.0, + Rfree=[1.01], + DiscFac=0.98051, + LivPrb=[1.0 - 1.0 / 160.0], + PermGroFac=[1.0 + 0.01812 / 4], + BoroCnstArt=None, + vFuncBool=False, + CubicBool=False, + UnempPrb=0.044, + IncUnemp=0.7, + TranShkStd=[np.sqrt(0.12)], + TranShkCount=7, + PermShkStd=[np.sqrt(0.003)], + PermShkCount=7, + T_retire=0, + UnempPrbRet=0.0, + IncUnempRet=0.0, +) + +# nested-grid design (runtime-budgeted port of the battery's Na=6000 ladder) +CE_GRID = dict(aMin=1e-6, aMax=1e5, Na=800) # ~72 nodes/decade +CE_K = 160 # ~2.2 decades trimmed per end +CE_K_SHALLOW = 80 +HS_GRID = dict(aMin=1e-4, aMax=1e8, Na=900) # ~75 nodes/decade +HS_K = 150 # 2.0 decades trimmed per end +HS_K_SHALLOW = 75 +SOLVE_TOL = 1e-12 +AUDIT_TOL = 1e-11 # the HS audit grid is ~2 decades longer; 1e-12 would hit +# core.solve_agent's max_cycles=5000 escape hatch + +# pre-registered gates (see module docstring for the measured values) +GATE_CE_BOTTOM = 1e-8 +GATE_CE_TOP = 1e-6 +GATE_CE_CONTAM = 1e-9 +GATE_CE_MPC = 1e-6 +GATE_CE_TWO_ROLES = 10.0 +GATE_CE_AUDIT = 1e-9 # G-AUD1 port: deep-bottom rails vs truth on window +GATE_HS_TOP = 1e-6 +GATE_HS_BOTTOM = 2e-4 +GATE_HS_MPC = 1e-5 +GATE_HS_TWO_ROLES_TOP = 1.3 # measured 2.42x on linux/x86-64; lowered from +# the 1.5 authoring strawman for platform robustness (the factor gates +# carry far less headroom than the error gates -- module docstring) +GATE_HS_TWO_ROLES_BOTTOM = 3.0 +GATE_HS_AUDIT = 1e-8 # G-AUD3 port: tall-top rails vs truth on window + +# Byte-zero pins, generated at parent commit df508470 (see gen note in the +# module docstring). Probes: m = [0.5, 1, 2, 5, 10, 20, 40]. +BYTE_ZERO_PROBES = np.array([0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 40.0]) +# config A: bone-stock IndShockConsumerType(cycles=0) +BYTE_ZERO_PIN_A = [ + 0.5, + 0.8652278427790324, + 1.098045258173503, + 1.3730629141572253, + 1.6897273979915504, + 2.234807386872702, + 3.2691485935849434, +] +# config B: cycles=0, CubicBool=True, vFuncBool=True, BoroCnstArt=None +BYTE_ZERO_PIN_B = [ + 1.0194785800743766, + 1.08444079226619, + 1.1836154886611796, + 1.405144565610918, + 1.7072978107134946, + 2.2460708318213296, + 3.269100840528629, +] +BYTE_ZERO_PIN_B_VFUNC_AT_5 = -14.103921502643747 + + +# ------------------------------------------------------------------- helpers +def log_grid(aMin, aMax, Na): + return np.exp(np.linspace(np.log(aMin), np.log(aMax), Na)) + + +def solve_agent(base_pars, grid, tol, **opts): + pars = dict(base_pars) + pars.update(opts) + agent = IndShockConsumerType(**pars) + agent.verbose = 0 + if grid is not None: + agent.aXtraGrid = np.asarray(grid, float) + agent.tolerance = tol + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent.solve() + return agent + + +def unwrap_m_nodes(cfunc): + """The EGM m-node grid of an assembled cFunc (corner included), through + any tail wrappers and the LowerEnvelope.""" + f = cfunc + while True: + if isinstance(f, (KappaBarTailInterp, DecayTailInterp)): + f = f.interp + elif hasattr(f, "functions"): + f = f.functions[0] + elif hasattr(f, "x_list"): + return np.asarray(f.x_list, float) + else: # pragma: no cover - would signal an assembly change + raise TypeError(type(f)) + + +def sup_rel_err(cfunc_variant, cfunc_truth, probes): + cv = np.asarray(cfunc_variant(probes), float) + ct = np.asarray(cfunc_truth(probes), float) + return float(np.max(np.abs(cv - ct) / ct)) + + +def attach_eval_only(solution, q_eff, CRRA, do_top=True, do_bottom=True): + """The 'tails-eval-only' variant: wrap a default (rails) solution's cFunc + post hoc with the SAME tail laws the in-solve option uses (two-term top, + strict kappa_bar bottom). Isolates role 2 (post-solve evaluation) from + role 1 (in-solve expectations).""" + m = unwrap_m_nodes(solution.cFunc) + f = solution.cFunc + if do_top: + f = DecayTailInterp( + f, + solution.MPCmin * solution.hNrm, + solution.MPCmin, + x_cut=float(m[-1]), + decay_extrap_Q=q_eff, + ) + if do_bottom: + # HARK's default bottom is the EGM secant, which lies BELOW the true + # concave cFunc, so the eval-only knot has K > 0 and strict=True holds + # (unlike the reference stack's c = m rail, which needed the corridor). + f = KappaBarTailInterp( + f, + solution.MPCmax, + CRRA, + solution.mNrmMin, + x_knot=float(m[1]), + strict=True, + ) + return f + + +_CACHE = {} + + +def ce_bundle(): + """All CE-rho2 solves, computed once and shared across tests.""" + if "CE" in _CACHE: + return _CACHE["CE"] + grid = log_grid(**CE_GRID) + tails = dict(decay_extrap_form="powerlaw", decay_extrap_form_lower="kappabar") + truth = solve_agent(CE_PARS, grid, SOLVE_TOL, **tails) + # G-AUD1 port: rails-only, bottom extended 4 decades at equal density + npd = (CE_GRID["Na"] - 1) / np.log10(CE_GRID["aMax"] / CE_GRID["aMin"]) + grid_aud = log_grid( + CE_GRID["aMin"] * 1e-4, CE_GRID["aMax"], CE_GRID["Na"] + int(4 * npd) + ) + audit = solve_agent(CE_PARS, grid_aud, SOLVE_TOL) + out = dict(grid=grid, truth=truth, audit=audit) + for k in (CE_K, CE_K_SHALLOW): + sub = grid[k:-k] + out[("rails", k)] = solve_agent(CE_PARS, sub, SOLVE_TOL) + out[("tails", k)] = solve_agent(CE_PARS, sub, SOLVE_TOL, **tails) + _CACHE["CE"] = out + return out + + +def hs_bundle(): + """All CAL-HS solves, computed once and shared across tests.""" + if "HS" in _CACHE: + return _CACHE["HS"] + grid = log_grid(**HS_GRID) + tails = dict(decay_extrap_form="powerlaw", decay_extrap_form_lower="kappabar") + truth = solve_agent(HS_PARS, grid, SOLVE_TOL, **tails) + # G-AUD3 port: rails-only, top extended 2 decades at equal density + npd = (HS_GRID["Na"] - 1) / np.log10(HS_GRID["aMax"] / HS_GRID["aMin"]) + grid_aud = log_grid( + HS_GRID["aMin"], HS_GRID["aMax"] * 1e2, HS_GRID["Na"] + int(2 * npd) + ) + audit = solve_agent(HS_PARS, grid_aud, AUDIT_TOL) + out = dict(grid=grid, truth=truth, audit=audit) + for k in (HS_K,): + sub = grid[k:-k] + out[("rails", k)] = solve_agent(HS_PARS, sub, SOLVE_TOL) + out[("tails", k)] = solve_agent(HS_PARS, sub, SOLVE_TOL, **tails) + out[("tails", HS_K_SHALLOW)] = solve_agent( + HS_PARS, grid[HS_K_SHALLOW:-HS_K_SHALLOW], SOLVE_TOL, **tails + ) + _CACHE["HS"] = out + return out + + +def bottom_window(truth_sol, small_sol, n=60): + """Log-spaced probes in the removed-bottom window, in EXCESS coordinates + (correct for psi-general mNrmMin < 0), strictly between the two solves' + first EGM nodes.""" + m1_t = float(unwrap_m_nodes(truth_sol.cFunc)[1]) + m1_s = float(unwrap_m_nodes(small_sol.cFunc)[1]) + mm = max(truth_sol.mNrmMin, small_sol.mNrmMin) + me_lo = (m1_t - truth_sol.mNrmMin) * 1.05 + me_hi = (m1_s - small_sol.mNrmMin) * 0.95 + return mm + np.exp(np.linspace(np.log(me_lo), np.log(me_hi), n)) + + +def top_window(truth_sol, small_sol, n=80): + m_top_t = float(unwrap_m_nodes(truth_sol.cFunc)[-1]) + m_top_s = float(unwrap_m_nodes(small_sol.cFunc)[-1]) + return np.exp(np.linspace(np.log(m_top_s * 1.05), np.log(m_top_t * 0.95), n)) + + +def interior_window(truth_sol, small_sol, n=200): + mm = max(truth_sol.mNrmMin, small_sol.mNrmMin) + m1 = max( + float(unwrap_m_nodes(truth_sol.cFunc)[1]), + float(unwrap_m_nodes(small_sol.cFunc)[1]), + ) + m_top_s = float(unwrap_m_nodes(small_sol.cFunc)[-1]) + return mm + np.exp( + np.linspace(np.log((m1 - mm) * 2.0), np.log((m_top_s - mm) / 2.0), n) + ) + + +def make_joint_dstn(psi_atoms, psi_probs, th_atoms, th_probs): + """Hand-built joint IncShkDstn (independent product measure), labeled the + way the solver consumes it.""" + PSI, TH = np.meshgrid(psi_atoms, th_atoms, indexing="ij") + P = np.outer(psi_probs, th_probs) + atoms = np.vstack([PSI.ravel(), TH.ravel()]) + return DiscreteDistributionLabeled( + P.ravel(), atoms, var_names=["PermShk", "TranShk"] + ) + + +# ============================================================ unit: the class +class TestKappaBarTailInterpUnit(unittest.TestCase): + """Hand-computed probes and guard trips for the new interpolant (mirrors + the reference library's self-test (ii)/(iv) cases).""" + + def body(self): + return LinearInterp(np.array([0.5, 1.0]), np.array([0.35, 0.7])) + + def test_hand_computed_tail_values_and_delegation(self): + # kap_bar=0.8, CRRA=2, knot (0.5, 0.35) => K = (0.4-0.35)/0.125 = 0.4 + f = KappaBarTailInterp(self.body(), 0.8, 2.0, 0.0, x_knot=0.5) + self.assertAlmostEqual(f.K, 0.4, places=14) + got = np.asarray(f(np.array([0.25, 0.1])), float) + np.testing.assert_allclose(got, [0.19375, 0.0796], rtol=1e-15) + # at/above the knot: delegated to the body + np.testing.assert_allclose( + np.asarray(f(np.array([0.5, 0.75])), float), [0.35, 0.525], rtol=1e-15 + ) + # level continuity at the knot (the class invariant) + eps = 1e-12 + self.assertAlmostEqual(float(f(0.5 - eps)), 0.35, places=10) + # explicit y_knot route agrees with the body-read route + f2 = KappaBarTailInterp(self.body(), 0.8, 2.0, 0.0, x_knot=0.5, y_knot=0.35) + self.assertEqual(float(f2(0.25)), float(f(0.25))) + + def test_mnrmmin_shift_and_constraint_zero(self): + body = LinearInterp(np.array([-0.5, 0.0]), np.array([0.35, 0.7])) + f = KappaBarTailInterp(body, 0.8, 2.0, -1.0, x_knot=-0.5) + self.assertAlmostEqual(f.K, 0.4, places=14) + self.assertAlmostEqual(float(f(-0.75)), 0.19375, places=15) + # at/below the constraint: consumption 0, MPC -> MPCmax + self.assertEqual(float(f(-1.0)), 0.0) + self.assertEqual(float(f(-2.0)), 0.0) + self.assertAlmostEqual(float(f.derivative(-1.0)), 0.8, places=15) + + def test_derivative_vs_central_fd_and_limit(self): + f = KappaBarTailInterp(self.body(), 0.8, 2.0, 0.0, x_knot=0.5) + for m in (0.35, 0.2, 0.1): + h = 1e-7 + fd = (float(f(m + h)) - float(f(m - h))) / (2 * h) + self.assertAlmostEqual(float(f.derivative(m)) / fd, 1.0, places=6) + # MPC -> MPCmax from below as me -> 0 (Theorem CE's content) + self.assertAlmostEqual(float(f.derivative(1e-12)), 0.8, places=9) + self.assertLessEqual(float(f.derivative(0.25)), 0.8) + + def test_guards_strict_corridor_and_exposure_gate(self): + body = LinearInterp(np.array([1.0, 2.0]), np.array([0.9, 1.8])) + # K < 0 (knot above the kap_bar line): strict raises with the + # st-cor-C4 grid-rule message; the corridor constructor admits it. + with self.assertRaises(ValueError) as cm: + KappaBarTailInterp(body, 0.8, 2.0, 0.0, x_knot=1.0) + self.assertIn("st-cor-C4", str(cm.exception)) + f = KappaBarTailInterp(body, 0.8, 2.0, 0.0, x_knot=1.0, strict=False) + self.assertFalse(f.in_regime) + self.assertAlmostEqual(f.K, -0.1, places=14) + # ... but try_make refuses it (MPC would exceed MPCmax throughout) + self.assertIsNone(KappaBarTailInterp.try_make(body, 0.8, 2.0, 0.0, x_knot=1.0)) + # outside even the corridor: both non-strict construction and try_make + body_far = LinearInterp(np.array([1.0, 2.0]), np.array([1.7, 3.4])) + with self.assertRaises(ValueError): + KappaBarTailInterp(body_far, 0.8, 2.0, 0.0, x_knot=1.0, strict=False) + self.assertIsNone( + KappaBarTailInterp.try_make(body_far, 0.8, 2.0, 0.0, x_knot=1.0) + ) + # negative-MPC leak of the raw corridor (recorded v2 item): K in + # regime (strict constructs) but (1+CRRA)*K*me**CRRA >= MPCmax, so the + # exposure gate refuses the in-solve tail. + body_neg = LinearInterp(np.array([1.0, 2.0]), np.array([0.3, 0.6])) + g = KappaBarTailInterp(body_neg, 0.8, 2.0, 0.0, x_knot=1.0) + self.assertTrue(g.in_regime) + self.assertLess(g.mpc_at_knot, 0.0) + self.assertIsNone( + KappaBarTailInterp.try_make(body_neg, 0.8, 2.0, 0.0, x_knot=1.0) + ) + # a good knot passes try_make and matches the direct construction + ok = KappaBarTailInterp.try_make(self.body(), 0.8, 2.0, 0.0, x_knot=0.5) + self.assertIsNotNone(ok) + self.assertTrue(ok.in_regime) + self.assertGreater(ok.mpc_at_knot, 0.0) + # malformed scalars fail closed (None), never raise + self.assertIsNone( + KappaBarTailInterp.try_make(self.body(), np.nan, 2.0, 0.0, x_knot=0.5) + ) + self.assertIsNone( + KappaBarTailInterp.try_make(self.body(), 0.8, 2.0, 0.6, x_knot=0.5) + ) # knot below mNrmMin + + +# ==================================================== unit: pf_decay additions +class TestCePsiRegime(unittest.TestCase): + def test_regime_I_on_ce_process(self): + agent = IndShockConsumerType(**CE_PARS) + PG = float((1.04 * 0.96) ** 0.5) # Thorn_Gamma at G=1, LivPrb=1 + reg = ce_psi_regime(agent.IncShkDstn[0], 2.0, PG) + self.assertEqual(reg["regime"], "I") + self.assertEqual(reg["anchor"], "st-thm-CE-psi") + self.assertAlmostEqual(reg["p_eff"], 0.05, places=12) + # hand value: 0.05**0.5 * PG / 1.0 + self.assertAlmostEqual(reg["lambda_min_fiber"], 0.05**0.5 * PG, places=12) + + def test_regime_II_on_fat_low_fiber(self): + psi = np.array([0.12, 1.1552941176470588]) # E[psi] = 1 + pp = np.array([0.15, 0.85]) + th = np.array([0.0, 1.0 / 0.7]) # E[theta] = 1 + tp = np.array([0.3, 0.7]) + joint = make_joint_dstn(psi, pp, th, tp) + PG = float((1.04 * 0.96) ** 0.5) + reg = ce_psi_regime(joint, 2.0, PG) + self.assertEqual(reg["regime"], "II") + self.assertEqual(reg["anchor"], "st-rem-CE-regime") + # zero transitory atom => p_eff = full worst-theta mass 0.3 + self.assertAlmostEqual(reg["p_eff"], 0.3, places=12) + self.assertAlmostEqual(reg["lambda_min_fiber"], 0.3**0.5 * PG / 0.12, places=10) + + def test_worst_joint_mass_fiber_selection(self): + # xi_min > 0: the worst JOINT atom is (psi_min, theta_min), so p_eff + # is the fiber-selected p_w * P[psi = psi_min] (st-def-ce-psi-objects) + psi = np.array([0.9, 1.1]) + pp = np.array([0.5, 0.5]) + th = np.array([0.5, 1.5]) + tp = np.array([0.5, 0.5]) + joint = make_joint_dstn(psi, pp, th, tp) + reg = ce_psi_regime(joint, 2.0, 0.99) + self.assertAlmostEqual(reg["p_eff"], 0.25, places=12) + self.assertAlmostEqual(reg["psi_min"], 0.9, places=12) + + +class TestAXtraMinFromTailTol(unittest.TestCase): + def test_hand_inversion(self): + # K = (0.8*0.5 - 0.35)/0.5**3 = 0.4 (up to float rounding); + # me_target = (tol*0.8/K)**(1/2)/1.5; aXtraMin = (1-0.8)*me_target + a = aXtraMin_from_tail_tol(0.5, 0.35, 0.8, 2.0, 1e-4) + K = (0.8 * 0.5 - 0.35) / 0.5**3 + me_target = (1e-4 * 0.8 / K) ** 0.5 / 1.5 + self.assertAlmostEqual(a, (1.0 - 0.8) * me_target, places=15) + # explicit-K route bypasses the measurement (equal up to the last-ulp + # difference between the measured K and the literal 0.4) + a2 = aXtraMin_from_tail_tol(None, None, 0.8, 2.0, 1e-4, K=0.4) + self.assertAlmostEqual(a, a2, places=15) + + def test_fail_closed(self): + # reference node ON/ABOVE the kap_bar line: K <= 0 -> nan (st-cor-C4) + self.assertTrue(np.isnan(aXtraMin_from_tail_tol(0.5, 0.45, 0.8, 2.0, 1e-4))) + self.assertTrue(np.isnan(aXtraMin_from_tail_tol(0.5, 0.35, 1.2, 2.0, 1e-4))) + self.assertTrue(np.isnan(aXtraMin_from_tail_tol(0.5, 0.35, 0.8, 2.0, np.nan))) + # tail_tol clamped at the float64-certifiable floor 1e-6 + self.assertEqual( + aXtraMin_from_tail_tol(0.5, 0.35, 0.8, 2.0, 1e-9), + aXtraMin_from_tail_tol(0.5, 0.35, 0.8, 2.0, 1e-6), + ) + + def test_expost_certificate_on_ce_solve(self): + """End-to-end: invert a coarse CE solve's bottom knot to an aXtraMin + for tail_tol, re-solve, and verify the certificate binds.""" + tail_tol = 1e-3 + coarse = solve_agent(CE_PARS, log_grid(1e-2, 1e4, 200), 1e-10) + sol = coarse.solution[0] + m1 = float(unwrap_m_nodes(sol.cFunc)[1]) + me1 = m1 - sol.mNrmMin + c1 = float(sol.cFunc(m1)) + a_min = aXtraMin_from_tail_tol(me1, c1, sol.MPCmax, 2.0, tail_tol) + self.assertTrue(np.isfinite(a_min) and a_min > 0.0) + fine = solve_agent(CE_PARS, log_grid(a_min, 1e4, 300), 1e-10) + fsol = fine.solution[0] + fm1 = float(unwrap_m_nodes(fsol.cFunc)[1]) + fme1 = fm1 - fsol.mNrmMin + fc1 = float(fsol.cFunc(fm1)) + measured = (fsol.MPCmax - fc1 / fme1) / fsol.MPCmax + self.assertLessEqual( + measured, + tail_tol, + "ex-post certificate failed: measured %.3e > " + "tail_tol %.1e" % (measured, tail_tol), + ) + + +# ================================================== option surface + byte-zero +class TestOptionSurface(unittest.TestCase): + def test_defaults_are_none(self): + agent = IndShockConsumerType(**CE_PARS) + self.assertIsNone(agent.decay_extrap_form) + self.assertIsNone(agent.decay_extrap_Q) + self.assertIsNone(agent.decay_extrap_form_lower) + + def test_auto_exponent_is_min_one_qstar(self): + ce = ce_bundle() + agent = ce[("tails", CE_K)] + params = powerlaw_decay_params_from_agent(agent, warn=False) + self.assertEqual(agent.decay_extrap_Q, min(1.0, params.q_star)) + self.assertEqual(agent.decay_extrap_Q, 1.0) # CE: q_star ~ 49 + hs = hs_bundle() + agent_hs = hs[("tails", HS_K)] + params_hs = powerlaw_decay_params_from_agent(agent_hs, warn=False) + self.assertEqual(agent_hs.decay_extrap_Q, min(1.0, params_hs.q_star)) + self.assertLess(agent_hs.decay_extrap_Q, 1.0) # HS: q_star ~ 0.376 + + def test_user_Q_after_auto_solve_is_respected(self): + """An explicit decay_extrap_Q assigned AFTER an auto-computed solve + must not be clobbered by the next solve's auto refresh (which applies + only while Q still equals the remembered auto value).""" + grid = log_grid(1e-4, 1e3, 100) + + def fresh(): + a = IndShockConsumerType(**CE_PARS) + a.verbose = 0 + a.aXtraGrid = grid + a.tolerance = 1e-8 + a.decay_extrap_form = "powerlaw" + return a + + agent = fresh() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent.solve() + self.assertEqual(agent.decay_extrap_Q, 1.0) # CE auto: min(1, ~49) + agent.decay_extrap_Q = 0.9 # user override AFTER the auto solve + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent.solve() + self.assertEqual(agent.decay_extrap_Q, 0.9) # respected, not reset + # a still-auto value keeps refreshing across solves... + agent2 = fresh() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent2.solve() + agent2.solve() + self.assertEqual(agent2.decay_extrap_Q, 1.0) + # ...and disabling the form clears a still-auto Q (no stale exponent) + agent2.decay_extrap_form = None + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent2.solve() + self.assertIsNone(agent2.decay_extrap_Q) + + def test_vpfunc_carries_the_tails(self): + """The role-1 mechanism: vPfunc = u'(cFunc(m')) is what the PREVIOUS + backward step's Euler expectation evaluates (calc_vp_next), so the + wrapped cFunc must be what vPfunc wraps -- one wiring point, both + roles.""" + ce = ce_bundle() + sol = ce[("tails", CE_K)].solution[0] + self.assertIsInstance(sol.cFunc, KappaBarTailInterp) + self.assertIsInstance(sol.vPfunc.cFunc, KappaBarTailInterp) + + def test_option_validation(self): + agent = IndShockConsumerType(**CE_PARS) + agent.verbose = 0 + agent.decay_extrap_form = "bogus" + with self.assertRaises(ValueError): + agent.solve() + agent2 = IndShockConsumerType(**CE_PARS) + agent2.verbose = 0 + agent2.decay_extrap_Q = 0.5 # without decay_extrap_form='powerlaw' + with self.assertRaises(ValueError): + agent2.solve() + + def test_solver_requires_explicit_Q(self): + agent = IndShockConsumerType(**CE_PARS) + agent.verbose = 0 + agent.solve() # default solve, to get a valid solution_next + with self.assertRaises(ValueError): + solve_one_period_ConsIndShock_with_tails( + agent.solution[0], + agent.IncShkDstn[0], + 1.0, + 0.96, + 2.0, + 1.04, + 1.0, + None, + agent.aXtraGrid, + False, + False, + decay_extrap_form="powerlaw", # decay_extrap_Q missing + ) + + def test_public_solver_signature_unchanged(self): + """The long-standing public solver must keep its EXACT argument + names: solve_one_cycle builds the solver's argument dict from those + names, so growing the shared signature would break agents whose + re-solve paths skip pre_solve (e.g. the HANK Jacobian machinery's + solve(presolve=False) -- the regression that motivated the + _with_tails split).""" + import inspect + + legacy = list(inspect.signature(solve_one_period_ConsIndShock).parameters) + self.assertEqual( + legacy, + [ + "solution_next", + "IncShkDstn", + "LivPrb", + "DiscFac", + "CRRA", + "Rfree", + "PermGroFac", + "BoroCnstArt", + "aXtraGrid", + "vFuncBool", + "CubicBool", + ], + ) + # ... and the options swap the solver in only when enabled + ce = ce_bundle() + self.assertIs( + ce[("tails", CE_K)].solve_one_period, + solve_one_period_ConsIndShock_with_tails, + ) + self.assertIs( + ce[("rails", CE_K)].solve_one_period, solve_one_period_ConsIndShock + ) + + +class TestByteZeroDefault(unittest.TestCase): + """Options unset => the solve is byte-for-byte the parent code path. + The pins were generated at parent commit df508470 on linux/x86-64: + equality is EXACT there and np.allclose(rtol=1e-9) on other platforms + (libm last-ulp differences; see the module docstring). The in-process + explicit-None-equals-stock test below is exact on EVERY platform.""" + + # exact pin equality only on the pin-generation platform + EXACT_PINS = sys.platform == "linux" and platform.machine() == "x86_64" + + def assert_pinned(self, got, pin): + got = np.atleast_1d(np.asarray(got, float)) + pin = np.atleast_1d(np.asarray(pin, float)) + if self.EXACT_PINS: + self.assertEqual(got.tolist(), pin.tolist()) + else: + np.testing.assert_allclose(got, pin, rtol=1e-9) + + def test_stock_infinite_horizon_agent_unchanged(self): + agent = IndShockConsumerType(cycles=0) + agent.verbose = 0 + agent.solve() + got = np.asarray(agent.solution[0].cFunc(BYTE_ZERO_PROBES), float) + self.assert_pinned(got, BYTE_ZERO_PIN_A) + self.assertEqual(type(agent.solution[0].cFunc).__name__, "LowerEnvelope") + + def test_cubic_vfunc_natural_constraint_unchanged(self): + agent = IndShockConsumerType( + cycles=0, CubicBool=True, vFuncBool=True, BoroCnstArt=None + ) + agent.verbose = 0 + agent.solve() + got = np.asarray(agent.solution[0].cFunc(BYTE_ZERO_PROBES), float) + self.assert_pinned(got, BYTE_ZERO_PIN_B) + self.assert_pinned( + float(agent.solution[0].vFunc(np.array([5.0]))[0]), + BYTE_ZERO_PIN_B_VFUNC_AT_5, + ) + + def test_explicit_none_options_identical_to_stock(self): + a = IndShockConsumerType(cycles=0) + a.verbose = 0 + a.solve() + b = IndShockConsumerType( + cycles=0, + decay_extrap_form=None, + decay_extrap_Q=None, + decay_extrap_form_lower=None, + ) + b.verbose = 0 + b.solve() + va = np.asarray(a.solution[0].cFunc(BYTE_ZERO_PROBES), float) + vb = np.asarray(b.solution[0].cFunc(BYTE_ZERO_PROBES), float) + self.assertEqual(va.tolist(), vb.tolist()) + + +# ================================================== nested-grid fidelity: CE +class TestCEFidelity(unittest.TestCase): + """CE-rho2 (bottom-primary, psi == 1, zero-income atom): the Theorem CE + story. Gates and measured values in the module docstring.""" + + @classmethod + def setUpClass(cls): + cls.ce = ce_bundle() + cls.truth = cls.ce["truth"].solution[0] + cls.rails = cls.ce[("rails", CE_K)].solution[0] + cls.tails = cls.ce[("tails", CE_K)].solution[0] + cls.q_eff = cls.ce[("tails", CE_K)].decay_extrap_Q + cls.pb = bottom_window(cls.truth, cls.tails) + cls.pt = top_window(cls.truth, cls.tails) + # the cross-solve measurement floor: deep-bottom rails audit vs truth + cls.floor_bottom = sup_rel_err( + cls.ce["audit"].solution[0].cFunc, cls.truth.cFunc, cls.pb + ) + + def test_subset_identity(self): + grid = self.ce["grid"] + np.testing.assert_array_equal( + np.asarray(self.ce[("tails", CE_K)].aXtraGrid), grid[CE_K:-CE_K] + ) + self.assertGreater(grid[CE_K], grid[0]) + self.assertLess(grid[-CE_K - 1], grid[-1]) + + def test_audit_non_circularity(self): + # G-AUD1 port: the rails-only solve on the 4-decades-deeper grid must + # agree with the tails truth ON the removed-bottom window. + self.assertLess( + self.floor_bottom, + GATE_CE_AUDIT, + "audit floor %.3e vs gate %.1e" % (self.floor_bottom, GATE_CE_AUDIT), + ) + + def test_bottom_fidelity_gate(self): + # floor discipline: the gate must sit above the printed audit floor + self.assertGreater( + GATE_CE_BOTTOM, + 10.0 * self.floor_bottom, + "gate %.1e not >= 10x audit floor %.3e" + % (GATE_CE_BOTTOM, self.floor_bottom), + ) + err = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + print( + "\nCE bottom: tails %.3e (gate %.1e, audit floor %.3e)" + % (err, GATE_CE_BOTTOM, self.floor_bottom) + ) + self.assertLess(err, GATE_CE_BOTTOM) + + def test_bottom_beats_rails(self): + err_r = sup_rel_err(self.rails.cFunc, self.truth.cFunc, self.pb) + err_x = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + self.assertGreater( + err_r, 10.0 * err_x, "rails %.3e vs tails %.3e" % (err_r, err_x) + ) + + def test_bottom_mpc_approaches_kappa_bar(self): + mpc = float(self.tails.cFunc.derivative(self.pb[0])) + dev = abs(mpc / self.tails.MPCmax - 1.0) + print( + "CE MPC at m=%.3e: %.12f vs MPCmax %.12f (dev %.2e)" + % (self.pb[0], mpc, self.tails.MPCmax, dev) + ) + self.assertLess(dev, GATE_CE_MPC) + + def test_top_fidelity_gate(self): + err = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pt) + err_r = sup_rel_err(self.rails.cFunc, self.truth.cFunc, self.pt) + print("CE top: tails %.3e rails %.3e (gate %.1e)" % (err, err_r, GATE_CE_TOP)) + self.assertLess(err, GATE_CE_TOP) + self.assertGreater(err_r, 10.0 * err) + + def test_two_roles_bottom(self): + # in-solve must be strictly better than the eval-only wrap; both + # sides must be above the measurement floor for the ratio to count. + cf_eval = attach_eval_only(self.rails, self.q_eff, 2.0) + err_eval = sup_rel_err(cf_eval, self.truth.cFunc, self.pb) + err_in = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + print( + "CE two-roles bottom: in-solve %.3e vs eval-only %.3e " + "(factor %.1fx, floor %.3e)" + % (err_in, err_eval, err_eval / err_in, self.floor_bottom) + ) + self.assertGreater(err_eval, self.floor_bottom) + self.assertGreater(err_in, 0.0) + self.assertLess(err_in, err_eval) + self.assertGreater(err_eval / err_in, GATE_CE_TWO_ROLES) + + def test_in_grid_contamination(self): + pi = interior_window(self.truth, self.tails) + c_x = sup_rel_err(self.tails.cFunc, self.truth.cFunc, pi) + c_r = sup_rel_err(self.rails.cFunc, self.truth.cFunc, pi) + print( + "CE contamination: tails %.3e rails %.3e (gate %.1e)" + % (c_x, c_r, GATE_CE_CONTAM) + ) + self.assertLess(c_x, GATE_CE_CONTAM) + self.assertLessEqual(c_x, c_r) + + def test_ladder_monotone(self): + # G-EX4 port (2-point ladder): a shallower trim is never worse. + tails_sh = self.ce[("tails", CE_K_SHALLOW)].solution[0] + pb_sh = bottom_window(self.truth, tails_sh) + err_sh = sup_rel_err(tails_sh.cFunc, self.truth.cFunc, pb_sh) + err_dp = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + print( + "CE ladder bottom: k=%d %.3e <= k=%d %.3e" + % (CE_K_SHALLOW, err_sh, CE_K, err_dp) + ) + self.assertLessEqual(err_sh, err_dp) + + def test_determinism(self): + # a fresh identical solve reproduces the cFunc byte-for-byte + grid = self.ce["grid"][CE_K:-CE_K] + again = solve_agent( + CE_PARS, + grid, + SOLVE_TOL, + decay_extrap_form="powerlaw", + decay_extrap_form_lower="kappabar", + ) + probes = np.concatenate([self.pb, self.pt]) + v1 = np.asarray(self.tails.cFunc(probes), float) + v2 = np.asarray(again.solution[0].cFunc(probes), float) + self.assertEqual(v1.tolist(), v2.tolist()) + + +# ================================================== nested-grid fidelity: HS +class TestHSFidelity(unittest.TestCase): + """CAL-HS (top-primary, psi-general): the Theorem A1/eq-powerlaw story at + the top; the Theorem CE-psi regime-I story at the bottom (mNrmMin < 0).""" + + @classmethod + def setUpClass(cls): + cls.hs = hs_bundle() + cls.truth = cls.hs["truth"].solution[0] + cls.rails = cls.hs[("rails", HS_K)].solution[0] + cls.tails = cls.hs[("tails", HS_K)].solution[0] + cls.q_eff = cls.hs[("tails", HS_K)].decay_extrap_Q + cls.pt = top_window(cls.truth, cls.tails) + cls.pb = bottom_window(cls.truth, cls.tails) + cls.floor_top = sup_rel_err( + cls.hs["audit"].solution[0].cFunc, cls.truth.cFunc, cls.pt + ) + + def test_audit_non_circularity(self): + # G-AUD3 port: rails-only on the 2-decades-taller grid vs truth + self.assertLess( + self.floor_top, + GATE_HS_AUDIT, + "audit floor %.3e vs gate %.1e" % (self.floor_top, GATE_HS_AUDIT), + ) + + def test_top_fidelity_gate(self): + self.assertGreater( + GATE_HS_TOP, + 10.0 * self.floor_top, + "gate %.1e not >= 10x audit floor %.3e" % (GATE_HS_TOP, self.floor_top), + ) + err = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pt) + err_r = sup_rel_err(self.rails.cFunc, self.truth.cFunc, self.pt) + print( + "\nHS top: tails %.3e rails %.3e (gate %.1e, audit floor %.3e)" + % (err, err_r, GATE_HS_TOP, self.floor_top) + ) + self.assertLess(err, GATE_HS_TOP) + self.assertGreater(err_r, 10.0 * err) + + def test_two_roles_top(self): + cf_eval = attach_eval_only(self.rails, self.q_eff, 2.0, do_bottom=False) + err_eval = sup_rel_err(cf_eval, self.truth.cFunc, self.pt) + err_in = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pt) + print( + "HS two-roles top: in-solve %.3e vs eval-only %.3e " + "(factor %.2fx, floor %.3e)" + % (err_in, err_eval, err_eval / err_in, self.floor_top) + ) + self.assertGreater(err_eval, self.floor_top) + self.assertLess(err_in, err_eval) + self.assertGreater(err_eval / err_in, GATE_HS_TWO_ROLES_TOP) + + def test_bottom_psi_general_regime_I(self): + # the GAP-CE-psi closure case: permanent shocks, mNrmMin < 0, worst + # JOINT atom = the lowest employed-income atom, deep regime I + self.assertLess(self.truth.mNrmMin, 0.0) + cf = self.tails.cFunc + self.assertIsInstance(cf, KappaBarTailInterp) + self.assertTrue(cf.in_regime) + reg = ce_psi_regime( + self.hs[("tails", HS_K)].IncShkDstn[0], + 2.0, + float((1.01 * 0.98051 * (1.0 - 1.0 / 160.0)) ** 0.5 / (1.0 + 0.01812 / 4)), + ) + self.assertEqual(reg["regime"], "I") + self.assertLess(reg["lambda_min_fiber"], 0.5) + err = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + err_r = sup_rel_err(self.rails.cFunc, self.truth.cFunc, self.pb) + print( + "HS bottom (psi-general): tails %.3e rails %.3e (gate %.1e)" + % (err, err_r, GATE_HS_BOTTOM) + ) + self.assertLess(err, GATE_HS_BOTTOM) + self.assertGreater(err_r, 10.0 * err) + mpc = float(cf.derivative(self.pb[0])) + dev = abs(mpc / self.tails.MPCmax - 1.0) + print( + "HS MPC at me=%.3e: dev %.2e (gate %.1e)" + % (self.pb[0] - self.tails.mNrmMin, dev, GATE_HS_MPC) + ) + self.assertLess(dev, GATE_HS_MPC) + + def test_two_roles_bottom(self): + cf_eval = attach_eval_only(self.rails, self.q_eff, 2.0) + err_eval = sup_rel_err(cf_eval, self.truth.cFunc, self.pb) + err_in = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pb) + print( + "HS two-roles bottom: in-solve %.3e vs eval-only %.3e " + "(factor %.1fx)" % (err_in, err_eval, err_eval / err_in) + ) + self.assertLess(err_in, err_eval) + self.assertGreater(err_eval / err_in, GATE_HS_TWO_ROLES_BOTTOM) + + def test_ladder_monotone(self): + tails_sh = self.hs[("tails", HS_K_SHALLOW)].solution[0] + pt_sh = top_window(self.truth, tails_sh) + err_sh = sup_rel_err(tails_sh.cFunc, self.truth.cFunc, pt_sh) + err_dp = sup_rel_err(self.tails.cFunc, self.truth.cFunc, self.pt) + print( + "HS ladder top: k=%d %.3e <= k=%d %.3e" + % (HS_K_SHALLOW, err_sh, HS_K, err_dp) + ) + self.assertLessEqual(err_sh, err_dp) + + +# ============================================================= regime gating +class TestRegimeGate(unittest.TestCase): + def test_regime_II_warns_and_refuses(self): + """A regime-II income process (fat low psi fiber + big zero-income + atom): the solve warns a ConstraintEndRegimeWarning NAMING + st-rem-CE-regime and keeps the default bottom segment -- the returned + cFunc equals the no-option solve exactly.""" + psi = np.array([0.12, 1.1552941176470588]) + pp = np.array([0.15, 0.85]) + th = np.array([0.0, 1.0 / 0.7]) + tp = np.array([0.3, 0.7]) + joint = make_joint_dstn(psi, pp, th, tp) + grid = log_grid(1e-4, 1e3, 120) + + def make(**opts): + pars = dict(CE_PARS) + pars.update(opts) + a = IndShockConsumerType(**pars) + a.verbose = 0 + a.IncShkDstn = [joint] + a.aXtraGrid = grid + a.tolerance = 1e-8 + return a + + base = make() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + base.solve() + gated = make(decay_extrap_form_lower="kappabar") + with warnings.catch_warnings(record=True) as wrec: + warnings.simplefilter("always") + gated.solve() + hits = [w for w in wrec if issubclass(w.category, ConstraintEndRegimeWarning)] + self.assertGreater(len(hits), 0) + self.assertIn("st-rem-CE-regime", str(hits[0].message)) + self.assertIn("regime II", str(hits[0].message)) + # refusal = the default bottom segment, byte-for-byte + probes = np.array([1e-4, 1e-2, 0.5, 5.0, 50.0]) + va = np.asarray(base.solution[0].cFunc(probes), float) + vb = np.asarray(gated.solution[0].cFunc(probes), float) + self.assertEqual(va.tolist(), vb.tolist()) + self.assertNotIsInstance(gated.solution[0].cFunc, KappaBarTailInterp) + + def test_artificial_constraint_warns_and_refuses(self): + """With HARK's default BoroCnstArt=0.0 binding (IncUnemp > 0 makes + BoroCnstNat < 0), the constraint end is a kink with MPC 1: the option + must warn and keep the default assembly.""" + pars = dict(CE_PARS) + pars.update(IncUnemp=0.3, BoroCnstArt=0.0) + base = IndShockConsumerType(**pars) + base.verbose = 0 + base.aXtraGrid = log_grid(1e-4, 1e3, 120) + base.tolerance = 1e-8 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + base.solve() + pars2 = dict(pars) + pars2.update(decay_extrap_form_lower="kappabar") + gated = IndShockConsumerType(**pars2) + gated.verbose = 0 + gated.aXtraGrid = log_grid(1e-4, 1e3, 120) + gated.tolerance = 1e-8 + with warnings.catch_warnings(record=True) as wrec: + warnings.simplefilter("always") + gated.solve() + hits = [w for w in wrec if issubclass(w.category, ConstraintEndRegimeWarning)] + self.assertGreater(len(hits), 0) + self.assertIn("artificial borrowing", str(hits[0].message)) + probes = np.array([0.1, 0.5, 5.0, 50.0]) + va = np.asarray(base.solution[0].cFunc(probes), float) + vb = np.asarray(gated.solution[0].cFunc(probes), float) + self.assertEqual(va.tolist(), vb.tolist()) + + def test_artificial_slack_constraint_builds_tail(self): + """A SLACK artificial constraint (BoroCnstArt < BoroCnstNat) must NOT + block the kappa_bar bottom tail: the natural constraint is still the + binding one, so Theorem CE applies (the gate is + BoroCnstNat >= BoroCnstArt, not BoroCnstArt is None). The tail is built + and recovers the removed-bottom window far better than the default EGM + secant. CE-rho2 has a zero-income atom => BoroCnstNat = 0, so -0.5 is + slack.""" + tails = dict(decay_extrap_form="powerlaw", decay_extrap_form_lower="kappabar") + grid = log_grid(1e-6, 1e4, 500) + k = 100 + sub = grid[k:-k] + truth_a = solve_agent(CE_PARS, grid, SOLVE_TOL, BoroCnstArt=-0.5, **tails) + tail_a = solve_agent(CE_PARS, sub, SOLVE_TOL, BoroCnstArt=-0.5, **tails) + rail_a = solve_agent(CE_PARS, sub, SOLVE_TOL, BoroCnstArt=-0.5) + truth, tail, rail = truth_a.solution[0], tail_a.solution[0], rail_a.solution[0] + # natural constraint still binds (mNrmMin == BoroCnstNat == 0 > -0.5), + # and the tail IS built despite the (slack) artificial constraint + self.assertEqual(truth.mNrmMin, 0.0) + self.assertIsInstance(tail.cFunc, KappaBarTailInterp) + # nested-grid fidelity in the removed-bottom window: tails >> rails + probes = bottom_window(truth, tail) + err_tails = sup_rel_err(tail.cFunc, truth.cFunc, probes) + err_rails = sup_rel_err(rail.cFunc, truth.cFunc, probes) + self.assertLess(err_tails, err_rails / 50.0) + self.assertLess(err_tails, 1e-6) + + def test_lifecycle_gate_flips_across_ages(self): + """The gate is evaluated per backward step: in a finite-horizon + lifecycle whose natural constraint varies with age and crosses a FIXED + artificial constraint, the kappa_bar tail attaches at some ages and is + refused at others (per-period wrap types differ). Ages alternate a + zero-income unemployment atom (tran_min=0 => natural constraint binds) + with a strictly positive worst income (tran_min>0 => BoroCnstNat drops + below the fixed artificial floor).""" + unemp = 0.05 + d_zero = make_joint_dstn( + [1.0], [1.0], [0.0, 1.0 / (1.0 - unemp)], [unemp, 1.0 - unemp] + ) + d_pos = make_joint_dstn([1.0], [1.0], [0.6, 1.4], [0.5, 0.5]) + pattern = ["zero", "pos", "zero", "pos"] + dstns = [d_zero if p == "zero" else d_pos for p in pattern] + T = len(pattern) + pars = dict(CE_PARS) + pars.update( + cycles=1, + T_cycle=T, + Rfree=[1.04] * T, + LivPrb=[1.0] * T, + PermGroFac=[1.0] * T, + TranShkStd=[0.1] * T, + PermShkStd=[0.0] * T, + BoroCnstArt=-0.3, # fixed, between the two natural regimes + decay_extrap_form_lower="kappabar", + ) + agent = IndShockConsumerType(**pars) + agent.verbose = 0 + agent.IncShkDstn = list(dstns) + agent.aXtraGrid = log_grid(1e-4, 1e3, 120) + agent.tolerance = 1e-9 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + agent.solve() + built = [ + isinstance(agent.solution[t].cFunc, KappaBarTailInterp) for t in range(T) + ] + # zero-income ages build the tail; positive-income ages refuse it + self.assertEqual(built, [True, False, True, False]) + # the refused (positive-income) ages are pinned at the artificial floor + for t, p in enumerate(pattern): + if p == "pos": + self.assertAlmostEqual(agent.solution[t].mNrmMin, -0.3, places=9) + + +# ============================================================== cubic path +class TestCubicPath(unittest.TestCase): + def test_cubic_both_tails(self): + """CubicBool=True: the top tail must be the composable DecayTailInterp + wrap (the cubic interpolant has no powerlaw option) with x_cut at the + top EGM knot -- required because the assembled cFunc is a LowerEnvelope + with no x_list.""" + agent = solve_agent( + CE_PARS, + log_grid(1e-5, 1e4, 300), + 1e-10, + CubicBool=True, + decay_extrap_form="powerlaw", + decay_extrap_form_lower="kappabar", + ) + cf = agent.solution[0].cFunc + self.assertIsInstance(cf, KappaBarTailInterp) + self.assertIsInstance(cf.interp, DecayTailInterp) + m_nodes = unwrap_m_nodes(cf) + self.assertEqual(cf.interp.x_cut, float(m_nodes[-1])) + self.assertEqual(cf.x_knot, float(m_nodes[1])) + # level continuity across both attachment points + for x in (cf.x_knot, cf.interp.x_cut): + lo = float(cf(x * (1 - 1e-11) if x > 0 else x - 1e-11)) + hi = float(cf(x * (1 + 1e-11) if x > 0 else x + 1e-11)) + self.assertAlmostEqual(lo, hi, places=7) + # tail MPCs: -> MPCmin from above at the top, -> MPCmax at the bottom + sol = agent.solution[0] + top_der = float(cf.derivative(np.array([1e7]))[0]) + self.assertGreater(top_der, sol.MPCmin) + self.assertLess(top_der, sol.MPCmin * (1.0 + 1e-3)) + bot_der = float(cf.derivative(np.array([1e-9]))[0]) + self.assertAlmostEqual(bot_der, sol.MPCmax, places=9) + + def test_cubic_default_untouched(self): + a = solve_agent(CE_PARS, log_grid(1e-5, 1e4, 300), 1e-10, CubicBool=True) + self.assertEqual(type(a.solution[0].cFunc).__name__, "LowerEnvelope") + + +# ==================================================== ergodic-grid diagnostics +# Task A/B consume the numeric core IMPORTED from BufferStockTheory-Latest's +# ergodic_coverage_lib (theory/powerlaw-decay). These tests SKIP where that +# checkout is absent (public-CI dormancy); they never vendor or reimplement it. +def _locate_ergodic_lib(): + for cand in ( + os.environ.get("BST_POWERLAW_DECAY_DIR"), + "/home/shared/github/llorracc/BufferStockTheory-Latest/theory/powerlaw-decay", + os.path.expanduser( + "~/github/llorracc/BufferStockTheory-Latest/theory/powerlaw-decay" + ), + ): + if cand and os.path.isdir(cand) and cand not in sys.path: + sys.path.insert(0, cand) + try: + import ergodic_coverage_lib # noqa: F401 + + return True + except ImportError: + return False + + +_HAVE_ERGODIC = _locate_ergodic_lib() + +# the pinned College-TOP calibration (Appendix of the 20260715-1030h prompt); the +# aXtra grid is the one the mNrmStE/mNrmTrg pins were measured on. +ERGODIC_PARS = dict( + cycles=0, + T_cycle=1, + CRRA=2.0, + Rfree=[1.01], + DiscFac=0.995714, + LivPrb=[1.0 - 1.0 / 160.0], + PermGroFac=[1.0 + 0.01958 / 4], + BoroCnstArt=None, + vFuncBool=False, + CubicBool=False, + UnempPrb=0.027, + IncUnemp=0.0, + TranShkStd=[0.12**0.5], + TranShkCount=7, + PermShkStd=[0.003**0.5], + PermShkCount=7, + T_retire=0, + UnempPrbRet=0.0, + IncUnempRet=0.0, + aXtraMin=0.001, + aXtraMax=1000.0, + aXtraCount=120, + aXtraNestFac=3, +) + + +def _ergodic_agent(**over): + pars = dict(ERGODIC_PARS) + pars.update(over) + a = IndShockConsumerType(**pars) + a.verbose = 0 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + a.update_income_process() + return a + + +@unittest.skipUnless( + _HAVE_ERGODIC, + "ergodic_coverage_lib not importable (no " + "BufferStockTheory-Latest checkout on sys.path)", +) +class TestErgodicScreen(unittest.TestCase): + """The ex-ante patience screen (Task A) — an adapter over the imported core. + Root pins are triple-confirmed (HARK brentq / BST bisection / EC2 _out.txt).""" + + @classmethod + def setUpClass(cls): + cls.d = ergodic_grid_diagnostics_from_agent(_ergodic_agent(), warn=False) + + def test_alpha_roots_on_own_atoms(self): + d = self.d + self.assertAlmostEqual(d.alpha_survivor, 2.7078, delta=1e-3) + self.assertAlmostEqual(d.alpha_counting, 3.8715, delta=1e-3) + self.assertAlmostEqual(d.alpha_harmenberg, 5.5288, delta=1e-3) + self.assertAlmostEqual(d.alpha_survivor_rawbeta, 0.4666, delta=1e-3) + + def test_ordering_and_corrected_harmenberg_gap(self): + d = self.d + self.assertLess(d.alpha_survivor, d.alpha_counting) + self.assertLess(d.alpha_counting, d.alpha_harmenberg) + # the correction of record: killed harm - count gap is strictly < 2 + self.assertLess(d.alpha_harmenberg - d.alpha_counting, 2.0) + + def test_gicnrm_split_and_mpcmin_eff(self): + d = self.d + self.assertFalse(d.GICNrm_raw_holds) # raw GIC-Nrm FAILS (1.00075) + self.assertTrue(d.GICNrm_eff_holds) # effective HOLDS (0.99762) + self.assertTrue(d.gicnrm_split) # mortality-financed tameness + self.assertAlmostEqual(d.MPCmin_eff, 0.0102053, delta=2e-6) + self.assertFalse(d.infinite_mean) + self.assertFalse(d.infinite_variance) + + def test_livprb_one_counting_equals_survivor(self): + d = ergodic_grid_diagnostics_from_agent( + _ergodic_agent(LivPrb=[1.0]), warn=False + ) + self.assertAlmostEqual(d.alpha_counting, d.alpha_survivor, delta=1e-9) + + def test_bounded_regime_degenerate_psi(self): + # psi degenerate at 1 => coefficient never exceeds 1 => bounded, routed + # before any root-find; cap = xi_max / (1 - GPF_eff). + d = ergodic_grid_diagnostics( + 1.01, + 1.0, + 0.99, + 2.0, + LivPrb=1.0, + PermShkDstn=([1.0], [1.0]), + TranShkDstn=([0.5, 1.5], [0.5, 0.5]), + warn=False, + ) + self.assertEqual(d.regime, "bounded") + self.assertAlmostEqual(d.m_sup_cap, 1.5 / (1.0 - d.GPF_eff), delta=1e-6) + self.assertTrue(np.isnan(d.alpha_counting)) + + def test_api_level_drift_guard(self): + import ergodic_coverage_lib as lib + + orig = lib.API_LEVEL + try: + lib.API_LEVEL = 999 + with self.assertRaises(ImportError): + ergodic_grid_diagnostics( + 1.01, 1.0, 0.99, 2.0, PermShkDstn=([1.0], [1.0]), warn=False + ) + finally: + lib.API_LEVEL = orig + + +@unittest.skipUnless(_HAVE_ERGODIC, "ergodic_coverage_lib not importable") +class TestErgodicReport(unittest.TestCase): + """The postmortem coverage certificate (Task B) — HARK-native simulation.""" + + @classmethod + def setUpClass(cls): + a = _ergodic_agent() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + a.solve() + cls.agent = a + cls.kw = dict(AgentCount=3000, T_sim=500, discard=300, seed=0) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cls.rep = ergodic_grid_report(a, **cls.kw) + + def test_stable_points(self): + # HARK pinning HARK; tolerance-insensitive (do NOT pin solution.hNrm) + self.assertAlmostEqual(self.rep.mNrmStE, 3.4946, delta=1e-3) + self.assertAlmostEqual(self.rep.mNrmTrg, 3.7063, delta=1e-3) + + def test_determinism(self): + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r2 = ergodic_grid_report(self.agent, **self.kw) + self.assertEqual(self.rep.quantiles, r2.quantiles) + self.assertEqual(self.rep.hill, r2.hill) + + def test_hill_above_counting_and_loose_coverage(self): + # pre-asymptotic thinning: measured Hill reads ABOVE the ex-ante root + self.assertGreaterEqual(self.rep.hill[0.01], self.rep.alpha_counting_exante) + self.assertLessEqual(self.rep.mass_above_thresholds[20.0], 1e-3) + + def test_never_mutates_the_users_agent(self): + before = float(self.agent.solution[0].cFunc(np.array([5.0]))[0]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + ergodic_grid_report( + self.agent, AgentCount=200, T_sim=50, discard=25, seed=1 + ) + after = float(self.agent.solution[0].cFunc(np.array([5.0]))[0]) + self.assertEqual(before, after) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 559890655..811b74ce5 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -4,6 +4,7 @@ from HARK.interpolation import ( IdentityFunction, + DecayTailInterp, LinearInterp, CubicInterp, CubicHermiteInterp, @@ -1057,3 +1058,1423 @@ def test_scalar_array_reversed(self): result_array = lioi(np.array([2.0, 2.0, 2.0]), y) np.testing.assert_array_almost_equal(result_scalar, result_array) + + +############################################################################### +# Decay extrapolation above the grid: exponential (legacy) and power-law tails +############################################################################### + + +class TestLinearInterpDecay(unittest.TestCase): + """Tests for LinearInterp's decay extrapolation toward a limiting line. + + Knot layout: y = intercept + slope*x - C*(x + h)**(-Q_true) with + h = intercept/slope, so the knots approach the limiting line from below + with slope falling toward ``slope`` from above -- the configuration the + top of a converged consumption function is in (Carroll-Kimball concavity), + and an exact power-law gap with known exponent Q_true. + """ + + intercept = 1.0 + slope = 0.5 # limiting line 1.0 + 0.5*x = 0.5*(x + 2); pivot h = 2 + C = 4.0 + Q_true = 1.5 + + def gap_truth(self, x): + h = self.intercept / self.slope + return self.C * (x + h) ** (-self.Q_true) + + def truth(self, x): + return self.intercept + self.slope * x - self.gap_truth(x) + + def knots(self, n=201, top=21.0): + x = np.linspace(1.0, top, n) + return x, self.truth(x) + + def test_exp_default_unchanged(self): + # The default form is the long-standing exponential decay; pin its + # closed form so any behavior change would be caught here. + x, y = self.knots() + f = LinearInterp(x, y, self.intercept, self.slope) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_form, "exp") + slope_top = (y[-1] - y[-2]) / (x[-1] - x[-2]) + A = self.intercept + self.slope * x[-1] - y[-1] + B = -(self.slope - slope_top) / A + q = np.array([25.0, 40.0, 100.0]) + expect = self.intercept + self.slope * q - A * np.exp(-B * (q - x[-1])) + np.testing.assert_allclose(f(q), expect, rtol=1e-12) + d_expect = self.slope + B * A * np.exp(-B * (q - x[-1])) + np.testing.assert_allclose(f.derivative(q), d_expect, rtol=1e-12) + + def test_exp_formula_survives_missing_form_attribute(self): + # Instances unpickled from versions predating decay_extrap_form must + # keep evaluating with the exponential formula. + x, y = self.knots() + f = LinearInterp(x, y, self.intercept, self.slope) + del f.decay_extrap_form + g = LinearInterp(x, y, self.intercept, self.slope) + q = np.array([25.0, 40.0, 100.0]) + np.testing.assert_array_equal(f(q), g(q)) + + def test_invalid_form_raises(self): + x, y = self.knots() + with self.assertRaises(ValueError): + LinearInterp(x, y, self.intercept, self.slope, decay_extrap_form="foo") + + def test_powerlaw_level_and_slope_continuous_at_top_knot(self): + x, y = self.knots() + f = LinearInterp( + x, y, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_form, "powerlaw") + slope_top = (y[-1] - y[-2]) / (x[-1] - x[-2]) + eps = 1e-7 + level_above = float(f(np.array([x[-1] + eps]))[0]) + self.assertAlmostEqual(level_above, y[-1] + slope_top * eps, places=10) + deriv_above = float(f.derivative(np.array([x[-1] + 1e-9]))[0]) + self.assertAlmostEqual(deriv_above, slope_top, places=9) + + def test_powerlaw_tail_is_exact_power_law(self): + # The extrapolated gap must have constant log-log slope equal to -Q + # in (x + h); an exponential tail's log-log slope diverges instead. + x, y = self.knots() + f = LinearInterp( + x, y, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + h = self.intercept / self.slope + lad = np.geomspace(25.0, 2000.0, 30) + gap = self.intercept + self.slope * lad - f(lad) + slopes = np.diff(np.log(gap)) / np.diff(np.log(lad + h)) + np.testing.assert_allclose(slopes, -f.decay_extrap_Q, rtol=1e-4) + + def test_powerlaw_beats_exp_against_truth(self): + # Dense knots on an exact power-law gap: the power-law tail recovers + # the true function far above the grid; the exponential kills the gap + # orders of magnitude too fast. + x, y = self.knots(n=20001) + fp = LinearInterp( + x, y, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + fe = LinearInterp(x, y, self.intercept, self.slope) + self.assertAlmostEqual(fp.decay_extrap_Q, self.Q_true, delta=1e-3) + lad = np.geomspace(25.0, 2000.0, 30) + gap_true = self.gap_truth(lad) + gap_pl = self.intercept + self.slope * lad - fp(lad) + gap_exp = self.intercept + self.slope * lad - fe(lad) + np.testing.assert_allclose(gap_pl, gap_true, rtol=1e-3) + # the exponential has destroyed most of the true gap by the ladder top + self.assertLess(gap_exp[-1], 0.5 * gap_true[-1]) + err_pl = np.abs(gap_pl - gap_true) + err_exp = np.abs(gap_exp - gap_true) + self.assertGreater(err_exp[-1], 100.0 * err_pl[-1]) + + def test_powerlaw_approaches_line_from_below_slope_from_above(self): + x, y = self.knots() + f = LinearInterp( + x, y, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + # Measurable window: the gap must stay above float64 cancellation + # noise (eps * line) for below-line/monotonicity to be observable from + # the outside -- the same pitfall as measuring consumption-function + # gaps on deep grids. + lad = np.geomspace(25.0, 1.0e5, 40) + vals = f(lad) + gap = self.intercept + self.slope * lad - vals + self.assertTrue(np.all(np.isfinite(vals))) + self.assertTrue(np.all(gap > 0.0)) # strictly below the line + self.assertTrue(np.all(np.diff(gap) < 0.0)) # gap shrinks monotonically + ders = f.derivative(lad) + self.assertTrue(np.all(ders > self.slope)) # slope above the limit... + self.assertTrue(np.all(np.diff(ders) < 0.0)) # ...falling toward it + # Astronomical query: finite, at the line to float precision, slope at + # the limit (the true gap ~1e-18 is below float64 resolution there). + far = np.array([1.0e12]) + val_far = float(f(far)[0]) + line_far = self.intercept + self.slope * far[0] + self.assertTrue(np.isfinite(val_far)) + self.assertLessEqual(val_far, line_far * (1.0 + 1e-12)) + der_far = float(f.derivative(far)[0]) + self.assertGreaterEqual(der_far, self.slope) + self.assertAlmostEqual(der_far, self.slope, places=10) + + def test_powerlaw_guards_disable_decay(self): + x = np.linspace(1.0, 21.0, 201) + h = self.intercept / self.slope + line = self.intercept + self.slope * x + # (a) top knot above the limiting line (approach from above) + y_above = line + self.C * (x + h) ** (-self.Q_true) + with self.assertWarns(UserWarning): + f = LinearInterp( + x, y_above, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + self.assertFalse(f.decay_extrap) + # (b) below the line but diverging from it (top slope below slope_limit) + y_diverge = line - 0.1 * (x + h) ** 0.5 + with self.assertWarns(UserWarning): + f = LinearInterp( + x, y_diverge, self.intercept, self.slope, decay_extrap_form="powerlaw" + ) + self.assertFalse(f.decay_extrap) + # (c) non-positive slope_limit + x2, y2 = self.knots() + with self.assertWarns(UserWarning): + f = LinearInterp(x2, y2, self.intercept, -0.5, decay_extrap_form="powerlaw") + self.assertFalse(f.decay_extrap) + + +class TestLinearInterpExplicitQ(unittest.TestCase): + """Tests for the keyword-only ``decay_extrap_Q`` (explicit power-law + exponent) extension of LinearInterp. + + Pre-registered tolerances: level continuity at the top knot to 1e-9 + (absolute); kinked derivative just above the top knot equals + ``slope_limit + Q*A/pivot`` to rel 1e-6; ``decay_extrap_Q=None`` + evaluations identical (==) to a construction without the kwarg. + + Since the two-term C1 attachment became the explicit-Q default + (decay_extrap_terms=2), the tests here pin ``decay_extrap_terms=1``: + they are the ONE-TERM regression suite, preserving the original + registrations verbatim. TestLinearInterpTwoTerm covers the default. + + # THEOREM-REF[BufferStockTheory-Latest @ c181870f :: theory/powerlaw-decay/final_proof.md :: §7. The computational payoff: why the compactified core is the right presentation :: The extrapolation form of record :: https://llorracc.github.io/BufferStockTheory-Latest/powerlaw-decay-theory/] + # The theorem's extrapolation form of record is g ~ C*(x+h)**(-q) with + # q = min(1, q*); the explicit-Q keyword lets callers attach that exponent + # level-matched at the top knot instead of the fitted 2-knot exponent. + """ + + intercept = 1.0 + slope = 0.5 # limiting line 1.0 + 0.5*x = 0.5*(x + 2); pivot h = 2 + C = 4.0 + Q_true = 1.5 + + def truth(self, x): + h = self.intercept / self.slope + return self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + + def knots(self, n=201, top=21.0): + x = np.linspace(1.0, top, n) + return x, self.truth(x) + + def test_explicit_Q_level_match_and_kink(self): + # F13 mechanics: level continuity preserved to 1e-9; derivative just + # above the top knot equals slope_limit + Q*A/pivot to rel 1e-6. + x, y = self.knots() + Q_new = 0.777 + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q_new, + decay_extrap_terms=1, + ) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q, Q_new) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + A = float(f.decay_extrap_A) + pivot = float(f.decay_extrap_pivot) + lvl = float(f(np.array([x[-1] + 1e-9]))[0]) + lvl_expect = self.intercept + self.slope * (x[-1] + 1e-9) - A + self.assertLessEqual(abs(lvl - lvl_expect), 1e-9) + der = float(f.derivative(np.array([x[-1] + 1e-12]))[0]) + der_expect = self.slope + Q_new * A / pivot + self.assertLessEqual(abs(der / der_expect - 1.0), 1e-6) + + def test_explicit_Q_tail_has_the_explicit_exponent(self): + x, y = self.knots() + Q_new = 0.777 + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q_new, + decay_extrap_terms=1, + ) + h = self.intercept / self.slope + lad = np.geomspace(25.0, 2000.0, 30) + gap = self.intercept + self.slope * lad - f(lad) + slopes = np.diff(np.log(gap)) / np.diff(np.log(lad + h)) + np.testing.assert_allclose(slopes, -Q_new, rtol=1e-4) + + def test_validation_errors(self): + x, y = self.knots() + with self.assertRaises(ValueError): # requires powerlaw form + LinearInterp(x, y, self.intercept, self.slope, decay_extrap_Q=0.5) + with self.assertRaises(ValueError): # requires the limiting line + LinearInterp(x, y, decay_extrap_form="powerlaw", decay_extrap_Q=0.5) + for bad in (0.0, -1.0, np.nan, np.inf): + with self.assertRaises(ValueError): + LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=bad, + ) + + def test_default_None_is_identical_to_current_behavior(self): + # decay_extrap_Q=None must be byte-identical to not passing the kwarg, + # for both forms (the new branch is dead until opted in). + x, y = self.knots() + q = np.geomspace(22.0, 5000.0, 50) + for form in ("exp", "powerlaw"): + f_old = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form=form) + f_new = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form=form, decay_extrap_Q=None) + np.testing.assert_array_equal(f_old(q), f_new(q)) + np.testing.assert_array_equal(f_old.derivative(q), f_new.derivative(q)) + f_fit = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw") + self.assertEqual(f_fit.decay_extrap_Q_source, "fitted") + self.assertAlmostEqual( + f_fit.decay_extrap_Q, + f_fit.decay_extrap_B * f_fit.decay_extrap_pivot, + places=14, + ) + + def test_rescue_where_fitted_form_disables_decay(self): + # Below the line but top slope BELOW slope_limit (fitted B <= 0): the + # fitted powerlaw must disable decay (warns); an explicit Q attaches a + # level-matched decaying tail instead — the rescue case. + x = np.linspace(1.0, 21.0, 201) + h = self.intercept / self.slope + line = self.intercept + self.slope * x + y_diverge = line - 0.1 * (x + h) ** 0.5 + with self.assertWarns(UserWarning): + f_fit = LinearInterp( + x, y_diverge, self.intercept, self.slope, + decay_extrap_form="powerlaw", + ) + self.assertFalse(f_fit.decay_extrap) + f_q = LinearInterp( + x, y_diverge, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.6, + decay_extrap_terms=1, + ) + self.assertTrue(f_q.decay_extrap) + self.assertEqual(f_q.decay_extrap_Q_source, "explicit") + # level-matched at the top knot, decaying toward the line above it + gap_top = self.intercept + self.slope * x[-1] - y_diverge[-1] + lad = np.geomspace(25.0, 2000.0, 20) + gap = self.intercept + self.slope * lad - f_q(lad) + self.assertTrue(np.all(gap > 0.0)) + self.assertTrue(np.all(np.diff(gap) < 0.0)) + self.assertLess(gap[0], gap_top) + + def test_explicit_Q_guard_still_refuses_above_line_knot(self): + # Top knot ABOVE the limiting line: even an explicit exponent cannot + # define a positive decaying gap; warn + disable, as the fitted form does. + x = np.linspace(1.0, 21.0, 201) + h = self.intercept / self.slope + y_above = self.intercept + self.slope * x + self.C * (x + h) ** (-self.Q_true) + with self.assertWarns(UserWarning): + f = LinearInterp( + x, y_above, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.6, + ) + self.assertFalse(f.decay_extrap) + + def test_pickle_roundtrip_and_missing_source_attribute(self): + import pickle + + x, y = self.knots() + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.777, + ) + g = pickle.loads(pickle.dumps(f)) + q = np.geomspace(22.0, 5000.0, 30) + np.testing.assert_array_equal(f(q), g(q)) + self.assertEqual(g.decay_extrap_Q_source, "explicit") + # instances unpickled from versions predating decay_extrap_Q_source + # (fitted powerlaw) must keep evaluating identically + f_fit = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw") + vals_before = f_fit(q).copy() + del f_fit.decay_extrap_Q_source + np.testing.assert_array_equal(f_fit(q), vals_before) + + +############################################################################### +# DecayTailInterp: the decay tail as a composable wrapper over any 1D body +############################################################################### + + +class TestDecayTailInterpParity(unittest.TestCase): + """The wrapper must reproduce LinearInterp's baked-in decay machinery + when composed over a bare linear body on the same knots: the level and + slope it reads at x_cut are byte-identical to the grid values the + baked-in setup uses, so values and derivatives must match bit for bit + (gate G1 of the decoupling pre-registration). + """ + + intercept = 1.0 + slope = 0.5 # limiting line 1.0 + 0.5*x; pivot h = 2 + C = 4.0 + Q_true = 1.5 + + def truth(self, x): + h = self.intercept / self.slope + return self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + + def knots(self, n=201, top=21.0): + x = np.linspace(1.0, top, n) + return x, self.truth(x) + + def probes(self, top=21.0): + # below-grid (NaN region), the bottom knot, interior points, the top + # knot itself, just above it, and the deep tail + return np.concatenate( + [ + np.array([0.25, 0.5, 1.0, 1.37, 10.3, top]), + np.geomspace(top * (1.0 + 1e-12), 100.0 * top, 40), + ] + ) + + def pair(self, form, Q=None, pre_compute=False): + x, y = self.knots() + body = LinearInterp(x, y, pre_compute=pre_compute) + kwargs = {"decay_extrap_form": form} + if Q is not None: + kwargs["decay_extrap_Q"] = Q + wrapped = DecayTailInterp(body, self.intercept, self.slope, **kwargs) + baked = LinearInterp(x, y, self.intercept, self.slope, **kwargs) + return wrapped, baked + + def assert_pair_byte_equal(self, wrapped, baked): + q = self.probes() + np.testing.assert_array_equal(wrapped(q), baked(q)) + np.testing.assert_array_equal(wrapped.derivative(q), baked.derivative(q)) + yw, dw = wrapped.eval_with_derivative(q) + yb, db = baked.eval_with_derivative(q) + np.testing.assert_array_equal(yw, yb) + np.testing.assert_array_equal(dw, db) + + def test_powerlaw_explicit_Q_byte_parity(self): + wrapped, baked = self.pair("powerlaw", Q=1.5) + self.assertTrue(wrapped.decay_extrap) + self.assertEqual(wrapped.decay_extrap_Q_source, "explicit") + self.assert_pair_byte_equal(wrapped, baked) + + def test_powerlaw_fitted_byte_parity(self): + wrapped, baked = self.pair("powerlaw") + self.assertEqual(wrapped.decay_extrap_Q_source, "fitted") + self.assertEqual(wrapped.decay_extrap_Q, baked.decay_extrap_Q) + self.assert_pair_byte_equal(wrapped, baked) + + def test_exp_byte_parity_and_deprecation(self): + # the wrapper's exp form is deprecated at birth (selecting it warns); + # LinearInterp's own exp default is untouched legacy behavior + with self.assertWarns(DeprecationWarning): + wrapped, baked = self.pair("exp") + self.assert_pair_byte_equal(wrapped, baked) + + def test_pre_compute_body_parity(self): + # pre_compute bodies evaluate knots through different float + # arithmetic, so the tail amplitude may differ by ulps: gate at + # 1e-13 relative (G1b); byte identity not required. + import warnings + + for form, Q in [("powerlaw", 1.5), ("powerlaw", None), ("exp", None)]: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + wrapped, baked = self.pair(form, Q=Q, pre_compute=True) + q = self.probes() + np.testing.assert_allclose( + wrapped(q), baked(q), rtol=1e-13, equal_nan=True + ) + np.testing.assert_allclose( + wrapped.derivative(q), baked.derivative(q), + rtol=1e-13, equal_nan=True, + ) + + def test_guard_parity_disable_paths(self): + # Guard-tripping configurations must disable decay in BOTH + # constructions, which then agree byte-for-byte above the cut where + # both fall back to the body's naive top-segment extrapolation. + x = np.linspace(1.0, 21.0, 201) + h = self.intercept / self.slope + # (a) top knot ABOVE the limiting line (level_diff < 0), slope still + # approaching (so the silent isclose guard does not preempt) + y_above = self.truth(x) + 1.0 + # (b) level below the line but slope flattening BELOW slope_limit + # (fitted B < 0): gap GROWS linearly toward the top + y_flat = self.intercept + self.slope * x - (0.1 + 0.01 * (x - 1.0)) + # (d) pivot <= 0: a limiting line with x-intercept above the grid top + icpt_neg = -30.0 + y_pneg = icpt_neg + self.slope * x - self.C * (x + 100.0) ** (-self.Q_true) + for y_bad, icpt in [(y_above, self.intercept), (y_flat, self.intercept), + (y_pneg, icpt_neg)]: + body = LinearInterp(x, y_bad) + with self.assertWarns(UserWarning): + wrapped = DecayTailInterp( + body, icpt, self.slope, decay_extrap_form="powerlaw" + ) + with self.assertWarns(UserWarning): + baked = LinearInterp( + x, y_bad, icpt, self.slope, decay_extrap_form="powerlaw" + ) + self.assertFalse(wrapped.decay_extrap) + self.assertFalse(baked.decay_extrap) + q = np.geomspace(22.0, 2000.0, 25) + np.testing.assert_array_equal(wrapped(q), baked(q)) + np.testing.assert_array_equal(wrapped(q), body(q)) + # (c) zero-uncertainty case: the body IS a line with the limiting + # slope; both disable SILENTLY (np.isclose guard) + y_line = 0.3 + self.slope * x + body = LinearInterp(x, y_line) + wrapped = DecayTailInterp(body, self.intercept, self.slope, + decay_extrap_form="powerlaw") + baked = LinearInterp(x, y_line, self.intercept, self.slope, + decay_extrap_form="powerlaw") + self.assertFalse(wrapped.decay_extrap) + self.assertFalse(baked.decay_extrap) + q = np.geomspace(22.0, 2000.0, 25) + np.testing.assert_array_equal(wrapped(q), baked(q)) + + def test_exact_zero_gap_at_cut_matches_linearinterp(self): + # Refuter finding (2026-07-11): a body whose value at x_cut lies + # EXACTLY on the limiting line (level_diff == 0.0) with slope not + # isclose used to raise ZeroDivisionError at construction (Python- + # float division), where LinearInterp's numpy arithmetic attaches + # B=inf (exp: evaluates to the line) or warn-disables (powerlaw). + # The wrapper must mirror LinearInterp, not crash. + import warnings + + x = np.linspace(1.0, 21.0, 101) + y = 1.0 + 0.5 * x - (21.0 - x) * 0.05 # gap closes to exact 0 at top + q = np.array([25.0, 40.0, 100.0]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + w_exp = DecayTailInterp(LinearInterp(x, y), 1.0, 0.5, + decay_extrap_form="exp") + b_exp = LinearInterp(x, y, 1.0, 0.5, decay_extrap_form="exp") + np.testing.assert_array_equal(w_exp(q), b_exp(q)) + # the exp tail with A=0, B=inf IS the limiting line + np.testing.assert_array_equal(w_exp(q), 1.0 + 0.5 * q) + with self.assertWarns(UserWarning): + w_pl = DecayTailInterp(LinearInterp(x, y), 1.0, 0.5, + decay_extrap_form="powerlaw") + with self.assertWarns(UserWarning): + b_pl = LinearInterp(x, y, 1.0, 0.5, decay_extrap_form="powerlaw") + self.assertFalse(w_pl.decay_extrap) + self.assertFalse(b_pl.decay_extrap) + np.testing.assert_array_equal(w_pl(q), b_pl(q)) + + def test_rescue_parity_explicit_Q_where_fit_disables(self): + # (b) above: fitted must disable, but an explicit exponent attaches a + # level-matched tail -- in both constructions, byte-identically. + x = np.linspace(1.0, 21.0, 201) + y_flat = self.intercept + self.slope * x - (0.1 + 0.01 * (x - 1.0)) + body = LinearInterp(x, y_flat) + wrapped = DecayTailInterp(body, self.intercept, self.slope, + decay_extrap_Q=1.0) + baked = LinearInterp(x, y_flat, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=1.0) + self.assertTrue(wrapped.decay_extrap) + self.assertTrue(baked.decay_extrap) + self.assert_pair_byte_equal(wrapped, baked) + + +class TestDecayTailInterpCubic(unittest.TestCase): + """Power-law tails composed over a cubic body -- the case the baked-in + machinery cannot express (CubicInterp carries only the legacy exponential + upper_row decay, with no power-law form and no explicit-exponent hook). + """ + + intercept = 1.0 + slope = 0.5 + C = 4.0 + Q_true = 1.5 + + def cubic_body(self, n=201, top=21.0): + h = self.intercept / self.slope + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + dydx = self.slope + self.Q_true * self.C * (x + h) ** (-self.Q_true - 1.0) + return CubicInterp(x, y, dydx), x, y + + def test_below_cut_is_the_bare_cubic(self): + body, x, y = self.cubic_body() + f = DecayTailInterp(body, self.intercept, self.slope, decay_extrap_Q=1.0) + q = np.linspace(1.0, 21.0, 517) # includes both grid endpoints + np.testing.assert_array_equal(f(q), body(q)) + np.testing.assert_array_equal(f.derivative(q), body.derivative(q)) + + def test_level_matched_at_cut(self): + body, x, y = self.cubic_body() + f = DecayTailInterp(body, self.intercept, self.slope, decay_extrap_Q=1.0) + lvl = float(body(np.array([21.0]))[0]) + gap = self.intercept + self.slope * 21.0 - lvl + # LEVEL match under the two-term default: the amplitudes sum to the + # level gap read from the body at the cut (A alone is the gap only + # in one-term mode) + self.assertEqual(f.decay_extrap_terms, 2) + self.assertEqual(f.decay_extrap_A + f.decay_extrap_A2, gap) + # continuity: the tail limit at the cut equals the body value there + val_above = float(f(np.array([21.0 + 1e-9]))[0]) + self.assertAlmostEqual(val_above, lvl, places=8) + + def test_tail_is_exact_power_law_with_the_explicit_exponent(self): + body, x, y = self.cubic_body() + Q = 0.62 # an explicit (theory-style) exponent != the body's own + f = DecayTailInterp(body, self.intercept, self.slope, decay_extrap_Q=Q, + decay_extrap_terms=1) + h = self.intercept / self.slope + lad = np.geomspace(25.0, 2000.0, 30) + gap = self.intercept + self.slope * lad - f(lad) + slopes = np.diff(np.log(gap)) / np.diff(np.log(lad + h)) + np.testing.assert_allclose(slopes, -Q, rtol=1e-4) + + def test_tail_derivative_matches_finite_difference(self): + body, x, y = self.cubic_body() + f = DecayTailInterp(body, self.intercept, self.slope, decay_extrap_Q=1.0) + q = np.geomspace(25.0, 500.0, 12) + eps = 1e-6 + fd = (f(q + eps) - f(q - eps)) / (2 * eps) + np.testing.assert_allclose(f.derivative(q), fd, rtol=1e-6) + + def test_fitted_form_reads_the_cubic_slope(self): + body, x, y = self.cubic_body() + f = DecayTailInterp(body, self.intercept, self.slope) # fitted powerlaw + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_Q_source, "fitted") + # the fitted exponent is built from the body's own slope reading at + # the cut (the cubic's analytic derivative), not a two-knot secant + h = self.intercept / self.slope + lvl = float(body(np.array([x[-1]]))[0]) + gap = self.intercept + self.slope * x[-1] - lvl + slope_at = float(body.derivative(np.array([x[-1]]))[0]) + B = (slope_at - self.slope) / gap + self.assertAlmostEqual(f.decay_extrap_Q, B * (x[-1] + h), places=12) + + def test_off_knot_cut_truncates_the_body(self): + body, x, y = self.cubic_body() + cut = 15.0 # strictly inside the body grid + f = DecayTailInterp(body, self.intercept, self.slope, x_cut=cut, + decay_extrap_Q=1.0, decay_extrap_terms=1) + # at and below the cut: still the body, untouched + q_lo = np.linspace(1.0, cut, 101) + np.testing.assert_array_equal(f(q_lo), body(q_lo)) + # just above the cut: the tail, level-matched at the cut + lvl = float(body(np.array([cut]))[0]) + self.assertAlmostEqual(float(f(np.array([cut + 1e-9]))[0]), lvl, places=8) + # farther out the tail (exponent 1.0) departs from the body's own + # values (its gap decays with a different exponent) + diff = abs(float(f(np.array([20.0]))[0]) - float(body(np.array([20.0]))[0])) + self.assertGreater(diff, 1e-3) + + +class TestDecayTailInterpGeneric(unittest.TestCase): + """Composition over arbitrary callables plus the mechanism-level edge + semantics: x_cut requirement, argument validation, the continuity + invariant, deep-tail stability, NaN routing, scalar/empty shapes, + distance.""" + + intercept = 1.0 + slope = 0.5 + + @staticmethod + def body(x): + # a smooth concave function below the line 1 + 0.5*x; deliberately a + # bare callable with NO derivative method + x = np.asarray(x, dtype=float) + return 1.0 + 0.5 * x - 3.0 / (x + 2.0) + + def test_explicit_Q_works_on_a_bare_callable(self): + # one-term mode: the only explicit mode available without a + # derivative method (the two-term default slope-matches) + f = DecayTailInterp(self.body, self.intercept, self.slope, x_cut=40.0, + decay_extrap_Q=1.0, decay_extrap_terms=1) + self.assertTrue(f.decay_extrap) + self.assertAlmostEqual( + float(f(np.array([40.0 + 1e-9]))[0]), float(self.body(40.0)), + places=8, + ) + + def test_fitted_form_requires_a_derivative_method(self): + with self.assertRaises(ValueError): + DecayTailInterp(self.body, self.intercept, self.slope, x_cut=40.0) + + def test_x_cut_required_without_a_grid(self): + with self.assertRaises(ValueError): + DecayTailInterp(self.body, self.intercept, self.slope, + decay_extrap_Q=1.0) + + def test_argument_validation(self): + import warnings + + with self.assertRaises(ValueError): + DecayTailInterp(self.body, None, None, x_cut=40.0, + decay_extrap_Q=1.0) + with self.assertRaises(ValueError): + DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_form="foo") + with self.assertRaises(ValueError): + DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_Q=-1.0) + with self.assertRaises(ValueError): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_form="exp", decay_extrap_Q=1.0) + + def test_continuity_invariant_no_amplitude_hook(self): + # Design ruling 2026-07-11: level continuity at x_cut is an INVARIANT + # of the class -- there is deliberately NO amplitude-override + # parameter (an imposed amplitude would force a level jump at the + # cut). Every attachable tail is level-matched. + with self.assertRaises(TypeError): + DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_Q=1.0, decay_extrap_A=0.1) + f = DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_Q=1.0, decay_extrap_terms=1) + # the amplitude IS the level gap at the cut: continuous by + # construction + gap = (1.0 + 0.5 * 40.0) - float(self.body(40.0)) + self.assertEqual(f.decay_extrap_A, gap) + self.assertAlmostEqual(float(f(np.array([40.0 + 1e-9]))[0]), + float(self.body(40.0)), places=8) + + def test_deep_tail_is_finite_below_line_monotone(self): + # normalized power-law evaluation: deep queries decay smoothly to the + # LINE, never to nan/inf, even at a large explicit exponent + f = DecayTailInterp(self.body, 1.0, 0.5, x_cut=40.0, + decay_extrap_Q=9.7, decay_extrap_terms=1) + q = np.geomspace(41.0, 4.0e7, 200) + vals = f(q) + line = 1.0 + 0.5 * q + self.assertTrue(np.all(np.isfinite(vals))) + self.assertTrue(np.all(vals <= line)) + self.assertTrue(np.all(np.diff(vals) > 0)) + # the gap has vanished to float resolution by the deep end + self.assertLess(line[-1] - vals[-1], 1e-6 * line[-1]) + + def test_nan_and_shape_semantics(self): + xg = np.linspace(1.0, 21.0, 41) + body_lin = LinearInterp(xg, self.body(xg)) + f = DecayTailInterp(body_lin, 1.0, 0.5, decay_extrap_Q=1.0) + out = f(np.array([np.nan, 0.2, 5.0, 50.0])) + self.assertTrue(np.isnan(out[0])) # NaN routes to the body + self.assertTrue(np.isnan(out[1])) # below grid, lower_extrap=False + self.assertTrue(np.isfinite(out[2]) and np.isfinite(out[3])) + self.assertEqual(np.shape(f(3.0)), ()) # scalar in, scalar out + self.assertEqual(f(np.array([])).size, 0) + self.assertEqual(f.derivative(np.array([])).size, 0) + + def test_distance_recurses_into_the_body(self): + xg = np.linspace(1.0, 21.0, 41) + b1 = LinearInterp(xg, self.body(xg)) + b2 = LinearInterp(xg, self.body(xg) + 1e-3) + f1 = DecayTailInterp(b1, 1.0, 0.5, decay_extrap_Q=1.0) + f2 = DecayTailInterp(b2, 1.0, 0.5, decay_extrap_Q=1.0) + self.assertEqual(f1.distance(f2), b1.distance(b2)) + self.assertEqual(f1.distance(f1), 0.0) + + def test_pickle_roundtrip(self): + import pickle + + xg = np.linspace(1.0, 21.0, 41) + body_lin = LinearInterp(xg, self.body(xg)) + f = DecayTailInterp(body_lin, 1.0, 0.5, decay_extrap_Q=1.0) + g = pickle.loads(pickle.dumps(f)) + q = np.geomspace(22.0, 5000.0, 30) + np.testing.assert_array_equal(f(q), g(q)) + np.testing.assert_array_equal(f.derivative(q), g.derivative(q)) + + +class TestLinearInterpTwoTerm(unittest.TestCase): + """The C1 two-term explicit-exponent attachment (decay_extrap_terms=2, + the DEFAULT; F11): gap = A*z**(-Q) + A2*z**(-(Q+1)) with + A2 = G*(Q_fit - Q), A = G - A2, level- AND slope-matched at the top knot. + + Pre-registered tolerances: derivative continuity at the top knot to + 1e-12 (absolute; the one-term kink at the same knot exceeds 1e-4 in the + fixture); closed-form amplitude identities exact (==); collapse at + Q == Q_fit byte-identical to the one-term tail; leading exponent + recovered at depth to rtol 1e-3. + """ + + intercept = 1.0 + slope = 0.5 + C = 4.0 + Q_true = 1.5 + + def knots(self, n=201, top=21.0): + h = self.intercept / self.slope + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + return x, y + + def build(self, Q, terms=2): + x, y = self.knots() + return x, y, LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q, + decay_extrap_terms=terms, + ) + + def test_default_is_two_term_and_C1(self): + x, y = self.knots() + f = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.62) + self.assertEqual(f.decay_extrap_terms, 2) # the default + slope_top = (y[-1] - y[-2]) / (x[-1] - x[-2]) + d_above = float(f.derivative(np.array([x[-1] + 1e-12]))[0]) + self.assertLess(abs(d_above - slope_top), 1e-12) # C1: no kink + # contrast: the one-term kink at the same knot is 8+ orders larger + f1 = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.62, + decay_extrap_terms=1) + d1 = float(f1.derivative(np.array([x[-1] + 1e-12]))[0]) + self.assertGreater(abs(d1 - slope_top), 1e-4) + + def test_closed_form_amplitudes_and_formula(self): + x, y, f = self.build(Q=0.62) + G = self.intercept + self.slope * x[-1] - y[-1] + Q_fit = f.decay_extrap_B * f.decay_extrap_pivot + self.assertEqual(f.decay_extrap_A2, G * (Q_fit - 0.62)) + self.assertEqual(f.decay_extrap_A, G - f.decay_extrap_A2) + # the evaluated gap IS the two-term closed form (atol covers the + # line-minus-value extraction cancellation, ~line*eps at the ladder + # top; the formula itself agrees to ~1e-15 relative) + lad = np.geomspace(25.0, 5000.0, 40) + z = (lad + f.decay_extrap_pivot - x[-1]) / f.decay_extrap_pivot + gap_form = (f.decay_extrap_A * z ** (-0.62) + + f.decay_extrap_A2 * z ** (-1.62)) + gap = self.intercept + self.slope * lad - f(lad) + np.testing.assert_allclose(gap, gap_form, rtol=1e-9, atol=1e-12) + + def test_collapse_at_Q_fit_is_byte_identical_to_one_term(self): + x, y = self.knots() + probe = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw") + Q_fit = float(probe.decay_extrap_Q) # the fitted exponent + f2 = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q_fit) + f1 = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q_fit, + decay_extrap_terms=1) + self.assertEqual(f2.decay_extrap_A2, 0.0) + # refuter finding (B1): the two-term evaluation of a collapsed tail + # differed from one-term by 1 ulp on the DERIVATIVE channel (op + # ordering); an exact collapse therefore stores the one-term + # representation outright, making byte-identity true by construction + self.assertEqual(f2.decay_extrap_terms, 1) + q = np.geomspace(21.0000001, 5000.0, 40) + np.testing.assert_array_equal(f2(q), f1(q)) + np.testing.assert_array_equal(f2.derivative(q), f1.derivative(q)) + + def test_leading_exponent_recovered_at_depth(self): + # Q = 1.2 keeps the amplitude mix mild (A2/A ~ 0.4), so the window + # [3e4, 3e5] is deep enough for the correction term (< 3.3e-4 + # relative) while the gap stays far above the float64 cancellation + # floor of the line-minus-value extraction + x, y, f = self.build(Q=1.2) + h = self.intercept / self.slope + lad = np.geomspace(3.0e4, 3.0e5, 30) + gap = self.intercept + self.slope * lad - f(lad) + slopes = np.diff(np.log(gap)) / np.diff(np.log(lad + h)) + np.testing.assert_allclose(slopes, -1.2, rtol=1e-3) + + def test_properties_below_line_mpc_floor_monotone(self): + x, y, f = self.build(Q=0.62) + q = np.geomspace(21.001, 1.0e5, 200) + line = self.intercept + self.slope * q + vals, dv = f.eval_with_derivative(q) + self.assertTrue(np.all(vals < line)) + self.assertTrue(np.all(dv > self.slope)) # MPC floor preserved + self.assertTrue(np.all(np.diff(vals) > 0.0)) + + def test_fallback_when_Q_fit_at_or_above_Q_plus_1(self): + # fixture Q_fit ~= 1.51; Q = 0.4 puts Q+1 = 1.4 below it + with self.assertWarns(UserWarning): + x, y, f = self.build(Q=0.4) + self.assertEqual(f.decay_extrap_terms, 1) + G = self.intercept + self.slope * x[-1] - y[-1] + self.assertEqual(f.decay_extrap_A, G) # one-term level match + + def test_degenerate_top_segment_falls_back_not_nan(self): + # Refuter finding (B2): a duplicated top knot with falling y gives + # slope_at_top = -inf, hence Q_fit = -inf, which passed the + # Q_fit < Q+1 branch test and attached A2 = -inf -- a silent all-NaN + # tail. The two-term branch now requires a FINITE Q_fit and falls + # back (warned) to the finite one-term tail instead. + x = np.array([8.0, 9.0, 10.0, 10.0]) + y = np.array([4.0, 4.8, 5.5, 5.4]) + with self.assertWarns(UserWarning): + f = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.6) + self.assertEqual(f.decay_extrap_terms, 1) + vals = f(np.array([12.0, 20.0, 100.0])) + self.assertTrue(np.all(np.isfinite(vals))) + + def test_terms_validation(self): + x, y = self.knots() + for bad in (0, 3, True, "2"): + with self.assertRaises(ValueError): + LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", + decay_extrap_Q=0.62, decay_extrap_terms=bad) + + def test_missing_terms_attribute_evaluates_one_term(self): + # instances unpickled from versions predating decay_extrap_terms are + # one-term constructions; eval must keep treating them as such + x, y, f = self.build(Q=0.62, terms=1) + q = np.geomspace(22.0, 5000.0, 30) + vals_before = f(q).copy() + del f.decay_extrap_terms + np.testing.assert_array_equal(f(q), vals_before) + + def test_two_term_pickle_roundtrip(self): + import pickle + + x, y, f = self.build(Q=0.62) + g = pickle.loads(pickle.dumps(f)) + q = np.geomspace(22.0, 5000.0, 30) + np.testing.assert_array_equal(f(q), g(q)) + np.testing.assert_array_equal(f.derivative(q), g.derivative(q)) + + def test_rescue_two_term_is_smooth(self): + # body still WIDENING its gap at the knot (slope below slope_limit): + # the two-term rescue extends it C1 -- the gap first keeps rising + # (interior maximum), then decays toward the line + x = np.linspace(1.0, 21.0, 201) + h = self.intercept / self.slope + y_div = self.intercept + self.slope * x - 0.1 * (x + h) ** 0.5 + f = LinearInterp(x, y_div, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.6) + self.assertEqual(f.decay_extrap_terms, 2) + slope_top = (y_div[-1] - y_div[-2]) / (x[-1] - x[-2]) + d_above = float(f.derivative(np.array([x[-1] + 1e-11]))[0]) + self.assertLess(abs(d_above - slope_top), 1e-9) # C1 at the knot + lad = np.geomspace(21.001, 1.0e5, 300) + gap = self.intercept + self.slope * lad - f(lad) + gap_top = self.intercept + self.slope * x[-1] - y_div[-1] + self.assertTrue(np.all(gap > 0.0)) + self.assertGreater(np.max(gap), gap_top) # interior maximum + self.assertLess(gap[-1], gap_top) # eventually decays below it + + +class TestLinearInterpMeasuredQ(unittest.TestCase): + """The measured-Q workflow on ``LinearInterp``: an exponent measured by + two log-log secants over the TOP THREE knots (in the shifted abscissa + ``x + h``), attached via ``decay_extrap_Q=Q2`` (the most local secant) + with the measurement riding along as ``q_diagnostics=(Q1, Q2, drift)`` + and readable back as ``self.local_q_diag`` -- the drop-in contract with + the downstream reference implementation (HAFiscal's + ``PowerLawDecayLinearInterp`` / its two-secant estimator). + + HARK-scale mirror of the HAFiscal (e) family + (``test_pf_asymptote_decay.py::test_powerlaw_flag_value_attaches_powerlaw_form``): + the same below-the-line / powerlaw-holds-the-gap-the-exponential-destroys / + in-sample-unchanged assertions, exercised against this file's synthetic + power-law truth instead of a solved consumption function, out to 75x the + grid top (the certification depth of the measured-Q construction). + + Pre-registered bounds (measured at authoring time; honest margins per + the platform-gating lesson on this branch, not byte-zero pins): on an + exact power-law fixture the two-secant measurement recovers the true + exponent to 1e-9 (measured 3e-12) with |drift| < 1e-9 (measured 6e-12); + the one-term attach equals the reference analytic tail to 1e-12 abs + (measured 0.0) and tracks the truth to 1e-12 rel (measured 1.3e-15); + the two-term default tracks the truth to 3e-5 rel (measured 3.2e-6) + while the exponential tail's worst error is >= 10x larger (measured + 65x) and destroys > 90% of the truth gap by ~5x the grid top (measured + gap ratio 0.053 at x=100). + """ + + intercept = 1.0 + slope = 0.5 # limiting line 1.0 + 0.5*x = 0.5*(x + 2); pivot h = 2 + C = 4.0 + Q_true = 1.5 + + def truth(self, x): + h = self.intercept / self.slope + return self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + + def knots(self, n=201, top=21.0): + x = np.linspace(1.0, top, n) + return x, self.truth(x) + + def measure(self, x, y): + """Two-secant measurement from the top three knots (the reference + estimator's semantics, computed inline: log-log secants of the gap + below the limiting line in the shifted abscissa ``x + h``).""" + h = self.intercept / self.slope + z = x[-3:] + h + gap = self.intercept + self.slope * x[-3:] - y[-3:] + lg, lz = np.log(gap), np.log(z) + Q1 = -(lg[1] - lg[0]) / (lz[1] - lz[0]) + Q2 = -(lg[2] - lg[1]) / (lz[2] - lz[1]) + return Q1, Q2, Q2 - Q1 + + def ladder(self, top=21.0): + return np.geomspace(top + 0.01, 75.0 * top, 400) + + def test_two_secant_recovers_truth_and_rider_attaches(self): + x, y = self.knots() + Q1, Q2, drift = self.measure(x, y) + # exact-power-law fixture: every log-log secant equals the exponent + self.assertLess(abs(Q2 - self.Q_true), 1e-9) + self.assertLess(abs(drift), 1e-9) + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + q_diagnostics=(Q1, Q2, drift), + ) + self.assertTrue(f.decay_extrap) + self.assertEqual(f.decay_extrap_form, "powerlaw") + self.assertEqual(f.decay_extrap_Q, Q2) + self.assertEqual(f.decay_extrap_Q_source, "explicit") + self.assertEqual(f.local_q_diag, (Q1, Q2, drift)) + + def test_rider_default_none_eval_neutral_and_survives_guard_disable(self): + x, y = self.knots() + Q1, Q2, drift = self.measure(x, y) + # default None, on every construction path (incl. plain exp form) + f_exp = LinearInterp(x, y, self.intercept, self.slope) + self.assertIsNone(f_exp.local_q_diag) + # rider never touches evaluation + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + q_diagnostics=(Q1, Q2, drift), + ) + f_no = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + ) + q = self.ladder() + np.testing.assert_array_equal(f(q), f_no(q)) + np.testing.assert_array_equal(f.derivative(q), f_no.derivative(q)) + # rider survives the guard-disable path (top knot above the line): + # diagnostics must remain inspectable exactly when the attach fails + h = self.intercept / self.slope + y_above = ( + self.intercept + self.slope * x + self.C * (x + h) ** (-self.Q_true) + ) + with self.assertWarns(UserWarning): + f_bad = LinearInterp( + x, y_above, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + q_diagnostics=(Q1, Q2, drift), + ) + self.assertFalse(f_bad.decay_extrap) + self.assertEqual(f_bad.local_q_diag, (Q1, Q2, drift)) + + def test_one_term_reference_form_identity(self): + # the terms=1 attach IS the reference implementation's tail: + # gap(x) = A * ((x + h)/(x_top + h))**(-Q2), A = level gap at the top + # knot; derivative slope_limit + Q2/(x + h) * gap + x, y = self.knots() + _, Q2, _ = self.measure(x, y) + f = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + decay_extrap_terms=1, + ) + q = self.ladder() + h = self.intercept / self.slope + z, z_top = q + h, x[-1] + h + A = self.intercept + self.slope * x[-1] - y[-1] + gap_ref = A * np.exp(-Q2 * np.log(z / z_top)) + c_ref = self.intercept + self.slope * q - gap_ref + self.assertLessEqual(np.max(np.abs(f(q) - c_ref)), 1e-12) + d_ref = self.slope + Q2 / z * gap_ref + self.assertLessEqual(np.max(np.abs(f.derivative(q) - d_ref)), 1e-12) + # and on the exact-power-law fixture it reproduces the truth itself + rel = np.max(np.abs(f(q) - self.truth(q)) / np.abs(self.truth(q))) + self.assertLessEqual(rel, 1e-12) + + def test_tail_below_line_beats_exp_in_sample_identical(self): + # the (e)-family core, at interpolation scale + x, y = self.knots() + Q1, Q2, drift = self.measure(x, y) + f_pl = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + q_diagnostics=(Q1, Q2, drift), + ) + f_exp = LinearInterp(x, y, self.intercept, self.slope) + q = self.ladder() + line = self.intercept + self.slope * q + c_pl, c_exp = f_pl(q), f_exp(q) + # (i) strictly below the limiting line everywhere out to 75x the top + self.assertTrue(np.all(c_pl < line)) + # (ii) weakly below the exponential variant (the power law holds the + # gap the exponential destroys), same 1e-9 slack as the mirror + self.assertTrue(np.all(c_pl <= c_exp * (1.0 + 1e-9))) + # (iii) gap destruction is material by ~5x the grid top ... + xx = np.array([100.0]) + gap_truth = self.C * (xx[0] + 2.0) ** (-self.Q_true) + gap_exp = self.intercept + self.slope * xx[0] - f_exp(xx)[0] + gap_pl = self.intercept + self.slope * xx[0] - f_pl(xx)[0] + self.assertLess(gap_exp / gap_truth, 0.1) + # ... while the measured-Q tail keeps it + self.assertGreater(gap_pl / gap_truth, 0.99) + self.assertLess(gap_pl / gap_truth, 1.01) + # (iv) in-sample: the tail form touches nothing at or below the top + m_in = np.linspace(x[0], x[-1], 313) + np.testing.assert_array_equal(f_pl(m_in), f_exp(m_in)) + + def test_far_field_truth_tracking_beats_exp(self): + x, y = self.knots() + Q1, Q2, drift = self.measure(x, y) + f_pl = LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=Q2, + q_diagnostics=(Q1, Q2, drift), + ) + f_exp = LinearInterp(x, y, self.intercept, self.slope) + q = self.ladder() + c_tr = self.truth(q) + rel_pl = np.max(np.abs(f_pl(q) - c_tr) / np.abs(c_tr)) + rel_exp = np.max(np.abs(f_exp(q) - c_tr) / np.abs(c_tr)) + # two-term default tracks the truth ... + self.assertLessEqual(rel_pl, 3e-5) + # ... at least 10x better than the exponential's worst error + self.assertGreater(rel_exp, 10.0 * rel_pl) + + +class TestDecayTailInterpTwoTerm(unittest.TestCase): + """Two-term default on the composable wrapper: byte parity with the + baked-in machinery over a linear body, C1 over a cubic body, and the + derivative requirement on bare callables.""" + + intercept = 1.0 + slope = 0.5 + C = 4.0 + Q_true = 1.5 + + def knots(self, n=201, top=21.0): + h = self.intercept / self.slope + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + return x, y + + def test_one_and_two_term_byte_parity_with_baked_in(self): + x, y = self.knots() + q = np.geomspace(21.0000001, 5000.0, 50) + for terms in (1, 2): + w = DecayTailInterp(LinearInterp(x, y), self.intercept, self.slope, + decay_extrap_Q=0.62, decay_extrap_terms=terms) + b = LinearInterp(x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=0.62, + decay_extrap_terms=terms) + self.assertEqual(w.decay_extrap_terms, terms) + np.testing.assert_array_equal(w(q), b(q)) + np.testing.assert_array_equal(w.derivative(q), b.derivative(q)) + + def test_two_term_C1_over_a_cubic_body(self): + h = self.intercept / self.slope + x = np.linspace(1.0, 21.0, 201) + y = self.intercept + self.slope * x - self.C * (x + h) ** (-self.Q_true) + dydx = self.slope + self.Q_true * self.C * (x + h) ** (-self.Q_true - 1.0) + body = CubicInterp(x, y, dydx) + f = DecayTailInterp(body, self.intercept, self.slope, + decay_extrap_Q=0.62) + self.assertEqual(f.decay_extrap_terms, 2) + d_body = float(body.derivative(np.array([21.0]))[0]) + d_above = float(f.derivative(np.array([21.0 + 1e-12]))[0]) + self.assertLess(abs(d_above - d_body), 1e-12) # C1 across the cut + # level match: amplitudes sum to the level gap at the cut + lvl = float(body(np.array([21.0]))[0]) + gap = self.intercept + self.slope * 21.0 - lvl + self.assertAlmostEqual(f.decay_extrap_A + f.decay_extrap_A2, gap, + places=15) + + def test_two_term_needs_a_derivative(self): + body = lambda z: 1.0 + 0.5 * np.asarray(z, dtype=float) - 3.0 / ( + np.asarray(z, dtype=float) + 2.0 + ) + with self.assertRaises(ValueError): + DecayTailInterp(body, 1.0, 0.5, x_cut=40.0, decay_extrap_Q=1.0) + f = DecayTailInterp(body, 1.0, 0.5, x_cut=40.0, decay_extrap_Q=1.0, + decay_extrap_terms=1) + self.assertTrue(f.decay_extrap) + + +class TestDecayTailInterpModeration(unittest.TestCase): + """The 'moderation_tail' form: tail-only use of the Method-of-Moderation + coordinates. Registered properties (plan_moderation_tail.md): C1 at ANY + cut with no guard (including cuts where the two-term form must fall + back), gap strictly inside (0, slope*hEx), asymptotic exponent Q, + exact collapse to the pinned-slope line when chip_cut == Q, refuse + (not warn-and-disable) on inconsistent moderation inputs, and opt-in + neutrality. Deep-probe measurements stop where the gap is still + representable against the line (gap > ~1e4 ulps of the level): beyond + that, evaluating "line minus tiny gap" saturates to the line in + float64 -- a representation floor of ANY decaying-gap form, not a + property of this one. + """ + + intercept = 1.0 + slope = 0.5 # optimist line 1 + 0.5*x; hNrm = 2 + Q = 1.0 + x_min = 0.0 # so mEx = x and hEx = 2; gap ceiling = 1.0 + + def steep_body(self, G=4.0, C=40.0, n=2001, top=21.0): + # gap = C*(x+h)^-G with G >> Q+1: the two-term guard trips here + h = self.intercept / self.slope + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - C * (x + h) ** (-G) + return LinearInterp(x, y) + + def mild_body(self, G=1.3, gap_cut=1e-2, n=2001, top=21.0): + # gap_cut sets C so the cut sits at omega_cut = gap_cut/ceiling + h = self.intercept / self.slope + C = gap_cut * (top + h) ** G + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - C * (x + h) ** (-G) + return LinearInterp(x, y) + + def rising_gap_body(self, n=2001, top=21.0): + # body slope BELOW slope_limit at the cut (gap widening): the + # fitted forms must disable here; moderation absorbs it (chip < 0) + x = np.linspace(1.0, top, n) + y = self.intercept + self.slope * x - (0.1 + 0.001 * x) + return LinearInterp(x, y) + + def wrap(self, body, **kw): + kw.setdefault("decay_extrap_form", "moderation_tail") + kw.setdefault("decay_extrap_Q", self.Q) + kw.setdefault("x_min", self.x_min) + return DecayTailInterp(body, self.intercept, self.slope, **kw) + + def test_C1_at_any_cut_including_guard_tripping_ones(self): + for body in ( + self.steep_body(G=2.5, C=15.0), + self.steep_body(G=4.0, C=40.0), + self.steep_body(G=6.0, C=200.0), + self.rising_gap_body(), + ): + f = self.wrap(body) + cut = f.x_cut + d_body = float(body.derivative(np.array([cut]))[0]) + d_tail = float(f.derivative(np.array([cut * (1 + 1e-12)]))[0]) + self.assertAlmostEqual(d_tail / d_body, 1.0, places=9) + # level continuity (the class invariant) + y_body = float(body(np.array([cut]))[0]) + y_tail = float(f(np.array([cut * (1 + 1e-13)]))[0]) + self.assertAlmostEqual(y_tail / y_body, 1.0, places=12) + + def test_bounds_gap_strictly_inside_zero_and_ceiling(self): + for body, floor_from in ( + (self.steep_body(), 1.0), # gap shrinking at the cut: + (self.rising_gap_body(), 2.0), # floor holds everywhere ... + ): # ... vs only past the bend + f = self.wrap(body) + q = np.geomspace(f.x_cut * (1 + 1e-10), 1e3 * f.x_cut, 4001) + gap = self.intercept + self.slope * q - f(q) + self.assertGreater(gap.min(), 0.0) + self.assertLess(gap.max(), f.decay_gap_ceiling) + # MPC floor f' > slope_limit holds where the gap is locally + # shrinking; a widening-gap cut (chip_cut < 0) C1-continues + # below slope_limit before the tail bends toward the line + deep = q >= floor_from * f.x_cut + self.assertGreaterEqual(f.derivative(q[deep]).min(), self.slope) + + def test_asymptotic_exponent_is_Q(self): + # mild slope mismatch => the exp(-u) correction is < 1e-3 over the + # fitted decade [1e3, 1e4]*cut while the gap stays representable + f = self.wrap(self.mild_body()) + q = np.geomspace(1e3 * f.x_cut, 1e4 * f.x_cut, 200) + gap = self.intercept + self.slope * q - f(q) + slope_fit = np.polyfit(np.log(q - self.x_min), np.log(gap), 1)[0] + self.assertAlmostEqual(slope_fit, -self.Q, delta=1e-3) + + def test_local_exponent_identity_exact(self): + # -dln(gap)/dln(mEx) == (1 - omega)*(Q + (chip-Q)*e^-u) is an exact + # identity of the form; verify by central log-differences + # dl = 0.05 keeps float-cancellation noise in the subtracted gap + # (the gap is ~1e5 ulps of the line at depth 300) well below the + # 1e-3 gate while truncation stays ~4e-4 at depth 3 + f = self.wrap(self.steep_body()) + for depth in (3.0, 30.0, 300.0): + xq = depth * f.x_cut + dl = 0.05 + qq = np.array([xq * np.exp(-dl), xq * np.exp(dl)]) + gap = self.intercept + self.slope * qq - f(qq) + measured = -(np.log(gap[1]) - np.log(gap[0])) / (2 * dl) + mEx = xq - self.x_min + u = np.log(mEx / f.decay_mEx_cut) + eu = f.decay_mEx_cut / mEx + chi = (f.decay_chi_cut + f.decay_extrap_Q * u + + (f.decay_chip_cut - f.decay_extrap_Q) * (1 - eu)) + omega = 1.0 / (1.0 + np.exp(chi)) + closed = (1 - omega) * ( + f.decay_extrap_Q + (f.decay_chip_cut - f.decay_extrap_Q) * eu + ) + self.assertAlmostEqual(measured / closed, 1.0, delta=1e-3) + + def test_collapse_to_pinned_line_when_chip_equals_Q(self): + # a body that IS a chi-line with slope Q: chip_cut == Q exactly, and + # the tail must reproduce the body's own extension to float precision + intercept, slope, Q, x_min = ( + self.intercept, self.slope, self.Q, self.x_min, + ) + h = intercept / slope + ceiling = slope * (h + x_min) + + class ChiLine: + def __init__(s, chi0, x0): + s.chi0, s.x0 = chi0, x0 + + def chi(s, x): + return s.chi0 + Q * np.log((x - x_min) / (s.x0 - x_min)) + + def __call__(s, x): + x = np.asarray(x, dtype=float) + om = 1.0 / (1.0 + np.exp(s.chi(x))) + return intercept + slope * x - ceiling * om + + def derivative(s, x): + x = np.asarray(x, dtype=float) + om = 1.0 / (1.0 + np.exp(s.chi(x))) + return slope + ceiling * om * (1 - om) * Q / (x - x_min) + + body = ChiLine(0.3, 21.0) + f = self.wrap(body, x_cut=21.0) + self.assertAlmostEqual(f.decay_chip_cut, Q, places=12) + probe = np.geomspace(21.0 * (1 + 1e-10), 100 * 21.0, 60) + np.testing.assert_allclose(f(probe), body(probe), rtol=1e-12) + np.testing.assert_allclose( + f.derivative(probe), body.derivative(probe), rtol=1e-10 + ) + + def test_guard_free_where_two_term_falls_back(self): + # at the same steep cut: two-term warns + falls back to one term + # with its closed-form kink; moderation attaches with NO kink + body = self.steep_body() + with self.assertWarns(UserWarning): + two = DecayTailInterp( + body, self.intercept, self.slope, decay_extrap_Q=self.Q + ) + self.assertEqual(two.decay_extrap_terms, 1) + cut = two.x_cut + d_body = float(body.derivative(np.array([cut]))[0]) + kink_two = abs( + float(two.derivative(np.array([cut * (1 + 1e-12)]))[0]) - d_body + ) + Q_fit = two.decay_extrap_B * two.decay_extrap_pivot + pred = ( + (Q_fit - self.Q) + * float(two.decay_extrap_A) + / two.decay_extrap_pivot + ) + self.assertAlmostEqual(kink_two / pred, 1.0, places=6) + f = self.wrap(body) + kink_mod = abs( + float(f.derivative(np.array([cut * (1 + 1e-12)]))[0]) - d_body + ) + self.assertLessEqual(kink_mod, 1e-9 * abs(d_body)) + + def test_applicability_violations_raise(self): + body = self.mild_body() + # missing Q + with self.assertRaises(ValueError): + DecayTailInterp( + body, self.intercept, self.slope, + decay_extrap_form="moderation_tail", x_min=0.0, + ) + # missing x_min + with self.assertRaises(ValueError): + DecayTailInterp( + body, self.intercept, self.slope, + decay_extrap_form="moderation_tail", decay_extrap_Q=1.0, + ) + # terms=1 is meaningless for a form that is C1 by construction + with self.assertRaises(ValueError): + self.wrap(body, decay_extrap_terms=1) + # x_min with the exp form + with self.assertRaises(ValueError): + DecayTailInterp( + body, self.intercept, self.slope, + decay_extrap_form="exp", x_min=0.0, + ) + # non-finite x_min + with self.assertRaises(ValueError): + self.wrap(body, x_min=np.inf) + # x_min at/above the cut + with self.assertRaises(ValueError): + self.wrap(body, x_min=25.0) + # hEx <= 0 + with self.assertRaises(ValueError): + self.wrap(body, x_min=-2.5) + # body at the cut BELOW the pessimist line (omega_cut >= 1) + with self.assertRaises(ValueError): + self.wrap(body, x_min=-1.99) + # body at the cut ON/ABOVE the optimist line (level_diff <= 0) + x = np.linspace(1.0, 21.0, 201) + above = LinearInterp(x, self.intercept + self.slope * x + 0.1) + with self.assertRaises(ValueError): + self.wrap(above) + # derivative-less bodies cannot slope-match + bare = lambda z: np.asarray(z, dtype=float) * 0.0 + 5.0 # noqa: E731 + with self.assertRaises(ValueError): + self.wrap(bare, x_cut=21.0) + + def test_opt_in_neutrality(self): + # without the new form, x_min stays None and no moderation state is + # created; the powerlaw paths are untouched (their byte-parity with + # LinearInterp is asserted by TestDecayTailInterpParity) + body = self.mild_body() + f = DecayTailInterp( + body, self.intercept, self.slope, decay_extrap_Q=self.Q + ) + self.assertIsNone(f.decay_x_min) + self.assertFalse(hasattr(f, "decay_chi_cut")) + self.assertEqual(f.decay_extrap_form, "powerlaw") + + def test_derivative_matches_finite_differences(self): + f = self.wrap(self.steep_body()) + q = np.geomspace(1.05 * f.x_cut, 200.0 * f.x_cut, 25) + eps = 1e-6 * q + fd = (f(q + eps) - f(q - eps)) / (2 * eps) + np.testing.assert_allclose(f.derivative(q), fd, rtol=1e-6) + + def test_deep_queries_saturate_to_the_line_without_warnings(self): + # far beyond float representability of the gap, the tail must + # evaluate to the line exactly, with finite derivative == slope + # and no runtime warnings (the chi clip) + import warnings + + f = self.wrap(self.steep_body()) + q = np.array([1e8 * f.x_cut, 1e12 * f.x_cut]) + with warnings.catch_warnings(): + warnings.simplefilter("error") + y = f(q) + d = f.derivative(q) + np.testing.assert_array_equal( + y, self.intercept + self.slope * q + ) + np.testing.assert_allclose(d, self.slope, rtol=1e-12) + + def test_guard_warning_enrichment(self): + # two-term trip WITH x_min: the warning carries the exact + # steepness diagnosis and both remedies + body = self.steep_body() + with self.assertWarnsRegex( + UserWarning, + r"moderation_tail.*s_mu=.*amplification.*guard-safe boundary", + ): + DecayTailInterp( + body, self.intercept, self.slope, + decay_extrap_Q=self.Q, x_min=self.x_min, + ) + # LinearInterp's baked-in trip warning names the same remedies + h = self.intercept / self.slope + x = np.linspace(1.0, 21.0, 2001) + y = self.intercept + self.slope * x - 40.0 * (x + h) ** (-4.0) + with self.assertWarnsRegex( + UserWarning, r"human-wealth-scale.*moderation_tail" + ): + LinearInterp( + x, y, self.intercept, self.slope, + decay_extrap_form="powerlaw", decay_extrap_Q=self.Q, + ) + + def test_pickle_roundtrip(self): + import pickle + + f = self.wrap(self.steep_body()) + g = pickle.loads(pickle.dumps(f)) + q = np.geomspace(1.01 * f.x_cut, 500.0 * f.x_cut, 40) + np.testing.assert_array_equal(f(q), g(q)) + np.testing.assert_array_equal(f.derivative(q), g.derivative(q)) diff --git a/tests/test_theorem_refs.py b/tests/test_theorem_refs.py new file mode 100644 index 000000000..e58a32080 --- /dev/null +++ b/tests/test_theorem_refs.py @@ -0,0 +1,92 @@ +"""CI wrapper for tools/update_theorem_refs.py (theorem-ref tag hygiene). + +The power-law-decay PR carries theorem-ref tags — pinned citations into the +private HAFiscal-Latest theorem repo (theory/powerlaw-decay/) — in its comments +and docstrings. ``tools/update_theorem_refs.py --check`` verifies tag SYNTAX +always, and tag RESOLUTION (file + section heading + optional label exist at +the pinned commit) whenever a theorem-repo checkout is available. + +Pre-registered expectations (declared before first run; never weakened): + * WITHOUT the theorem repo (the public econ-ark CI case) the checker must + DEGRADE GRACEFULLY: syntax-check only, print a NOTICE, exit 0. + * It must find at least ``MIN_EXPECTED_TAGS`` tags (the PR landed 22; a + collapse to fewer than 20 means tags were mass-deleted or the scanner + broke) and 0 malformed. + * WITH a local theorem-repo checkout (private-side dev boxes), every tag + must additionally RESOLVE at its pin: exit 0 and no UNRESOLVED lines. + +Note: this file deliberately never spells the uppercase tag token, so the +scanner has nothing to parse here. +""" + +import os +import subprocess +import sys +import unittest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +TOOL = os.path.join(REPO_ROOT, "tools", "update_theorem_refs.py") + +# Known private-side checkout of the theorem repo (worktree pin 71ca7c61); +# $THEOREM_REF_REPO overrides. Absent on public CI — that path is the +# graceful-degradation test. +THEOREM_REPO = os.environ.get( + "THEOREM_REF_REPO", + "/home/shared/github/llorracc/HAFiscal-Latest/.worktrees/powerlaw-theorem", +) + +MIN_EXPECTED_TAGS = 20 + + +def _run_check(extra_args, env_overrides=None): + env = dict(os.environ) + env.pop("THEOREM_REF_REPO", None) # the test controls repo availability + if env_overrides: + env.update(env_overrides) + return subprocess.run( + [sys.executable, TOOL, "--check"] + extra_args, + cwd=REPO_ROOT, + env=env, + capture_output=True, + text=True, + timeout=300, + ) + + +class TestTheoremRefsCheck(unittest.TestCase): + def test_check_without_theorem_repo_degrades_gracefully(self): + """No theorem repo: syntax-check only, NOTICE printed, exit 0.""" + r = _run_check([]) + self.assertEqual( + r.returncode, 0, + f"--check without the theorem repo must exit 0, got " + f"{r.returncode}\nstdout:\n{r.stdout}\nstderr:\n{r.stderr}", + ) + self.assertIn("NOTICE", r.stdout, r.stdout) + self.assertIn(" 0 malformed", r.stdout, r.stdout) + n_tags = int(r.stdout.rsplit("check: ", 1)[1].split(" tag(s)")[0]) + self.assertGreaterEqual( + n_tags, MIN_EXPECTED_TAGS, + f"expected >= {MIN_EXPECTED_TAGS} tags, scanner found {n_tags}", + ) + + @unittest.skipUnless( + os.path.isdir(os.path.join(THEOREM_REPO, ".git")) + or os.path.isfile(os.path.join(THEOREM_REPO, ".git")), + "theorem repo checkout not available (public CI)", + ) + def test_check_with_theorem_repo_all_tags_resolve(self): + """Theorem repo present: every tag resolves at its pin; exit 0.""" + r = _run_check(["--repo", THEOREM_REPO]) + self.assertEqual( + r.returncode, 0, + f"--check with the theorem repo must exit 0, got " + f"{r.returncode}\nstdout:\n{r.stdout}\nstderr:\n{r.stderr}", + ) + self.assertNotIn("UNRESOLVED", r.stdout, r.stdout) + self.assertNotIn("MALFORMED", r.stdout, r.stdout) + self.assertIn(" 0 finding(s) total", r.stdout, r.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/update_theorem_refs.py b/tools/update_theorem_refs.py new file mode 100755 index 000000000..fa8c4d066 --- /dev/null +++ b/tools/update_theorem_refs.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python3 +"""update_theorem_refs.py — scan, verify, and re-pin THEOREM-REF tags. + +Home-agnostic, stdlib-only. Lives next to the tagged code (HARK branch); the +theorem repo (HAFiscal-Latest, private) is addressed via --repo / $THEOREM_REF_REPO. + +HOME: ``tools/`` at the HARK repo root — HARK already keeps repo utility +scripts there (``tools/nb_exec.py``), while ``tests/`` holds unittest suites +only; a scan/re-pin CLI belongs with the former. Run from the repo root: + + python3 tools/update_theorem_refs.py --check + python3 tools/update_theorem_refs.py --check --repo + +The no-paths default scan (git-tracked *.py under the cwd, see below) covers +every tagged file in the repo, currently: HARK/ConsumptionSaving/pf_decay.py, +HARK/interpolation.py, HARK/ConsumptionSaving/ConsAggShockModel.py, +tests/ConsumptionSaving/test_pf_decay.py, tests/test_interpolation.py. +CI wrapper: tests/test_theorem_refs.py (degrades gracefully when the private +theorem repo is absent, e.g. on public econ-ark CI). + +TAG GRAMMAR (one tag per line, inside a comment or docstring) +============================================================== + + # THEOREM-REF[ @ :: ::
[::