epistemic-robotics

The Epistemic Robotics Project

Execution layer · eplansys · ROS 2

Running a policy on a robot

A planner that reasons about knowledge is only half of an autonomous system. The other half has to execute what it returns, and what it returns is not a sequence of actions but a policy, branching on what the robot turns out to observe. This page reports the execution layer built for that: the shape a policy takes on the wire, the behavior tree that runs it, and the epistemic state the tree consults while it does.

Implemented in eplansys, on top of PlanSys2. Technical report (PDF)

The mismatch

Three things a classical executor assumes

PlanSys2 executes a plan by rendering it as a behavior tree: one subtree per action, in order, each guarding its action with the PDDL conditions around it. Nothing about that is wrong. It is exactly right for a plan that commits to one future. Every assumption in it fails for a plan that does not.

A plan is a sequence. plansys2_msgs/Plan is a flat list of timed items, so item \(i\) is followed by item \(i+1\). A policy for a partially observable domain is a tree: after a sensing action, which action comes next depends on what was seen. Flattening it keeps one contingency and silently assumes it happens.

An action has one name. The grounder writes an action as a single token, pickup-A-hold_r2, while the executor splits (pickup r2 A) into a name and parameters and looks the name up in the PDDL domain to find the behavior tree that drives the hardware. These are two vocabularies, not two spellings of one, and an untranslated name matches nothing: the plan arrives, and the robot does not move.

A precondition is a fact. The problem expert holds what is true, and answers whether a PDDL condition holds. The corridor is clear lives there. r1 knows whether the corridor is clear is not a fact about the corridor at all; no set of predicates records it, and nothing in a classical stack can be asked.

A robot that plans to sense before committing needs all three of these to change, and none of them is a change to the planner.

The policy on the wire

One message type, extended and not replaced

Two interfaces stand between the planner and the executor and both take a flat plan: the solver plugin returns one, and the tree builder consumes one. Rather than fork either, the policy travels in additional fields of the existing message, so that the two components keep exchanging the same type.

Field on PlanItemWhat it carries
childrenone continuation per possible outcome, by item index
outcomesthe observation selecting each, in the same order
sensingwhether the outcome must be observed at all
knowledge_requirementsthe epistemic conditions the action needs
epistemic_actionthe action's name in the planner's own vocabulary

A classical planner sets none of them, and such a plan is read exactly the way PlanSys2 writes it. So every plan is a policy here, and the distinction that matters is whether the policy branches. That reading is plan-wide and not per item: once any node names a continuation, a node without one ends its branch instead of falling through to the next item, which would be a different plan entirely.

One subtlety cost real work. The executor keys its table of running actions by action expression and start time in milliseconds. Two branches running the same action at the same time would collide onto one entry and drive one action executor from two places, invisible until the day the second branch is taken, because branches are alternatives that never both run. Times are therefore assigned uniquely across the whole policy, each node strictly after its parent ends.

The behavior tree

PlanSys2's subtree, and the three components around it

Each policy node renders as the PlanSys2 action subtree, unchanged, the same nodes driving the same action performers against the same problem expert, wrapped in what a sequence cannot express.

One policy node, rendered coin-in-the-box · multi-pointed
Sequence node 0 · (peek A):0 EPISTEMIC CheckKnowledge (K A looking_A): asked of the epistemic state, not the problem expert PLANSYS2 · UNCHANGED WaitAtStartReq → ApplyAtStartEffect ReactiveSequence [ CheckOverAllReq, ExecuteAction ] CheckAtEndReq → ApplyAtEndEffect the action performer runs here EPISTEMIC ApplyEpistemicUpdate product update; writes the observed outcome EPISTEMIC EpistemicSwitch outcomes = e-peek-pos ; e-peek-neg observed e-peek-pos Sequence · node 1 · (shout-tails A) observed e-peek-neg AlwaysSuccess: the policy ends here EPISTEMIC CheckEpistemicGoal (Kw A tails): asked once, of the state that actually resulted A node with a single continuation renders without a switch, so a plan that never branches comes out as the flat sequence PlanSys2 would have built.
The extension is additive. The grey block is PlanSys2's action subtree, untouched. The four red nodes are what a policy needs on top of it, and the switch is the only one with no classical counterpart at all.
NodePlanSys2 counterpartWhat it adds
CheckKnowledge CheckOverAllReq conditions about knowledge, which no predicate records
ApplyEpistemicUpdate ApplyAtEndEffect the DEL product update, one level above the effects
EpistemicSwitch none runs the continuation planned for what was observed
CheckEpistemicGoal the executor's goal check asks whether the goal holds, and does not assume assuming a leaf implies it

Two of these deserve their reasoning stated. An action changes what agents know by more than its own effects. A public announcement informs everyone who was listening, and an observation that finds nothing still rules worlds out, which is why the update is a distinct node and not part of applying an effect.

