Redesign the solver seam as an ABC (GridSearch + DCEGM stub) - #388
Conversation
Route the existing brute-force grid search through a per-regime solver configuration and a builder registry, with no change to the numerics. - `Regime.solver: BruteForce | DCEGM` (default `BruteForce()`), exported from `lcm` alongside the `DCEGM` configuration class. - `_lcm.solution.registry`: `SolverBuildContext`, `SolverKernels`, the `SolverKernelBuilder` protocol, and `_build_brute_force_kernels` (the former `_build_max_Q_over_a_per_period`), dispatched on `type(regime.solver)` via `SOLVER_KERNEL_BUILDERS`. - `DCEGM` is published as the final configuration surface (fields + field validation), but its engine is not yet wired in; a regime requesting it is rejected at model build with `NotImplementedError`. Behavior-preserving: the full test suite passes unchanged and an explicit `BruteForce()` yields the same value function as the default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full discrete-continuous endogenous grid method, re-rooted as a single commit on top of the solver-selection seam (PR #388). The tree reproduces the reviewed feat/dcegm tip exactly, minus two root-level audit scratch files. On top of the seam this adds the `_lcm.egm` engine, DC-EGM build-time validation, the `_build_dcegm_kernels` builder and the EGM-specific SolverBuildContext / SolverKernels / SolutionPhase fields, and replaces the seam's build-time `NotImplementedError` guard with the real solver. To be split into reviewable sub-PRs at review time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Benchmark comparison (main → HEAD)Comparing
|
Points the benchmark feature's aca-model at feat/dcegm-solver's tip — main's audit fixes + the DC-EGM solver + smooth-share eligibility — the version the solver-seam work is developed against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
timmens
left a comment
There was a problem hiding this comment.
I really like the idea of making the backward induction solver a replaceable component.
I am not a huge fan of the current implementation, but I could be convinced otherwise. Some things that I find odd:
- To me a
Solverabstraction is the perfect use-case for inheritance, or at least aProtocolinterface. I have looked for a single interface / abstraction in the PR that tells me what the "solver contract" is. Having a clear abstraction should also make it simple for users to implement custom solvers (thinking of optimagic's algorithm interface here). - The tests don't seem to test anything interesting again.
- I actually don't see why we need a DCEGM stub in this PR. I would expect the first PR to design a solver interface / abstraction, for which we then implement the
GridSearch(Solver)and only check that this works. Then in the next PR we add DCEGM.
Replace the `type(solver)`-keyed builder registry with a polymorphic `Solver` ABC. The engine now calls `solver.validate(context)` then `solver.build_period_kernels(context)` — no `SOLVER_KERNEL_BUILDERS` dict, no `BruteForce | DCEGM` union, no standalone DC-EGM guard. - `_lcm/solution/contract.py` (new): the `Solver` ABC (abstract `build_period_kernels`, default no-op `validate`), `SolverBuildContext`, and `SolverKernels`. An engine leaf — imports nothing that reaches `lcm.solvers`, so the façade can re-export it without an import cycle. - `_lcm/solution/solvers.py` (new): `GridSearch(Solver)` (the relocated grid-search builder, with function-local `jax`/`get_max_Q_over_a` imports) and `DCEGM(Solver)` (the published config; `validate` raises the not-yet-available guard, so a regime requesting it is rejected at model build). - `lcm/solvers.py` → thin re-export façade; `registry.py` deleted; the `processing` dispatch and `Regime.solver` field updated. - Rename the default solver `BruteForce` → `GridSearch` (more descriptive; alpha permits the break). Faithful to dcegm-solver-seam-abc-design.md (ABC, not Protocol). Layer 2 (generic KernelResult) does not apply here — the stub seam has no EGM fork. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
44e8522 to
e0fecdd
Compare
mj023
left a comment
There was a problem hiding this comment.
I also like the object-oriented approach more. I think the stub is fine, the PR is small enough for this. The interface is a bit limiting as I can imagine that some solvers (Reinforcement learning) would need different aspects of the Regime, like the transition functions, or would return something else then the max_Q_over_A functions. But the interface is not complicated, so I would think we can cahnge this if we ever want to implement such a solver.
The full discrete-continuous endogenous grid method, re-rooted as a single commit on top of the solver-selection seam (PR #388). The tree reproduces the reviewed feat/dcegm tip exactly, minus two root-level audit scratch files. On top of the seam this adds the `_lcm.egm` engine, DC-EGM build-time validation, the `_build_dcegm_kernels` builder and the EGM-specific SolverBuildContext / SolverKernels / SolutionPhase fields, and replaces the seam's build-time `NotImplementedError` guard with the real solver. To be split into reviewable sub-PRs at review time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e0fecdd to
44e8522
Compare
The full discrete-continuous endogenous grid method, re-rooted as a single commit on top of the solver-selection seam (PR #388). The tree reproduces the reviewed feat/dcegm tip exactly, minus two root-level audit scratch files. On top of the seam this adds the `_lcm.egm` engine, DC-EGM build-time validation, the `_build_dcegm_kernels` builder and the EGM-specific SolverBuildContext / SolverKernels / SolutionPhase fields, and replaces the seam's build-time `NotImplementedError` guard with the real solver. To be split into reviewable sub-PRs at review time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The solve module's job is backward induction; name it for that. Renames src/_lcm/solution/solve_brute.py → backward_induction.py and tests/solution/test_solve_brute.py → test_backward_induction.py, updates every import (model.py, simulation/compile.py, test_beartype_claw.py), the two test-function names, and the doc/docstring references. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A module-level jnp.array constant triggers the PREALLOCATE=true pool reservation on device 0 at import time — before any solve, and even in processes that only import the model code to schedule work. A second such process then OOMs on device 0 at startup. Document the host-array (numpy) pattern for module-level constants in the tuning guide's memory section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Behavior-preserving refactor that introduces a per-regime solver-selection
seam as a polymorphic
SolverABC, so the upcoming DC-EGM PR is a pureaddition whose review can concentrate on the DC-EGM logic alone.
What this does
Solver(ABC)(_lcm/solution/contract.py): an abstractbuild_period_kernels(*, context) -> SolverKernelsplus a default no-opvalidate(*, context). The engine dispatches polymorphically on the solverinstance —
solver.validate(context)thensolver.build_period_kernels(context)— with no switch on solver type and noBruteForce | DCEGMunion.contract.pyis a true engine leaf (no importcycle:
import lcmresolves cleanly).GridSearch(Solver)(the renamedBruteForce) andDCEGM(Solver)live in_lcm/solution/solvers.py, withvalidate+build_period_kernelsmethodsand deferred numerical imports;
lcm/solvers.pyis a thin re-export façade.The
type(solver)-keyedSOLVER_KERNEL_BUILDERSregistry andregistry.pyare deleted.
Regime.solver: Solver(defaultGridSearch()), exported fromlcmalongside the
DCEGMconfiguration class.DCEGMis published as the final configuration surface (fields + fieldvalidation), but its engine is not yet wired in: a regime requesting it is
rejected at model build with a clear
NotImplementedError.Not in scope (the DC-EGM PR, stacked on top of this)
The
_lcm/egm/*engine, build-time DC-EGM validation, theSolutionPhaseEGMfields, the EGM-specific
SolverBuildContext/SolverKernelsfields, and theDC-EGM kernel builder.
solve_brute.pyand the simulation phase are unchanged.Verification
is a faithful move;
solver=GridSearch()explicitly ≡ the default valuefunction.
tests/test_solvers.pyasserts the default solver, the polymorphic-dispatch parity,
DCEGMconstruction + field validation, and the build-time guard.
tyandprekclean.🤖 Generated with Claude Code