ENH: Add the POMDPs.jl extension for DiscreteDP - #405
Open
oyamad wants to merge 7 commits into
Open
Conversation
Implements issue #398: QuantEcon's exact solution methods (VFI/PFI/MPFI) become available to explicit-finite POMDPs.jl models, via the first package extension in the repository (weakdeps POMDPs v1 and POMDPTools v1, dual trigger). Core gains two POMDPs-independent additions: the accessor markov_chain(res) = res.mc (the uniform vocabulary over results and policies; `@inferred`-tested), and the exported function stub DiscreteDPSolver with a MethodError hint (registered in a new __init__) directing users to load POMDPs and POMDPTools. The extension ext/QuantEconPOMDPsExt.jl provides: the constructor method DiscreteDP(m::POMDPs.MDP), an index-free tabulator into the state-action pair sparse form that enumerates states(m)/actions(m), builds its own IndexMaps (whose uniqueness validation doubles as a buffer-reuse detector), computes expected rewards in the 4-argument form under weighted_iterator, encodes isterminal states as zero-reward self-loops (value exactly 0), checks closure of the state space under transitions with informative errors, and attaches state_values/action_values so results self-decode; the options-carrying solver struct DiscreteDPSolver{Algo} <: POMDPs.Solver with a single qualified method extending the core generic (zero-argument default is VFI, mirroring native solve); POMDPs.solve returning DiscreteDPPolicy <: POMDPs.Policy, which forwards action/value to the core decode functors (sharing one IndexMap) and keeps the native result reachable as policy.res with recorded solver options; and an internal, testing-grade as_mdp exposing a DiscreteDP as a POMDPs.MDP for round trips (public naming of the export direction is deferred). Tests (test/test_pomdps.jl, run last since loading POMDPs makes the bare name solve ambiguous): importer structure and expected-reward values, terminal-state encoding, cross-validation of R and Q against POMDPTools.SparseTabularMDP, validation errors (duplicate states, transitions leaving the state space), exact round trips native -> as_mdp -> importer over both formulations including random models, and solver/policy behavior including option recording. POMDPs and POMDPTools are test-target dependencies, so the regular CI matrix exercises the extension. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The companion of examples/ifp_transient_shocks.jl: the same model of https://python.quantecon.org/ifp_egm_transient_shocks.html written as a POMDPs.jl model in six interface methods, with the Gauss-Hermite quadrature and the Young lottery living inside the model's transition method (the discretization is part of the model), tabulated by the extension's importer, and solved both result-centric (DiscreteDP(hh) + native solve + DDPPolicyFunction + markov_chain figures) and in the POMDPs idiom end to end (DiscreteDPSolver, action/value, stepthrough). Relative to the native construction, everything index-shaped disappears from user code: no R/Q assembly, no -Inf convention, and no IndexMap (the importer owns the index maps; the analysis side uses enumerate). The example requires POMDPs, POMDPTools, and Plots, which are not package dependencies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extension's docstrings (the DiscreteDP importer method, DiscreteDPSolver, DiscreteDPPolicy) are rendered on a new API page below the QuantEcon section. The docs build loads POMDPs and POMDPTools (added to the docs project) to activate the extension and passes the extension module to makedocs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion Experimental. DiscreteDP(m::POMDPs.MDP; sparse=Val(true)) selects the tabulation formulation: Val(true) (the default) gives the state-action pair form with sparse storage, Val(false) the dense product form, constructed directly (n and the number of actions are known upfront, so the final R and Q arrays are the only allocations; no counting pass, no transient state-action pair representation). DiscreteDPSolver(algo; sparse=Val(true), ...) carries the same flag as a type parameter, so the formulation is determined in the type domain end to end. Supersedes the earlier dense-SA-storage design on this branch: targeting dense storage within the state-action pair formulation required a pair-counting pass and a storage-dispatched accumulator, while the product form is both simpler (two independent tabulation methods over a shared header) and allocation-optimal; the dense product output is verified equal to to_product_form of the sparse import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eights through as-is Terminal states are now encoded as zero-reward self-loops under every global action, so models whose per-state action set is empty at terminal states (a natural POMDPs.jl idiom) import correctly in both storage forms; previously the sparse form emitted no pair and the dense form left an all--Inf row, and both constructors rejected the result. Regression test added for both forms. The branch skip in tabulation changes from w > 0 to iszero(w): the importer no longer silently drops negative or NaN weights, which used to alter the model (e.g. a NaN branch imported as a valid renormalized transition). Invalid weights now pass through exactly as they would in a native constructor call, whose validity contract is the caller's; the docstring records this and the Float64 normalization of imported data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
Detailed description about the implementation, generated by Claude Code Fable 5: Implementation notes
Documentation and tests
For the release notes: new feature — the 🤖 Generated with Claude Code (Claude Fable 5) |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds an optional POMDPs.jl extension that converts finite MDP models to DiscreteDP and exposes QuantEcon solvers as POMDP policies.
Changes:
- Adds POMDP model tabulation, solver, policy, and controlled-chain integration.
- Registers POMDPs/POMDPTools as weak dependencies.
- Adds tests, API documentation, and an income-fluctuation example.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
Project.toml |
Configures extension dependencies and test targets. |
ext/QuantEconPOMDPsExt.jl |
Implements the POMDP bridge. |
ext/examples/ifp_transient_shocks_pomdps.jl |
Demonstrates extension usage. |
src/QuantEcon.jl |
Exports APIs and registers an error hint. |
src/markov/ddp.jl |
Adds solver stub and Markov-chain accessor. |
test/test_pomdps.jl |
Adds extension tests. |
test/test_ddp.jl |
Tests core APIs without the extension loaded. |
test/runtests.jl |
Runs POMDP tests last. |
docs/Project.toml |
Adds documentation dependencies. |
docs/make.jl |
Loads and documents the extension. |
docs/src/api/QuantEconPOMDPsExt.md |
Adds the extension API page. |
Suppressed comments (1)
ext/QuantEconPOMDPsExt.jl:179
- This docstring is attached to a type, but it presents a constructor signature and omits the required
# Fieldssection. Document the type signature and its three stored fields; constructor usage can remain in the prose or receive a separate method docstring.
DiscreteDPSolver(algo=VFI; sparse=Val(true), max_iter=250,
epsilon=1e-3, k=20)
POMDPs.jl solver based on the `DiscreteDP` solution methods. `algo` is
one of `VFI`, `PFI`, or `MPFI`; `sparse` selects the tabulation
formulation (`Val(true)` for state-action pair form with sparse
storage, `Val(false)` for dense product form; value-typed, and carried
as a type parameter of the solver); the remaining keyword options are
those of `solve`. `POMDPs.solve(solver, m)` tabulates `m` via
`DiscreteDP(m)`, solves it, and returns a `DiscreteDPPolicy`.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The importer DiscreteDP(m::POMDPs.MDP) docstring and the core DiscreteDPSolver stub docstring gain the # Arguments and # Returns sections required by the docstring style guide. The extension's DiscreteDPSolver docstring, which presented a constructor signature while attached to the struct, is split per the guide's type/function convention: the struct docstring now shows the parametric type name with a # Fields section, and the constructor method carries the signature docstring with # Arguments and # Returns. Docs build checked (no new warnings; the interpolation warnings on the core API page pre-date this change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Close #398
This PR adds the package extension
QuantEconPOMDPsExtconnecting ourDiscreteDPto the JuliaPOMDP ecosystem.POMDPs.jlis a package that provides a core interface for working with MDPs (Markov decision processes) and POMDPs (partially observable Markov decision processes). (DiscreteDPis a finite MDP.)POMDPsandPOMDPToolsare added as weak dependencies (weakdeps).QuantEconPOMDPsExtis activated only withusing POMDPs, POMDPTools.Features
Modeling interface
This allows us to use the POMDPs.jl interface as a simple interface for specifying a
DiscreteDPmodel (see also QuantEcon/QuantEcon.py#228). As an example, consider the Aiyagari model as described in https://julia.quantecon.org/multi_agent_models/aiyagari.html (compare the corresponding script in #402):Joining the JuliaPOMDP ecosystem
This extension exposes our solution methods through a
POMDPs.Solver(DiscreteDPSolver), so we can use tools from the JuliaPOMDP ecosystem to work with the model and the computed policy.See "Interacting with Policies" in the POMDPs.jl documentation for details on working with the
policyobject, and "Implemented Simulators" for the tools that consume it (notesimulate, likesolve, must be qualified asPOMDPs.simulate).The native
DPSolveResultremains reachable aspolicy.res.Questions
POMDPsandPOMDPToolsas weakdeps?DiscreteDPjoin the JuliaPOMDP ecosystem. Are we comfortable with that?