Skip to content

ENH: Add the POMDPs.jl extension for DiscreteDP - #405

Merged
oyamad merged 13 commits into
masterfrom
pomdps-extension
Oct 10, 2026
Merged

oyamad merged 13 commits into
masterfrom
pomdps-extension

Conversation

@oyamad

@oyamad oyamad commented Aug 11, 2026

Copy link
Copy Markdown
Member

Close #398

This PR adds the package extension QuantEconPOMDPsExt connecting our DiscreteDP to the JuliaPOMDP ecosystem.

  • POMDPs.jl is a package that provides a core interface for working with MDPs (Markov decision processes) and POMDPs (partially observable Markov decision processes). (DiscreteDP is a finite MDP.)
  • POMDPs and POMDPTools are added as weak dependencies (weakdeps).
  • The extension QuantEconPOMDPsExt is activated only with using POMDPs, POMDPTools.
  • The change is purely additive: no existing behavior changes.

Features

Modeling interface

This allows us to use the POMDPs.jl interface as a simple interface for specifying a DiscreteDP model (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):

using QuantEcon, POMDPs, POMDPTools

# Model specification as a subtype of POMDPs.MDP{S,A}
struct Household{TZ<:MarkovChain,TA<:AbstractVector,TU} <:
        POMDPs.MDP{Tuple{Float64,Float64},Float64}
    r::Float64
    w::Float64
    sigma::Float64
    beta::Float64
    z_chain::TZ
    a_vals::TA
    u::TU
end

function Household(; r = 0.01,
                   w = 1.0,
                   sigma = 1.0,
                   beta = 0.96,
                   z_chain = MarkovChain([0.9 0.1; 0.1 0.9], [0.1; 1.0]),
                   a_min = 1e-10,
                   a_max = 18.0,
                   a_size = 200,
                   a_vals = range(a_min, a_max, length = a_size),
                   u = sigma == 1 ? x -> log(x) :
                       x -> (x^(1 - sigma) - 1) / (1 - sigma))
    return Household(r, w, sigma, beta, z_chain, a_vals, u)
end

# The POMDPs.jl interface
POMDPs.states(am::Household) =
    Iterators.product(am.a_vals, am.z_chain.state_values)
POMDPs.actions(am::Household) = am.a_vals
POMDPs.actions(am::Household, (a, z)::Tuple) =
    (a_new for a_new in am.a_vals if am.w * z + (1 + am.r) * a - a_new > 0)
POMDPs.reward(am::Household, (a, z)::Tuple, a_new) =
    am.u(am.w * z + (1 + am.r) * a - a_new)
POMDPs.transition(am::Household, (a, z)::Tuple, a_new) =
    SparseCat([(a_new, z_new) for z_new in am.z_chain.state_values],
              am.z_chain.p[findfirst(==(z), am.z_chain.state_values), :])
POMDPs.discount(am::Household) = am.beta

am = Household(; a_max = 20.0, r = 0.03, w = 0.956)

# A native DiscreteDP can be constructed from a POMDPs.MDP model
am_ddp = DiscreteDP(am)
results = QuantEcon.solve(am_ddp, PFI)

# The rest is the same as in #402
a_vals = am.a_vals
z_vals = am.z_chain.state_values
pf = DDPPolicyFunction(results)               # (a, z) -> next period assets
a_star = [pf((a, z)) for a in a_vals, z in z_vals]

# To obtain the controlled MarkovChain
mc = markov_chain(results)  # or mc = results.mc
K = sum(stationary_distributions(mc)[1] .* first.(mc.state_values))

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.

# Household defined as above
am = Household(; a_max = 20.0, r = 0.03, w = 0.956)

# Our `POMDPs.Solver`
solver = DiscreteDPSolver(PFI)     # solver isa POMDPs.Solver
policy = POMDPs.solve(solver, am)  # policy isa POMDPs.Policy

# queries
a_vals = am.a_vals
z_vals = am.z_chain.state_values
s = (a_vals[117], z_vals[2])            # (a, z) ≈ (11.658, 1.0)
action(policy, s)                       # sigma(a, z)
value(policy, s)                        # v(a, z)

a_star = [action(policy, (a, z)) for a in a_vals, z in z_vals]  # same table as above

# To obtain the controlled MarkovChain
mc = markov_chain(policy)
K = sum(stationary_distributions(mc)[1] .* first.(mc.state_values))

