Hierarchical Task Network planning with uncertainty in Ruby
This is an extension of HyperTensioN to work with disjunctions, probabilities, rewards, axioms, assignments, calls and semantic attachments, currently incompatible with the original intermediate representation. Hype can help you in the conversion process from UJSHOP to Ruby, but you can ignore the Hype and use HyperTensioN U as a standalone library.
Usage:
Hype domain problem [output] [max plans=-1(all)] [min probability=0]
Output:
rb - generate Ruby files to HyperTensioN U(default)
run - same as rb with execution
debug - same as run with execution log
To convert and execute the cookie example is simple:
ruby Hype.rb examples/cookie/cookie.ujshop examples/cookie/pb1.ujshop run
Or call the Ruby problem:
# Use with debug option
ruby examples/cookie/pb1.rb debug
# No need to call Hype twice, the problem will load the domain
ruby examples/cookie/pb1.ujshop.rb debug
The expected input for HyperTensioN U is based on a modified version of the JSHOP formalism. Two files define domain and problem as a planning instance. The domain defines the rules that never change, while the problem defines a situation that requires planning. Several problems may refer to the same domain, as many situations may happen within the same constraints. Differently from JSHOP descriptions the operators may have uncertain effects with known probabilities instead of a cost. Rewards are used to better evaluate which plan is better, instead of the total plan cost. External function calls and semantic attachments are also available.
; This is a comment line
(defdomain domain-name (
; Reward function
(:rewards ; Any numeric can be a reward
(achieve (pre1 a) 10) ; Obtaining (pre1 a) from one state to another adds 10 to valuation
(maintain (pre2 b) -0.5) ; Keeping (pre2 b) from one state to another subtracts 0.5 from valuation
)
(:operator (!op-name1 ?t1 ?t2)
; Preconditions
() ; empty set
; Del effects
()
; Add effects
(
(pre1 ?t2)
)
; Probability
1 ; 100% is the default probability
)
(:operator (!op-name2 ?t1)
; Preconditions
(and ; Expressions with AND OR NOT are supported by operator preconditions, AND can be omitted
(pre1 ?t1)
(pre2 ?t1)
)
op-name2-label1 ; Label can be omitted, otherwise must be unique
; Del effects
(
(pre3 ?t1)
)
; Add effects
()
0.8 ; 80% probability
op-name2-label2 ; Another set of effects for this action
; Del effects
()
; Add effects
(
(pre3 ?t1)
)
0.2 ; 20% probability
)
(:method (method-name ?t1 ?t2)
label ; Label can be omitted, otherwise must be unique
; Preconditions
(
(pre1 ?t1)
(not (pre2 ?t2))
)
; Subtasks
(
(!op-name1 ?t1)
(method-name ?t2 ?t1)
)
; Other decompositions for the method may be put here
)
))
(defproblem problem-name domain-name
; Start with this ground predicates as true
(
(pre1 object)
)
; Tasks to be executed
(
(method-name object another-object)
)
)
Sometimes functions must be called to solve a problem beyond the reach of declarative descriptions. Basic functions are already implemented:
- Binary math
+
,-
,*
,/
,%
,^
- Unary math
abs
,sin
,cos
,tan
- Comparison
=
,!=
,<
,>
,<=
,>=
- List
member
(call < (call abs (call - (call sin ?var) 0.5)) 1)
Other functions can be used through external calls the user implements in the external.rb
file in the same folder as the domain.
The external.rb
must define a Domain_name
module with methods that are expected to return String objects, numbers are expected to be in the Float format (5.to_f.to_s == "5.0"
).
This is a requirement of the generate
method that expects Strings to replace variables, if you only need to forward values through subtasks you can ignore this limitation.
Calls that only operate on external structures must return a truthy value to avoid failing preconditions.
Instance variables from HyperTensioN U can be accessed directly.
An example of calls is available at examples/external.
Note that the state of external structures is not implicitly saved, which may impact search results that try to decompose using other methods or operator effects.
To avoid this problem one can limit the number of plans to be searched, add more preconditions or explicitly duplicate such structures.
The most common case is what would happen if a task consumed an element from the queue and later on failed, that element would not be in the queue anymore and a different decomposition would take place.
Meta calls are possible through send
to use variables as function names, (call send ?function ?param1 ?param2)
.
The result of an expensive call may be necessary across several terms. Instead of repeating the entire call, one can create a new variable and assign the value of such call. Assignments are limited to preconditions.
(assign ?newvar (call - ?var 5))
Some predicates are too complex for the user to describe with addition and deletion effects from the state, like (visible ?agent ?object)
after ?agent
is moved.
These predicates either require external structures or libraries to be fast and easy to maintain.
Instead of using calls in unusual ways to discover all the objects that are visible for a certain agent, we can exploit off-the-shelf libraries and delegate this unification to an external procedure.
Such external methods are semantic attachments, a term coined by Weyhrauch (1980) to describe the attachment of an interpretation to a predicate symbol using an external procedure.
Semantic attchments in planning was already explored by Christian Dornhege et al. (2009).
A semantic attachment signature must be explicitly defined as such and can be used as regular predicates in preconditions.
(:attachments (visible ?agent ?object))
Semantic attachment are defined in external.rb
, like external calls, but implemented to yield
unifications instead of return
values.
Free variables are used as terms and expected to be assigned by the semantic attachment to possible values before resuming control back to the HTN, or return in case of failure to unify more values.
Since all variables are Strings the semantic attachment implementation must replace all empty Strings, free variables, with actual values before yielding.
The same semantic attachment can be repeatedly used to unify different free variables.
Ground variables are not expected to be replaced, in case all variables are ground the semantic attachment yields if the current values satisfy.
An example of semantic attachments is available at examples/search_circular.
def visible(agent, object)
pos = POSITION[agent]
# Agent is ground and object is free
if object.empty?
MAP[pos].each {|obj|
object.replace(obj)
yield
}
# Both variables are ground
elsif MAP[pos].include?(object)
yield
end
end
- Add list variable support
- Support complex expressions in method preconditions