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.implemented
— Functionimplemented(function, Tuple{Arg1Type, Arg2Type})
Check whether there is an implementation available that will return a suitable value.
POMDPLinter.@implemented
— Macro@implemented function(::Arg1Type, ::Arg2Type)
Check whether there is an implementation available that will return a suitable value.
POMDPLinter.check_requirements
— Functioncheck_requirements(r::AbstractRequirementSet)
Check whether the methods in r
have implementations with implemented()
. Return true if all methods have implementations.
POMDPLinter.show_requirements
— Functionshow_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.
POMDPLinter.get_requirements
— Functionget_requirements(f::Function, args::Tuple)
Return a RequirementSet for the function f and arguments args.
POMDPLinter.requirements_info
— Functionrequirements_info(s::Union{Solver, Simulator}, p::Union{POMDP,MDP}, ...)
Print information about the requirement for solver s.
POMDPLinter.@POMDP_require
— Macro@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.
POMDPLinter.@POMDP_requirements
— Macroreqs = @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.
POMDPLinter.@warn_requirements
— Macro@warn_requirements solve(solver, problem)
Print a warning if there are unmet requirements.
POMDPLinter.@req
— Macro@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.
POMDPLinter.@subreq
— Macro@subreq f(arg1, arg2)
In a @POMDP_requirements
or @POMDP_require
block, include the requirements for f(arg1, arg2)
as a child argument set.