See "Interacting with Policies" in the POMDPs.jl documentation for details on working with the policy object, and "Implemented Simulators" for the tools that consume it (note simulate, like solve, must be qualified as POMDPs.simulate).
The native DPSolveResult remains reachable as policy.res.

Questions

  1. Do we allow this package extension, with POMDPs and POMDPTools as weakdeps?
  2. This PR will in effect have our DiscreteDP join the JuliaPOMDP ecosystem. Are we comfortable with that?

oyamad and others added 6 commits August 2, 2026 19:12
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>
@oyamad

oyamad commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Detailed description about the implementation, generated by Claude Code Fable 5:


Implementation notes

  • The importer DiscreteDP(m::POMDPs.MDP; sparse=Val(true)) is index-free: it enumerates states(m) and actions(m) and builds its own value-to-index maps (stateindex/actionindex/initialstate are not consumed), attaching the enumerations as state_values/action_values from ENH: Add state_values/action_values to DiscreteDP #402 so that policies decode to model objects. The default output is the state-action pair form with a sparse transition matrix; sparse=Val(false) builds the dense product form directly.
  • Rewards are the expected reward(m, s, a, sp) under transition(m, s, a) (3-argument models are covered by the POMDPs.jl fallback). Terminal states are encoded as zero-reward self-loops under every global action, so their value is exactly zero and actions(m, s) may be empty at terminal states. Transition weights are taken as-is (only exactly-zero branches are dropped), and numerical data are normalized to Float64, the convention of the ecosystem's own tabulation (SparseTabularMDP).
  • The core gains only entry points: the markov_chain(res) accessor, the DiscreteDPSolver function stub whose MethodError hint directs users to load the trigger packages, the two exports (markov_chain, DiscreteDPSolver), and the Project.toml weakdeps entries. Everything else lives in the extension.
  • DiscreteDPSolver(algo; sparse, max_iter, epsilon, k) carries the algorithm and the native solve options; the returned DiscreteDPPolicy is queried by state value, and the native DPSolveResult stays reachable as policy.res.
  • Model export (as_mdp, exposing a DiscreteDP as a POMDPs.MDP) ships internal, as round-trip test infrastructure; its public naming is deferred.

Documentation and tests

  • API page docs/src/api/QuantEconPOMDPsExt.md; worked example ext/examples/ifp_transient_shocks_pomdps.jl, the POMDPs.jl twin of examples/ifp_transient_shocks.jl from ENH: Add state_values/action_values to DiscreteDP #402, with the discretization (quadrature plus Young's lottery) inside transition — the model, not the tabulation, owns it. It is placed under ext/examples/ because it requires the weakdep trigger packages.
  • test/test_pomdps.jl runs last in the suite (loading POMDPs makes the bare solve ambiguous in Main; test_ddp.jl tests the method-less stub and its hint before the extension loads): importer structure and expected values, terminal states with empty actions(m, s) in both storage forms, the sparse option, cross-validation against POMDPTools' SparseTabularMDP, validation errors (duplicate states, transitions leaving the state space), round trips through the internal as_mdp, and the solver/policy surface. POMDPs and POMDPTools are test-target dependencies, so regular CI exercises the extension; the solver also passes POMDPTools' test_solver smoke test on SimpleGridWorld and TabularMDP/RandomMDP.

For the release notes: new feature — the QuantEconPOMDPsExt extension, with new exports markov_chain and DiscreteDPSolver.

🤖 Generated with Claude Code (Claude Fable 5)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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 # Fields section. 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.

Comment thread test/test_pomdps.jl
Comment thread ext/QuantEconPOMDPsExt.jl
Comment thread src/markov/ddp.jl
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>
oyamad added a commit to QuantEcon/ContinuousDPs.jl that referenced this pull request Sep 5, 2026
Add the public type ActionInterval(lo, hi) to the core (minimum,
maximum, in, rand): the set-valued counterpart of a one-dimensional
ContinuousActions at one state, and the representation of a feasible
action set at the POMDPs.jl interface. Model import: when
actions(m, s) returns an ActionInterval, the model is solved with a
continuous scalar action space whose state-dependent bounds are the
interval's endpoints, by the derivative-free inner solver (the
general-tier kernel forces Brent); a finite actions(m) keeps the
existing discrete path. Model export returns ActionInterval for
continuous action spaces (replacing the ext-internal CDPActionInterval).