And the switch fails on an outcome the policy does not list, and does not default to a branch. Every branch was built for a different belief; running one anyway is a robot acting confidently on a belief nothing supports. The failure propagates to the executor, whose answer to a failed plan is to replan, from the state that actually resulted.

Reaching a leaf is not achieving the goal

Every leaf of a policy was believed to satisfy the goal, but that belief was formed against the model at planning time. If execution diverged from it, a policy can run to completion in a branch that no longer establishes anything. The goal check runs once, after the tree, against the state that resulted.

The epistemic state

An expert for knowledge, alongside the expert for facts

The tree needs something to ask. A lifecycle node holds a pointed Kripke model \(\mathcal{M} = (W, \{R_i\}, V, W^{*})\) and answers three questions, which are exactly the three that executing a policy raises.

ServiceQuestion
load_taskbe the model of this grounded task
check_formuladoes (K r1 (clear corridor)) hold now?
apply_actionthis action ran: update, and say what was observed

It advances by executed actions and not by watching the world, which is what makes it a belief state and not a log. When it disagrees with what the robot observed, the disagreement surfaces at apply_action as an outcome the model cannot account for, a reason to replan, not to overwrite the model quietly.

Which outcome occurred is a question about the world, not about the model. When the state designates a single world it already answers it. When it designates several it genuinely does not know, and the observation has to come from whoever did the sensing.

Live trace · multi-pointed coin-in-the-box verbatim service responses
# A must come to know whether the coin lies tails. It starts not knowing.
check_formula  (Kw A tails)          → holds=False

# The box is shut, so looking is not yet possible. The guard says so.
apply_action   peek_A                → refused: not applicable in the
                                        current epistemic state

apply_action   open_A                → ok · 4 worlds, 2 designated

# Now A can look. But the model designates two candidate worlds, so it
# cannot know which outcome the world produced, and it declines to guess.
apply_action   peek_A                → refused: 2 possible outcomes here
                                        and none was observed

# The robot reports what it saw. The model narrows to one world.
apply_action   peek_A  e-peek-pos    → ok · outcome=e-peek-pos
                                        6 worlds, 1 designated

check_formula  (Kw A tails)          → holds=True   # A looked
check_formula  (Kw B tails)          → holds=False  # B was not looking
The last two lines are the point. A looked and now knows; B was in the room and still does not. Quasi-private sensing, surviving intact from the event model through the product update to a service answer a behavior tree can act on. The two refusals are the design working, not failing: a guard that will not pass an inapplicable action, and a model that will not invent an observation it cannot have.

Structure

Where the dependency falls

Extending an executor usually means depending on it. That was worth avoiding: a package that needs plansys2_executor can only be built against this fork, while one that does not can be built and tested against a released distribution.

PackageContentsNeeds the executor
plansys2_epistemic_msgsthe three service definitionsno
plansys2_epistemic_executorpolicy, tree rendering, the four nodes, the stateno
plansys2_epistemic_bt_builderthe tree-builder pluginyes

The nodes reach the executor through a generic hook and not a special case: a parameter naming behavior tree node libraries to load before a tree is built, and the plan already published on the blackboard. Twenty-four lines in the executor, no new dependencies, and nothing in it that knows what an epistemic node is.

# the whole configuration, on top of the ordinary PlanSys2 params
executor:
  ros__parameters:
    bt_builder_plugin: "EpistemicBTBuilder"
    bt_node_plugins:   ["libplansys2_epistemic_bt_nodes.so"]

Verification

What is checked, and what is not

PackageTestsFailures
plansys2_epistemic_planner380
plansys2_epistemic_executor390
plansys2_epistemic_msgs50
from a clean build of six packages820

Three of these are worth naming. Every rendered tree is handed to the real BehaviorTree.CPP parser, because a tree that will not parse is worthless however good it looks. The formula written by the planner is parsed back by the state and compared as a formula, not as a string. The two are separate processes with separate interning, and nothing but that agreement connects them. And the node library is loaded exactly as the executor loads it, so the extension mechanism is checked.

Not checked here: the four nodes ticking against a live state node under a full stack, and the builder inside a running executor. Both need the whole PlanSys2 system up, which this fork's sources cannot build on the distribution available locally; they are exercised in continuous integration. The live trace above was produced against the real state node, driven by hand.

Open work

The observation a sensing action produces is supplied on a port that a domain-specific tree binds to whatever its performer reports. Wiring that to real perception, so that the branch a robot takes is chosen by its sensors and not by a model that happens to designate one world, is the next step, and the one that turns this from an architecture into a demonstration.

See it run

The same layer, on a robot

Everything above is the architecture: the policy on the wire, the tree that runs it, the epistemic state it consults. The six-room survey is that layer executing a mission end to end, with the knowledge coming from a map the robot builds with its own laser while it drives.

A knowledge goal, executed

An EPDDL mission over six rooms, a policy that branches on an observation not yet made, and a recorded run in which the branch is decided by what the occupancy grid turns out to say.

The six-room survey