OmniPlan is a ROS 2 framework for automated task planning and execution. It integrates multiple classical planners with flexible execution mechanisms including direct action implementation, state machines and behavior trees. The framework supports both knowledge base and knowledge graph approaches for state management, enabling planning solutions for robotic applications. Finally, OmniPlan can be extended through the creation of new plugins to integrate new planners and new knowledge sources.
- Key Features
- Installation
- Demos
- OmniPlan TUI Monitor
- API Development
- Plugin Architecture: Extensible design using ROS 2 pluginlib for omni customization.
- Multiple PDDL Planners: Support for POPF, SMTP, VHPOP, Colin, LPG, and OPTIC planners, plus the VAL plan validator.
- Flexible Execution: Execute plans using direct actions, YASMIN state machines or Behavior Trees.
- Knowledge Management: Choose between knowledge base or knowledge graph approaches or integrate your own implementation.
- Structural Plan Cache: Two-level cache (exact + structural) that reuses plans across structurally equivalent problems through role-based PDDL normalization.
- Multi-Robot Task Allocation (MRTA): Built-in allocator plugins (Round-Robin, SSI Affinity, Greedy Auction, CBBA, and Coalition Formation) to distribute goals across robot fleets.
- ROS 2 Native: Built on ROS 2 with proper message interfaces.
# Clone this repo
cd ~/ros2_ws/src
git clone https://github.com/mgonzs13/omni_plan
# Install dependecies
cd ~/ros2_ws
vcs import src < src/omni_plan/dependencies.repos
rosdep install --from-paths src --ignore-src -r -y
# SMTPlan+ dependency
sudo apt install libz3-dev -y
# Colin and OPTIC dependecy
sudo apt install coinor-libcbc3 -y
# Optional: You may need to create symbolic link
sudo ln -s /usr/lib/x86_64-linux-gnu/libCbc.so.3.10.11 /usr/lib/x86_64-linux-gnu/libCbc.so.3
# Build the workspace
colcon build --symlink-installTo run the tests:
colcon test --executor sequential --packages-select omni_plan omni_plan_knowledge_base omni_plan_knowledge_graph omni_plan_popf omni_plan_vhpop omni_plan_smtp omni_plan_optic omni_plan_lpg omni_plan_colin omni_plan_mrta omni_plan_cache omni_plan_homeostatic omni_plan_val omni_plan_dispatcher omni_plan_yasmin omni_plan_bt omni_plan_tests
colcon test-result --verboseeasy_plan_2_1.mp4
The framework includes several demo packages showcasing different planning and execution approaches:
ros2 launch omni_plan_demos popf_kg_demo.launch.pyros2 launch omni_plan_demos popf_kb_demo.launch.pysmtp_kb_demo.launch.py/smtp_kg_demo.launch.py: SMTP planner demosvhpop_kb_demo.launch.py/vhpop_kg_demo.launch.py: VHPOP planner demoslpg_kb_demo.launch.py/lpg_kg_demo.launch.py: LPG planner demosoptic_kb_demo.launch.py/optic_kg_demo.launch.py: OPTIC planner demos
ros2 run omni_plan_demos knowledge_graph_demoDemonstrates concurrent action execution using the knowledge graph. Three robots pick up parts in parallel, then a convergence step assembles them once all predecessors have completed.
ros2 launch omni_plan_demos popf_assembly_demo.launch.pyThe PDDL domain models pick-up (per-robot) and assemble (requires all
parts) as durative actions with OVER_ALL battery conditions so the planner
schedules them as a true parallel graph.
ros2 run omni_plan_demos assembly_demoThe omni_plan_tui package provides a terminal-based monitoring interface for
active plan execution. It subscribes to three topics and renders a live,
colour-coded view in the terminal using ncurses.
ros2 run omni_plan_tui omni_plan_tui_node| Tab | Key | Description |
|---|---|---|
| Plan Execution | 1 |
Level-grouped graph view of all actions with real-time status icons and elapsed times |
| FSM State | 2 |
Current YASMIN state machine state and full hierarchy |
| Action Catalog | 3 |
Complete list of loaded plugin actions with parameters, conditions and effects |
| Key | Action |
|---|---|
q / Q |
Quit |
1 / 2 / 3 |
Jump to Plan / FSM / Actions tab |
Tab / ] |
Next tab |
[ |
Previous tab |
↑ / ↓ |
Scroll one row |
PgUp / PgDn |
Scroll ten rows |
Home / End |
Jump to top / bottom |
| Mouse click (tab bar) | Switch to clicked tab |
| Topic | Message type | QoS |
|---|---|---|
/omni_plan/actions_info |
omni_plan_msgs/ActionInfoArray |
Transient-local (latched) |
/omni_plan/plan_execution |
omni_plan_msgs/PlanExecutionStatus |
Default |
/fsm_viewer |
yasmin_msgs/StateMachine |
Default |
OmniPlan uses a plugin-based architecture that allows developers to extend the framework by creating new planners, plan validators, and actions. All plugins are loaded using ROS2's pluginlib system.
PDDL managers handle domain and problem generation from action definitions and manage the current world state. They support different state representation approaches (e.g., knowledge base vs. knowledge graph). Inherit from omni_plan::PddlManager:
#include "omni_plan/pddl_manager.hpp"
class MyPddlManager : public omni_plan::PddlManager {
public:
MyPddlManager() : PddlManager() {}
protected:
// Generate PDDL domain and problem from current state
std::pair<omni_plan::pddl::Domain, omni_plan::pddl::Problem>
get_pddl() const override {
// Implement your state representation logic here
// Return a pair of Domain and Problem objects
}
// Check if there are any goals to achieve
bool has_goals() const override {
// Query your state representation for pending goals
}
// Clear all current goals
bool clear_goals() const override {
// Clear goals from your state representation
}
// Check if a predicate exists in the current state
bool predicate_exists(const omni_plan::pddl::Predicate &predicate) const override {
// Query your state representation for the predicate
}
// Check if a predicate is part of the goal conditions
bool predicate_is_goal(const omni_plan::pddl::Predicate &predicate) const override {
// Check if the predicate is in the goals
}
// Apply a single effect to the current state
void apply_effect(const omni_plan::pddl::Effect &exp) override {
// Update your state representation with the effect
// May add or delete predicates depending on the effect type
}
};To create a new planner, inherit from the omni_plan::Planner base class. The class defines 7 virtual methods — you can take one of two approaches depending on how your planner works.
| Method | Visibility | Purpose |
|---|---|---|
generate_plan(Domain, Problem) -> Plan |
public | Entry point: produce a Plan from in-memory PDDL objects |
parse_plan(Domain, str) -> Plan |
public | Convert raw planner output into a Plan |
generate_plan(str domain_path, str problem_path) -> string |
protected | Write PDDL to files, run external planner, return raw output |
has_solution(str) -> bool |
protected | Does the raw output indicate a valid plan? |
parse_action_line(string) -> pair<str, vector<str>> |
protected | Extract action name + parameters from one output line |
get_lines_with_actions(str) -> vector<string> |
protected | Filter output lines that represent actions |
parse_start_time(str) -> float |
protected | Extract the numeric start time from a line |
Override generate_plan(Domain, Problem) directly when you want to bypass the file-writing / parsing pipeline (e.g. for wrapper planners that delegate to a sub-planner, or planners that work entirely in memory). If the default parse_plan logic is not suitable, override parse_plan as well.
class MyPlanner : public omni_plan::Planner {
public:
// MUST override — entry point
pddl::Plan generate_plan(const pddl::Domain &domain,
const pddl::Problem &problem) const override;
// Optional — override only if the default parse logic doesn't fit
pddl::Plan parse_plan(const pddl::Domain &domain,
const std::string &str_plan) const override;
};Override generate_plan(string, string) to write domain/problem to files, invoke your planner, and return its raw output. Override has_solution to recognise a successful result. The parsing helpers have sensible defaults for common PDDL planner output formats; override them only when the format differs.
Important: add using Planner::generate_plan; to expose the public
generate_plan(Domain, Problem) overload (otherwise it is hidden by the
protected override).
class MyPlanner : public omni_plan::Planner {
public:
using Planner::generate_plan; // expose public overload
protected:
// MUST override — write PDDL to files, run planner, return raw output
std::string generate_plan(const std::string domain_path,
const std::string problem_path) const override;
// MUST override — check if the raw output contains a valid solution
bool has_solution(const std::string &plan_str) const override;
// Optional — only if the default parser doesn't understand the output
std::pair<std::string, std::vector<std::string>>
parse_action_line(std::string line) const override;
// Optional — only if action lines need different filtering
std::vector<std::string>
get_lines_with_actions(const std::string &plan_str) const override;
// Optional — only if the start-time format differs from "0.000:"
float parse_start_time(const std::string &line) const override;
};The default implementations assume a common PDDL output format where lines look like 0.000: (action_name param1 param2). For planner output that differs, override the relevant parse methods.
The omni_plan_cache package provides a CachePlanner wrapper that sits in front of any planner plugin and caches its results. On subsequent requests with the same or structurally equivalent problems, the cache returns the stored plan without invoking the wrapped planner. This is especially useful for expensive external planners in repetitive or multi-step tasks.
- Exact cache — keyed by full domain + problem PDDL text. When an identical problem is seen again the cached plan is returned directly.
- Structural cache — keyed by a role-based signature that ignores object names and preserves only object types and their structural roles in predicates. A domain is structurally equivalent to another if it can be obtained by renaming objects in a type-preserving way. The structural cache handles this by:
- Computing a role key for each predicate argument (predicate name + argument position + fact/goal role).
- Sorting
objects_by_typeby role order so placeholder indices reflect semantic role rather than arbitrary names. - Applying a two-phase name-rewriting scheme (
__TMP_oldname__intermediate markers) to avoid collisions during swap renames.
If neither cache level matches, the problem is delegated to the wrapped planner and the result is stored in both caches.
The omni_plan_mrta package adds multi-robot task allocation on top of the standard planning pipeline. An MrtaPlanner decomposes the PDDL problem into per-team sub-problems, solves them in parallel with the configured sub-planner, and merges the resulting plans by sorting all actions by start time.
| Symbol | Meaning |
|---|---|
| Number of robots | |
| Number of goals | |
| Robot index | |
| Goal index | |
| Set of initial-state ground predicates | |
| Goal predicate with argument list |
All proximity-aware allocators share a common spatial model built from
Co-occurrence adjacency. Two objects
The adjacency list is deduplicated so each edge appears once regardless of how many facts contain the same pair.
Robot-position arguments. Let
Sum-BFS distance. A single BFS from robot
Excluding occupied positions prevents a robot that happens to share one argument of a multi-argument goal from gaining a trivial 1-hop advantage over a robot genuinely closer to the goal's primary unoccupied locations.
The simplest allocator. It makes no use of the initial state or action schemas.
Goal
A Sequential Single-Item auction with 1-hop co-occurrence affinity bidding (Gerkey & Matarić, 2004).
Affinity score. For robot
Bid. The bid accounts for the affinity minus a load-balancing penalty:
At each auction round the
Limitation. Only direct (1-hop) co-occurrence is captured. Indirect spatial relationships (robot → location → object → goal) are invisible to this allocator. Prefer the Greedy Auction Allocator when indirect proximity matters.
SSI auction with full BFS-distance bidding in the co-occurrence graph.
Bid. Let
The load coefficient is chosen so that load balancing only resolves ties within a distance tier — a robot one hop closer always beats a more lightly loaded but more distant robot. At each auction round the
Consensus-Based Bundle Algorithm (Choi, Brunet & How, 2009) with delete-relaxed heuristic bidding and a post-convergence load-balancing pass.
For each robot
h_add — additive relaxation: the cost of an action is
h_max — max relaxation: the cost of an action is
The resulting cost map gives
Let
This ensures h-cost differences always dominate BFS differences in the bid value. For reachable goals the bid is:
where
where
The load-balancing penalty
Let
Bundle phase — each robot greedily extends its bundle:
Robot
Consensus phase — global winner for goal
Every robot that holds
The loop terminates when no bundle changes occur in a full round (convergence), or after
After CBBA converges, a load-rebalancing pass drives every robot's goal count to within 1 of the optimal
Each iteration identifies the receiver (robot with minimum load) and the donor (most-loaded robot with
The steal is only attempted when the receiver has a finite h-cost for the goal. The loop repeats until
ROS parameter: allocator.use_h_max (bool, default false).
A three-phase PDDL-aware allocator that identifies goals requiring multi-robot cooperation, forms the smallest feasible coalition for each, and then assigns remaining single-robot goals via a greedy BFS auction.
For each robot
Otherwise it is classified as multi-robot (MR): no single robot's action pool can reach
For each MR goal
The smallest
All robots in
ROS parameter: allocator.max_coalition_size (int, default 3).
Remaining goals are assigned to still-available solo robots using the same SSI BFS-distance auction as the Greedy Auction Allocator, augmented with a capability bonus to prefer robots that can actually achieve the goal over those that cannot:
where
| Plugin | Key formula | Complexity |
|---|---|---|
RoundRobinAllocator |
||
SsiAffinityAllocator |
||
GreedyAuctionAllocator |
||
CbbaAllocator |
|
|
CoalitionFormationAllocator |
h_add classification + subset search + BFS auction |
To implement a custom allocator, inherit from omni_plan_mrta::TaskAllocator:
#include "omni_plan_mrta/task_allocator.hpp"
class MyAllocator : public omni_plan_mrta::TaskAllocator {
public:
MyAllocator() : TaskAllocator() {}
std::vector<omni_plan_mrta::TeamAllocation>
allocate(const std::vector<std::string> &robots,
const std::vector<omni_plan::pddl::Predicate> &goals,
const omni_plan::pddl::Problem &problem,
const std::map<std::string, std::shared_ptr<omni_plan::pddl::Action>>
&actions) const override {
// Assign each goal to one or more robots.
// Return a vector of TeamAllocation, one entry per robot group.
// Each TeamAllocation contains:
// robots — list of robot names in the team (≥ 1)
// goal_indices — indices into `goals` assigned to this team
std::vector<omni_plan_mrta::TeamAllocation> result;
// ... your allocation logic ...
return result;
}
};Register the allocator in an allocator_plugins.xml file:
<class_libraries>
<library path="my_allocator">
<class name="my_pkg/MyAllocator"
type="my_pkg::MyAllocator"
base_class_type="omni_plan_mrta::TaskAllocator" />
</library>
</class_libraries>| Parameter | Default | Description |
|---|---|---|
allocator.use_h_max |
false |
Use the h_max deletion-relaxation heuristic instead of h_add. |
| Parameter | Default | Description |
|---|---|---|
allocator.max_coalition_size |
3 |
Maximum number of robots that may form a single coalition. |
Register your planner in a plugins.xml file and export it using PLUGINLIB_EXPORT_CLASS.
Plan validators verify that generated plans are correct. Inherit from omni_plan::PlanValidator:
#include "omni_plan/plan_validator.hpp"
class MyValidator : public omni_plan::PlanValidator {
public:
MyValidator() : PlanValidator() {}
// Validate plan against domain, problem and plan
bool validate_plan(const pddl::Domain &domain,
const pddl::Problem &problem,
const pddl::Plan &plan) const override {
// Implement validation logic using your preferred validator
}
protected:
// Validate plan against domain, problem and plan files
bool validate_plan(const std::string &domain_path,
const std::string &problem_path,
const std::string &plan_path) const override {
// Implement validation logic using your preferred validator
}
};A PlanDispatcher plugin controls how the actions of a plan are executed once the planning graph has been built. Two built-in strategies are provided (SequentialPlanDispatcher and ParallelPlanDispatcher). You can create your own by:
- Inheriting from
omni_plan::PlanDispatcherand implementingdispatch_actions(). - Registering it as a pluginlib plugin with base class
omni_plan::PlanDispatcher.
// my_dispatcher/include/my_dispatcher/my_plan_dispatcher.hpp
#include "omni_plan/plan_dispatcher.hpp"
namespace my_dispatcher {
class MyPlanDispatcher : public omni_plan::PlanDispatcher {
public:
MyPlanDispatcher() : omni_plan::PlanDispatcher() {}
protected:
omni_plan::pddl::ActionStatus dispatch_actions(
const std::vector<omni_plan::pddl::GraphNode::Ptr> &all_nodes) override {
for (const auto &node : all_nodes) {
if (this->is_canceled()) {
return omni_plan::pddl::ActionStatus::CANCELED;
}
this->set_node_status(node->node_num,
omni_plan_msgs::msg::PlanActionStatus::RUNNING);
this->publish_exec_status(
omni_plan_msgs::msg::PlanExecutionStatus::RUNNING);
auto action = node->action.action;
this->push_current_action(action);
omni_plan::pddl::ActionStatus result =
this->run_node_action(node, action);
this->remove_current_action(action);
if (result != omni_plan::pddl::ActionStatus::SUCCEEDED) {
if (this->cancel_on_abort_) {
this->cancel_plan();
}
return result;
}
}
this->clear_current_actions();
return omni_plan::pddl::ActionStatus::SUCCEEDED;
}
};
} // namespace my_dispatcher| Helper | Description |
|---|---|
is_canceled() |
Returns true if cancel_plan() has been called. |
cancel_plan() |
Cancels all running actions and sets the cancellation flag. |
run_node_action(node, action) |
Applies PDDL effects, runs the action, and rolls back on failure. Returns ActionStatus. |
push_current_action(action, use_cache) |
Registers an action as currently running (enables cancellation). Returns the action instance to use — if use_cache=true a cached copy is returned when the action is already in use (for parallel branches). |
remove_current_action(action) |
Un-registers a completed action. |
clear_current_actions() |
Clears all tracked actions (call at the end of dispatch). |
set_node_status(node_num, status) |
Updates the per-node execution status for publishing. |
publish_exec_status(overall) |
Publishes the current status snapshot on /omni_plan/plan_execution. |
acquire_cached_action(action) |
Returns an idle clone from the action pool (or creates one). |
release_cached_action(action) |
Returns a clone to the pool for future reuse. |
| Flag | Default | Description |
|---|---|---|
cancel_on_abort_ |
false |
When true, call cancel_plan() automatically whenever an action aborts. |
cancel_on_new_goals_ |
false |
When true, monitor the PDDL manager for new goals and cancel execution if any appear. |
Both flags are exposed as ROS parameters: plan_dispatcher.cancel_on_abort and plan_dispatcher.cancel_on_new_goals.
The built-in ParallelPlanDispatcher also exposes plan_dispatcher.execution_threads (integer, defaults to std::hardware_concurrency()) to control the size of its thread pool.
Actions define the executable behaviors in your planning domain. All actions inherit from omni_plan::pddl::Action and must implement the run and cancel methods. All action types must be registered in a plugins.xml file and exported using the appropriate PLUGINLIB_EXPORT_CLASS macro.
For simple actions implemented directly in C++:
#include "omni_plan/pddl/action.hpp"
class MyAction : public omni_plan::pddl::Action {
public:
MyAction()
: Action("my_action", {
{"param1", "type1"},
{"param2", "type2"}
}) {
// Add preconditions
this->add_condition(omni_plan::pddl::START, "predicate_name",
{"param1", "param2"});
// Add effects
this->add_effect(omni_plan::pddl::END, "predicate_name",
{"param1"}, true); // true for negated effect
}
omni_plan::pddl::ActionStatus run(const std::vector<std::string> ¶ms) override {
// Implement your action execution logic
// Return SUCCEEDED, CANCELED, or ABORTED
return omni_plan::pddl::ActionStatus::SUCCEEDED;
}
void cancel() override {
// Handle action cancellation
}
};For actions that use YASMIN state machines defined programmatically:
#include "omni_plan_yasmin/yasmin_action.hpp"
class MyYasminAction : public omni_plan_yasmin::YasminAction {
public:
MyYasminAction()
: YasminAction("my_action", {
{"param1", "type1"},
{"param2", "type2"}
}) {
// Define PDDL conditions and effects as in regular actions
// Build your YASMIN state machine
this->add_state("STATE1", std::make_shared<MyState1>());
this->add_state("STATE2", std::make_shared<MyState2>());
this->add_transition("STATE1", "STATE2", "outcome1");
// ... configure state machine
}
};For actions using YASMIN state machines defined in XML files:
#include "omni_plan_yasmin/yasmin_factory_action.hpp"
class MyYasminFactoryAction : public omni_plan_yasmin::YasminFactoryAction {
public:
MyYasminFactoryAction()
: YasminFactoryAction("my_action",
{{"param1", "type1"}, {"param2", "type2"}},
"/path/to/state_machine.xml") {
// Define PDDL conditions and effects
// The state machine is loaded from the XML file
}
protected:
// Optional: populate the blackboard before the state machine runs
yasmin::Blackboard::SharedPtr create_blackboard() override {
auto bb = YasminFactoryAction::create_blackboard();
// Add custom entries to the blackboard here
return bb;
}
};For actions implemented as Behavior Trees:
#include "omni_plan_bt/bt_action.hpp"
class MyBtAction : public omni_plan_bt::BtAction {
public:
MyBtAction()
: BtAction("my_action",
{{"param1", "type1"}, {"param2", "type2"}},
"/path/to/behavior_tree.xml") {
// Define PDDL conditions and effects
// The behavior tree is loaded from the XML file
}
protected:
// Optional: write action parameters into the BT blackboard before execution
void load_data_in_blackboard() override {
// Use this->set_input<T>("key", value) to populate blackboard entries
}
};