Add ext/examples/cdp_ex_aiyagari_pomdps.jl: the Aiyagari household with
the same POMDPs.jl model specification as QuantEcon/QuantEcon.jl#405,
solved by collocation with continuous assets and a continuous savings
choice (productivity enters through a piecewise-linear basis with nodes
at its values), and aggregate capital from a long rollout.

Tests: the interval-action import matches the native ContinuousActions
problem solved with Brent to roundoff; action() stays within the
interval; model export's interval type. Docs, README, and the agent
instructions updated for the widened action requirement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
oyamad and others added 2 commits October 10, 2026 12:18
Co-Authored-By: Codex (GPT-6 Astra) <codex@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oyamad and others added 3 commits October 10, 2026 13:57
test_pomdps.jl is wrapped in a module that loads the packages as a user does (using QuantEcon, POMDPs, POMDPTools), so the tests run in the user's namespace and the POMDPs exports stay out of Main. The DiscreteDPSolver stub test moves from test_ddp.jl to the top of that module, before POMDPs and POMDPTools are loaded, and is skipped if the extension is already active. The file no longer has to run last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Check each action against a per-action state marker before evaluating its transition. Both sparse and dense tabulation now reject the first duplicate with the same state-and-action error, independently of the reward value. This fixes dense import accepting duplicates with infinite rewards and avoids waiting for the native constructor to reject sparse duplicates.

Reuse one integer array throughout tabulation, without clearing the global action space at every state. Cover adjacent and nonadjacent duplicates with finite and infinite rewards, verify that duplicate transitions are never evaluated, and preserve action reuse across states in both formulations.

Validation: focused POMDPs/DDP tests and the full test suite passed.

Co-Authored-By: Codex (GPT-6 Astra) <codex@openai.com>
@oyamad
oyamad merged commit 1ad70e0 into master Oct 10, 2026
10 checks passed
@oyamad
oyamad deleted the pomdps-extension branch October 10, 2026 06:50
oyamad added a commit to QuantEcon/ContinuousDPs.jl that referenced this pull request Oct 10, 2026
* ENH: Add the POMDPs.jl interface extension

ext/ContinuousDPsPOMDPsExt.jl, activated by the dual-trigger weakdeps
[POMDPs, POMDPTools]. The public surface is the solver direction:
POMDPs.solve(::CollocationSolver, m) solves any explicit-finite
POMDPs.jl MDP by collocation, wrapping the model's transition as a
general-tier _TransitionKernel (_POMDPKernel over weighted_iterator)
carried in a kernel-injected ContinuousDP, and returns a
CollocationPolicy <: POMDPs.Policy (action = policy functor, value =
value functor, native result reachable as policy.res). Requirement
checks with informative errors: finite action set; no terminal state at
any collocation node (v1 restriction; self-loop encoding is the planned
follow-up); at least one feasible action per node (models legitimately
error on infeasible pairs, which are mapped to -Inf and never
evaluated); reward arity chosen by a probe call, with the expected form
over branches as the fallback.

The model direction is internal: ext-local as_mdp (no core stub; the
public naming is deferred to a QuantEcon-owned generic) returning
CDPMDP{S,A}, with multi-dimensional states as NTuple{N,Float64} via a
statedim keyword (a zero-dependency alternative to the SVector plan),
interval action sets for continuous actions, and initialstate accepted
as a state, number, or distribution.

Tests: adapter equivalence against native solves (identical policy,
coefficients to roundoff) on a discrete-action growth model and a
2-D-state belief MDP (trimmed odu); expected-form reward; simulators
(stepthrough); requirement-check errors; model-direction conformance,
Symbol action types, statedim=2, and the exact round trip through the
POMDPs entrance. Full suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* TST: Add the rollout-consistency cross-check; widen node agreement

Port the two remaining test assets from the parked pomdps-interface
branch, adapted to the current adapter API: the mean discounted return
of seeded RolloutSimulator episodes must match the fitted value within
Monte Carlo error, and the policy/value node agreement is checked over
the full collocation-node sweep instead of three sample points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* DOC: Add the POMDPs.jl odu example (both definition styles)

