Specifying Requirements

Purpose

When a researcher or student wants to use a solver in the POMDPs ecosystem, the first question they will ask is "What do I have to implement to use this solver?". The requirements interface provides a standard way for solver writers to answer this question.

Internal interface

The most important functions in the requirements interface are get_requirements, check_requirements, and show_requirements.

get_requirements(f::Function, args::Tuple{...}) should be implemented by a solver or simulator writer for all important functions that use the POMDPs.jl interface. In practice, this function will rarely by implemented directly because the @POMDP_require macro automatically creates it. The function should return a RequirementSet object containing all of the methods POMDPs.jl functions that need to be implemented for the function to work with the specified arguments.

check_requirements returns true if all of the requirements in a RequirementSet are met, and show_requirements prints out a list of the requirements in a RequirementSet and indicates which ones have been met.

@POMDP_require

The @POMDP_require macro is the main point of interaction with the requirements system for solver writers. It uses a special syntax to automatically implement get_requirements. This is best shown by example. Consider this @POMDP_require block from the DiscreteValueIteration package:

@POMDP_require solve(solver::ValueIterationSolver, mdp::Union{MDP,POMDP}) begin
    M = typeof(mdp)
    S = statetype(mdp)
    A = actiontype(mdp)
    @req discount(::M)
    @subreq ordered_states(mdp)
    @subreq ordered_actions(mdp)
    @req transition(::M,::S,::A)
    @req reward(::M,::S,::A,::S)
    @req stateindex(::M,::S)
    as = actions(mdp)
    ss = states(mdp)
    s = first(ss)
    a = first(as)
    dist = transition(mdp, s, a)
    D = typeof(dist)
    @req support(::D)
    @req pdf(::D,::S)
end

The first expression argument to the macro is a function signature specifying what the requirements apply to. The above example implements get_requirements{P<:Union{POMDP,MDP}}(solve::typeof(solve), args::Tuple{ValueIterationSolver,P}) which will construct a RequirementSet containing the requirements for executing the solve function with ValueIterationSolver and MDP or POMDP arguments at runtime.

The second expression is a begin-end block that specifies the requirements. The arguments in the function signature (solver and mdp in this example) may be used within the block.

The @req macro is used to specify a required function. Each @req should be followed by a function with the argument types specified. The @subreq macro is used to denote that the requirements of another function are also required. Each @subreq should be followed by a function call.

requirements_info

While the @POMDP_require macro is used to specify requirements for a specific method, the requirements_info function is a more flexible communication tool for a solver writer. requirements_info should print out a message describing the requirements for a solver. The exact form of the message is up to the solver writer, but it should be carefully thought-out because problem-writers will be directed to call the function (via the @requirements_info macro) as the first step in using a new solver.

By default, requirements_info calls show_requirements on the solve function. This is adequate in many cases, but in some cases, notably for online solvers such as MCTS, the requirements for solve do not give a good indication of the requirements for using the solver. Instead, the requirements for action should be displayed. The following example shows a more informative version of requirements_info from the MCTS package. Since action requires a state argument, requirements_info prompts the user to provide one.

function POMDPs.requirements_info(solver::AbstractMCTSSolver, problem::Union{POMDP,MDP})
    if statetype(typeof(problem)) <: Number
        s = one(statetype(typeof(problem)))
        requirements_info(solver, problem, s)
    else
        println("""
            Since MCTS is an online solver, most of the computation occurs in `action(policy, state)`. In order to view the requirements for this function, please, supply a state as the third argument to `requirements_info`, e.g.

                @requirements_info $(typeof(solver))() $(typeof(problem))() $(statetype(typeof(problem)))()

                """)
    end
end

function POMDPs.requirements_info(solver::AbstractMCTSSolver, problem::Union{POMDP,MDP}, s)
    policy = solve(solver, problem)
    requirements_info(policy, s)
end

function POMDPs.requirements_info(policy::AbstractMCTSPolicy, s)
    @show_requirements action(policy, s)
end

@warn_requirements

The @warn_requirements macro is a useful tool to improve usability of a solver. It will show a requirements list only if some requirements are not met. It might be used, for example, in the solve function to give a problem writer a useful error if some required methods are missing (assuming the solver writer has already used @POMDP_require to specify the requirements for solve):

function solve(solver::ValueIterationSolver, mdp::Union{POMDP, MDP})
    @warn_requirements solve(solver, mdp)

    # do the work of solving
end

@warn_requirements does perform a runtime check of requirements every time it is called, so it should not be used in code that may be used in fast, high-performance loops.

Determining whether a function is implemented

When checking requirements in check_requirements, or printing in show_requirements, the implemented function is used to determine whether an implementation for a function is available. For example implemented(discount, Tuple{NewPOMDP}) should return true if the writer of the NewPOMDP problem has implemented discount for their problem. In most cases, the default implementation,

implemented(f::Function, TT::TupleType) = method_exists(f, TT)

will automatically handle this, but there may be cases in which you want to override the behavior of implemented, for example, if the function can be synthesized from other functions. Examples of this can be found in the default implementations of the generative interface funcitons.

API

POMDPLinter.implementedFunction
implemented(function, Tuple{Arg1Type, Arg2Type})

Check whether there is an implementation available that will return a suitable value.

source
POMDPLinter.@implementedMacro
@implemented function(::Arg1Type, ::Arg2Type)

Check whether there is an implementation available that will return a suitable value.

source
POMDPLinter.check_requirementsFunction
check_requirements(r::AbstractRequirementSet)

Check whether the methods in r have implementations with implemented(). Return true if all methods have implementations.

source
POMDPLinter.show_requirementsFunction
show_requirements(r::AbstractRequirementSet)

Check whether the methods in r have implementations with implemented() and print out a formatted list showing which are missing. Return true if all methods have implementations.

source
POMDPLinter.@POMDP_requireMacro
@POMDP_require solve(s::CoolSolver, p::POMDP) begin
    PType = typeof(p)
    @req states(::PType)
    @req actions(::PType)
    @req transition(::PType, ::S, ::A)
    s = first(states(p))
    a = first(actions(p))
    t_dist = transition(p, s, a)
    @req rand(::AbstractRNG, ::typeof(t_dist))
end

Create a get_requirements implementation for the function signature and the requirements block.

source
POMDPLinter.@POMDP_requirementsMacro
reqs = @POMDP_requirements CoolSolver begin
    PType = typeof(p)
    @req states(::PType)
    @req actions(::PType)
    @req transition(::PType, ::S, ::A)
    s = first(states(p))
    a = first(actions(p))
    t_dist = transition(p, s, a)
    @req rand(::AbstractRNG, ::typeof(t_dist))
end

Create a RequirementSet object.

source
POMDPLinter.@reqMacro
@req f( ::T1, ::T2)

Convert a f( ::T1, ::T2) expression to a (f, Tuple{T1,T2})::Req for pushing to a RequirementSet.

If in a @POMDP_requirements or @POMDP_require block, marks the requirement for including in the set of requirements.

source
POMDPLinter.@subreqMacro
@subreq f(arg1, arg2)

In a @POMDP_requirements or @POMDP_require block, include the requirements for f(arg1, arg2) as a child argument set.

source