flowchart TD
A[ROS 2 Inputs
sensors, map, localization,
route, suggestions, zones, perception]
B[Domain Layer
subscribe and cache latest data
]
C[Conditions Layer
evaluate named predicates on the domain
]
D[Rules Layer
load yaml rules and check require and forbid then resolve priority
]
E[Behaviours Layer
run the selected behaviour using the domain and params
]
F[Decision Publisher
publish decision to ros 2 topics
]
G[External Systems
controllers, monitors, visualization]
%% Flow with explicit outputs on edges
A --> B
B -->|world state snapshot| C
C -->|condition state| D
D -->|behaviour name| E
E -->|decision object| F
F -->|ros 2 messages| G
- Domain (
domain.hpp) — owns all ROS 2 subscriptions and the “live” world state derived from them (ego state, route info, safety corridor, suggested trajectories, caution zones, timestamps, etc.). It also holds planning adapters/tools and a small amount of internal flags likesent_assistance_request. - Conditions (
conditions.hpp) — a registry (ConditionMap) of boolean predicatesfn(const Domain&, const ConditionParams&) -> bool.evaluate_conditions()runs every predicate and returns aConditionState(map ofname -> true/false). - Rules (
rules.hpp) — declarative mapping from condition names to behaviour names. Rules are loaded from YAML viaload_rules_yaml(path)and chosen bychoose_behaviour(state, rules). - Behaviours (
behaviours.hpp) — a registry (BehaviourMap) of behaviour functions that do the heavy lifting: produce a plan/trajectory (or alternative outputs) using the data inDomain. Typical entries includefollow_route,safety_corridor,request_assistance,minimum_risk, etc. - DecisionPublisher (
decision_publisher.hpp) — wraps the ROS 2 publishers:trajectory_publisher(final decision),trajectory_suggestion_publisher(soft suggestions),assistance_publisher,traffic_participant_publisher. It exposessetup(node, topics)andpublish(Decision).
- DecisionMaker (
decision_maker.hpp/.cpp) — the ROS 2 component node that wires everything:- Declares and reads parameters (
DecisionParams, includingInTopics/OutTopics, timing likerun_delta_time, andrules_file). domain.setup(node, params.domain_params, params.in_topics)to subscribe to inputs.- Builds the registries:
conditions::make_condition_map()andbehaviours::make_behaviour_map(). - Loads rules with
rules::load_rules_yaml(rules_file). - Starts a periodic timer (period =
run_delta_time) that executes the main looprun().
- Declares and reads parameters (
- Snapshot the current
Domain(updated via subscriptions). - Evaluate Conditions:
state = evaluate_conditions(domain, params.condition_params, condition_map). - Select Behaviour:
behaviour_name = choose_behaviour(state, rules)(highest priority matching rule). - Run Behaviour: Call the registered behaviour function, e.g.
behaviour_map.at(behaviour_name)(domain, params.planning_params). - Publish Decision:
publisher.publish(decision)(trajectory / suggestion / assistance / participant).
This design keeps logic data‑driven (rules YAML) and open‑ended (registries for conditions/behaviours).
All parameters are declared in decision_types.hpp (and friends). Key groups you can expect:
- Input topics (
InTopics): suggested trajectory, acceptance, caution zones, perception, map/lanelet sources, etc. - Output topics (
OutTopics): trajectory decision, trajectory suggestion, assistance request, traffic participant. - Timing:
run_delta_time(main loop period). - Files:
rules_filepointing to a YAML with rule definitions. - Per‑module params:
DomainParams,ConditionParams,PlanningParams…
The exact names are exposed as ROS 2 parameters (e.g.,
topic_trajectory_decision,topic_trajectory_suggestion,topic_assistance_request,topic_traffic_participant, and so on).
The decision rules define when each behaviour should run, based on evaluated conditions. They are loaded from a YAML file (e.g., rules.yaml) at startup, and can be swapped out or tuned without recompiling.
Each rule entry has:
-
name — descriptive label for readability/logging.
-
require — list of condition names that must be true for the rule to be eligible.
-
forbid — list of condition names that must be false.
-
behaviour — the behaviour function to execute when the rule matches.
-
priority — numeric value used to break ties (higher = wins if multiple match).
The decision loop evaluates conditions → applies rules top-down by priority → runs the winning behaviour.
You can add new inputs, new conditions, new behaviours, and new rules without modifying the decision loop.
- Extend the state in
Domain(e.g., addstd::optional<MyMsg>or a bespoke struct). - In
Domain::setup(node, domain_params, in_topics), add a subscription:add_subscription<my_msgs::msg::Foo>( node, in_topics.foo_topic, [this](my_msgs::msg::Foo::SharedPtr msg) { this->foo_state = *msg; }); - If the input needs conversion, add an adapter (see existing
adore_dynamics_adapters.hppusage).
Keep subscriptions QoS simple (depth=1) unless you need history;
Domainis designed to hold the latest view.
- Signature:
bool my_condition(const Domain&, const ConditionParams&). - Register it in
conditions::make_condition_map():namespace adore::conditions { bool my_condition(const Domain& d, const ConditionParams& p); inline ConditionMap make_condition_map() { return ConditionMap{ // existing ones… { "my_condition", &my_condition }, }; } } // namespace adore::conditions
- Now the condition can be referenced by name in the YAML
require/forbidlists.
Tips (for deterministic, testable conditions):
- Use only
Domain’s cached state; avoid direct ROS calls inside a condition. - Keep conditions pure (no side‑effects).
- Return
falseon missing/invalid data; expose separate conditions likehas_foofor availability.
-
Signature:
Decision my_new_behaviour(const Domain&, const PlanningParams&)(exact typedef as inbehaviours.hpp).
It should compute one or more of:- a final trajectory (for
trajectory_publisher), - a suggested trajectory (optional),
- an assistance request (optional),
- a traffic participant (optional).
- a final trajectory (for
-
Register it in
behaviours::make_behaviour_map():namespace adore::behaviours { Decision my_new_behaviour(const Domain&, const PlanningParams&); inline BehaviourMap make_behaviour_map() { return BehaviourMap{ // existing behaviours… { "my_new_behaviour", &my_new_behaviour }, }; } } // namespace adore::behaviours
-
If your behaviour needs a default participant message, reuse the helper:
auto participant = behaviours::make_default_participant(domain, planning_params); -
Behaviours should be stateless (preferred). If you need state, store it in
Domainor a dedicated cache keyed by timestamps fromDomainto keep the main loop re‑entrant.
Once the condition and behaviour are registered, reference them in rules.yaml and adjust priority. No code changes needed in the decision loop.
time_headway
- Type:
std_msgs::msg::Float64 - Description: Received time headway and updates the planner to use this variable time headway.
DecisionPublisher::setup(node, topics) initializes publishers using the out‑topic names from parameters. Call publish(decision) to emit whatever is populated in the Decision (trajectory, suggestions, assistance, participant).
Messages use adapter typedefs defined in adore_dynamics_adapters.hpp (e.g., TrajectoryAdapter, ParticipantAdapter) to decouple the planner from message schemas. See existing behaviours for examples of producing these adapters.
- Naming: Functions and data members use
snake_case; classes/structs usePascalCase. - Modern C++: Prefer
std::optional,std::variant,std::chrono, range‑based loops, and RAII. Avoid raw pointers. - Purity: Conditions are side‑effect free; behaviours publish only via the returned
Decision(not directly). - Determinism: Tie decisions to
Domaintimestamps to avoid flicker. Use priority to resolve ambiguity.
Eclipse Public License 2.0 (EPL‑2.0). See the headers in each file.