Counterpart of examples/cdp_ex_odu.jl: the same belief MDP defined in
the POMDPs.jl interface — as an explicit problem type and as a
QuickMDP — and solved through POMDPs.solve(CollocationSolver(basis), m).
The file is written for POMDPs.jl users and self-checks the two styles
against each other; the correspondence with the native interface is
recorded here instead:

- The native factorization (g, shocks, weights) appears pre-composed
  as the transition distribution: the SparseCat support is g applied
  to the quadrature nodes (`shocks`, the same vector as the native
  example's) and its probabilities are the native state-dependent
  callable weights(s) evaluated at the current belief.
- Acceptance becomes an explicit Deterministic branch instead of an
  identity g.
- Validated against the native example: basis coefficients agree to
  ~1.2e-10 (the general-traversal tier vs the structured callable-
  weights path), reservation wages agree to the grid point at all
  probed beliefs, and a seeded rollout reproduces the same acceptance
  event. The two POMDPs definition styles agree bitwise with each
  other.

Placed under ext/examples/ because it needs the extension's trigger
packages (POMDPs, POMDPTools, QuickPOMDPs) plus Distributions, beyond
the ContinuousDPs ecosystem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* DOC: Document the POMDPs.jl interface

Add docs/src/pomdps.md, leading with the solver direction: solving
explicit-finite POMDPs.jl MDPs with CollocationSolver via POMDPs.solve.
Contents: a savings-rate optimal growth QuickMDP example (validated to
reproduce the analytic constant rate), the requirement set checked at
solve time, the CollocationPolicy semantics, the belief-MDP twin of
examples/cdp_ex_odu.jl with the kernel-equivalence note, the
QuantEcon/POMDPs solve-and-simulate name-clash table, and why
CollocationSolver does not subtype POMDPs.Solver. The internal model
direction is deliberately undocumented. Add the page to the Documenter
pages and short cross-referencing sections to README.md and
docs/src/index.md. Both code examples were run end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* FIX: Convert solver coordinates to the model's declared state type

The adapter passed the core's coordinate points -- node-row views in the multidimensional case -- directly to the model's methods, so a model whose actions/reward/transition methods are typed on statetype(m) (e.g. NTuple{2,Float64}) hit MethodErrors, or silently dispatched to the global actions(m) fallback instead of its actions(m, s) method.

States now cross the package boundary through a converter fixed once at solve time by probing the first collocation node: pass-through on exact match (zero-cost, and always the case for scalar states), the natural elementwise conversion for Tuple state types, POMDPs.convert_s when the model provides a method for the coordinate form, and the documented indexable-points contract otherwise. Every converter is a no-op on an already-converted state, so nested application is safe. Regression test: a fully typed 2-D MDP agrees exactly with its untyped QuickMDP twin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* FIX: Reject a scalar initial state in as_mdp when statedim > 1

A Real initialstate combined with statedim > 1 fell through to the distribution branch, storing a bare number as the initial-state object, so POMDPs.initialstate(m) returned a number rather than a distribution. The malformed combination is now rejected with an informative error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* MAINT: Tighten interface-probe error handling; refine POMDPs requirement docs

The actions(m) and reward-arity probes no longer swallow InterruptException, and the actions(m) probe now includes the original error text in its ArgumentError instead of discarding it.

Documentation refinements: terminal states must be absent from the entire basis domain, not only at the collocation nodes (which are checked; off-node states are the model's responsibility); actions(m, s) must be a subset of actions(m), since actions outside the global set are not seen by the solver; comments and docstrings now say model import/export instead of the solver/model direction wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ENH: Accept continuous scalar actions through the POMDPs.jl interface

Add the public type ActionInterval(lo, hi) to the core (minimum,
maximum, in, rand): the set-valued counterpart of a one-dimensional
ContinuousActions at one state, and the representation of a feasible
action set at the POMDPs.jl interface. Model import: when
actions(m, s) returns an ActionInterval, the model is solved with a
continuous scalar action space whose state-dependent bounds are the
interval's endpoints, by the derivative-free inner solver (the
general-tier kernel forces Brent); a finite actions(m) keeps the
existing discrete path. Model export returns ActionInterval for
continuous action spaces (replacing the ext-internal CDPActionInterval).

Add ext/examples/cdp_ex_aiyagari_pomdps.jl: the Aiyagari household with
the same POMDPs.jl model specification as QuantEcon/QuantEcon.jl#405,
solved by collocation with continuous assets and a continuous savings
choice (productivity enters through a piecewise-linear basis with nodes
at its values), and aggregate capital from a long rollout.

Tests: the interval-action import matches the native ContinuousActions
problem solved with Brent to roundoff; action() stays within the
interval; model export's interval type. Docs, README, and the agent
instructions updated for the widened action requirement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* DOC: Solve the docs' growth model with continuous actions; test QuickMDP interval actions

The documentation example now declares the feasible actions of the
familiar growth model (reward, transition, and action bounds) as an
ActionInterval through QuickMDP's function-valued `actions`, keeping the
consumption choice continuous; finite action sets are described in one
sentence. The returned-policy section covers both action forms. A test
pins the QuickMDP function-valued interval path: identical coefficients
to the explicit-type interval model.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* DOC: Define the Aiyagari example in both styles (explicit type and QuickMDP)

Add the QuickMDP definition of the household model to the example, with
its interval-valued `actions` through QuickMDP's function form, and
cross-check it against the explicit problem type (identical
coefficients), mirroring the odu example's structure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* ENH: Accept an ActionInterval as the action space of a ContinuousDP

A constant ActionInterval is the state-independent one-dimensional
continuous action space: ContinuousActions(itv) builds it with constant
bound functions, and every ContinuousDP constructor form (keyword,
positional, copy) accepts an interval as `actions`. ActionInterval
itself stays a set at one state, not an ActionSpace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Revert "ENH: Accept an ActionInterval as the action space of a ContinuousDP"

This reverts commit 17d29fe.

* ENH: Address Copilot review of the POMDPs.jl extension

- ActionInterval requires finite bounds
- Continuous actions require Float64 <: actiontype(m)
- as_mdp requires the weights to form a probability vector
- CollocationPolicy refreshes its policy functor after set_eval_nodes!
- Fix the index.md overview wording (finite actions or an interval)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* FIX: Accept non-vector action collections and skip zero-probability branches in the POMDPs.jl extension

- vec(collect(actions(m))): a finite collection may collect into a
  matrix (e.g. a Cartesian product of tuple-valued actions)
- The POMDPs kernel skips exact-zero weights before evaluating the
  payload (a SparseCat may list impossible successors where the model's
  reward is non-finite or errors); negative or non-finite weights still
  propagate
- rand(rng, ::ActionInterval) uses a clamped convex combination, whose
  value stays finite when the interval width overflows

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* DOC: Drop the "explicit-finite" label; state finite support explicitly

"Explicit-finite MDP" was a coinage from the planning notes, not
POMDPs.jl terminology, and it became inaccurate once the extension
accepted a continuous scalar action interval besides finite action
sets: the label said finite while its own gloss said otherwise. With
a continuous state space it also invited the wrong reading.

Remove the label from the docs page, README, index.md, the extension
source, and the agent instructions, describing the accepted models
directly instead, and bring the two summaries that still said only
"finite actions" (extension header comment, agent instructions) in
line with the action-interval support.

The one requirement the label still carried, that transition
distributions have finite support, is now stated in the requirements
list of the docs page and in the POMDPs.solve docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* MAINT: Reword the action-enumeration error of the POMDPs.jl extension; test it

The error raised when actions(m) cannot be collected said the solver
"requires an explicit finite action set" and then offered an
ActionInterval as the alternative, which is not finite. State what
failed instead (the action set could not be enumerated), and name
what was being collected rather than "it".

The branch was untested: add a model that defines only the
state-dependent actions(m, s) and check the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* DOC: Note the Float64 action-type requirement; link the Aiyagari example

The continuous-action path of the POMDPs.jl interface solves and
returns Float64 actions and requires the model's action type to accept
them, which POMDPs.solve checks; the requirements list on the docs page
did not say so. Also link the Aiyagari household example shipped in
ext/examples from the page (readers of the rendered docs have no other
path to it), and reflow a paragraph left ragged by the previous edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* MAINT: Update QuantEcon compat to 0.20

QuantEcon 0.20 ships its own POMDPs.jl extension, which loads alongside
ours; the full suite passes with 0.20.0 resolved and both active.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ENH: Add a POMDPs.jl interface for DiscreteDP (extension or companion package)

2 participants