Repository navigation
fix: harden visualization and refresh project documentation #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jaagut
wants to merge
1
commit into
fix/runtime-lifecycle
Choose a base branch
from
fix/visualization-and-documentation
base: fix/runtime-lifecycle
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,185 +1,147 @@ | ||
| # dynamic_stack_decider | ||
| # Dynamic Stack Decider | ||
|
|
||
| A lightweight framework for decision making and behavior defining for robots and other agents in changing environments. | ||
| It combines the advantages of behavior trees, decision trees and state machines. | ||
| DSD selects persistent actions through a hierarchy of decisions that can be | ||
| reevaluated as the environment changes. The active stack records the decisions | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't even the point really. |
||
| behind the current action, making it useful for inspecting robot behavior. | ||
|
|
||
| ## General Structure | ||
| Python classes implement decisions and actions. A `.dsd` file defines their | ||
| control flow: | ||
|
|
||
| In general, the dynamic stack decider consists of actions and decisions. | ||
| A decision is a python class extending `dynamic_stack_decider.AbstractDecisionElement`. | ||
| It should implement the `perform()` method where, depending on some information, a string is returned. | ||
|
|
||
| An action is a python class extending `dynamic_stack_decider.AbtractActionElement`. | ||
| It should also implement the `perform()` method where code can be executed that performs an action. | ||
| It should not return anything. | ||
|
|
||
| The DSD follows a tree-like structure that is specified in the DSD domain-specific language. | ||
| Let's look at an example: | ||
|
|
||
| ``` | ||
| --> | ||
| $Mode | ||
| BALL --> $BallSeen | ||
| YES --> @TrackBall | ||
| NO --> @SearchBall | ||
| PATTERN --> @LookAround | ||
| ```text | ||
| -->HeadBehavior | ||
| $BallSeen | ||
| YES --> @TrackBall | ||
| NO --> @SearchBall | ||
| ``` | ||
|
|
||
| This example is a very simple head behavior for a robot. | ||
| The arrow (`-->`) at the beginning of the file indicates the start of the behavior. | ||
| Decisions are prefix with `$`, actions with `@`. | ||
| Their names correspond to the class names of the python classes implementing the respective action or decision. | ||
|
|
||
| The first decision is the `Mode` decision. | ||
| It decides about the mode of the head and returns the string `BALL` or `PATTERN`. | ||
| By convention, the returned strings are in capital letters. | ||
| The returned strings are indented and followed by an arrow; the element after the arrow is executed when the corresponding string is returned by the decision. | ||
| `BallSeen.perform()` returns an outcome such as `YES` or `NO`. | ||
| `TrackBall.perform()` and `SearchBall.perform()` perform one tick of work and | ||
| return nothing. Actions retain their instance state between updates and complete | ||
| with `return self.pop()`. | ||
|
|
||
| <p align="center"> | ||
| <img src="dynamic_stack_decider/docs/_static/DSD_Stack_Example.png" alt="A visualization of the Stack" width="350px"/> | ||
| </p> | ||
| ## Try the example | ||
|
|
||
| The underlying model for the DSD is a stack. | ||
| The root element (`Mode` in the example above) is always at the bottom of the stack. | ||
| When it makes a decision, the result is pushed on the stack and executed, for example the `BallSeen` decision. | ||
| Then, depending on the result, the next decision or action is pushed and executed, for example the `TrackBall` action. | ||
| This actions stays on top of the stack until one of the following conditions is met: | ||
| 1. It calls `self.pop()` to pop itself from the stack. | ||
| Then, the decision that is now on top of the stack is executed again and depending on the result, another action or decision is pushed. | ||
| 2. One of the decisions on the stack is reevaluated (see #Reevaluation below). | ||
| Install [Pixi](https://pixi.sh), then run the checked-in example: | ||
|
|
||
| The `__init__` method of an element is called when it is pushed on the stack. | ||
| It receives the following arguments: | ||
| * `blackboard`: an arbitrary object that is shared between all stack elements | ||
| * `dsd`: the instance of the dynamic stack decider | ||
| * `parameters`: an optional dictionary of parameters | ||
| ```sh | ||
| pixi run --locked demo | ||
| ``` | ||
|
|
||
| The `perform` method is called when it is on top of the stack or getting reevaluated. | ||
| It receives the argument `reevaluate` which is set to `True` when the element is getting reevaluated and to `False` when it is executed normally. | ||
| The example uses a shared blackboard and switches from searching to tracking. | ||
| It requires neither a ROS node nor hardware. The core currently imports ROS | ||
| Python libraries; Pixi supplies those dependencies. | ||
|
|
||
| ## Starting the DSD | ||
| ## Define elements | ||
|
|
||
| To start the DSD, a `dynamic_stack_decider.DSD` object has to be created. | ||
| Its arguments are a blackboard, which is passed to all elements, and an optional ROS topic that is used for debugging. | ||
| Then, `register_actions` and `register_decisions` have to be called with the name of a folder containing the actions and decisions, respectively. | ||
| After registering the elements, `load_behavior` should be used to load the file containing the behavior description in the DSD language. | ||
| Finally, to actually execute the stack decider, `update` should be called repeatedly. | ||
| The blackboard can be any application object. Use the generic base classes to | ||
| keep its interface visible to the type checker: | ||
|
|
||
| On each update, all elements on the stack except the top elements are reevaluated from bottom to top, if requested. | ||
| Then, the top element is executed. | ||
| If it was a decision, the resulting action is pushed on the stack, to be executed in the next call to `update`. | ||
| ```python | ||
| from dataclasses import dataclass | ||
|
|
||
| ## Reevaluation | ||
| from dynamic_stack_decider import AbstractActionElement, AbstractDecisionElement | ||
|
|
||
| Normally, only the topmost action or decision of the stack is executed and another decision can only be executed if the topmost element pops itself from the stack. | ||
| Sometimes, it is useful to also execute particular decisions when they are not on top of the stack. | ||
| For example in the example above, if the mode can be set externally, it would make sense to check on every update of the stack if the decision has changed. | ||
| To achieve that behavior, the `get_reevaluate` should be implemented, either to just return `True` or to implement more sophisticated reevaluation criteria. | ||
|
|
||
| When the element is reevaluated, `perform` is called on the element. | ||
| If the result of `perform` is the same as the last time it was called, nothing happens and the next decision is reevaluated. | ||
| If the result of `perform` is different from the last time, everything above the reevaluated decision is discarded and a new element, depending on the result of `perform`, is pushed on the stack and executed. | ||
| @dataclass | ||
| class Blackboard: | ||
| ball_seen: bool = False | ||
|
|
||
| An action can call `self.do_not_reevaluate()` to avoid reevaluation of the stack on the next call to `update`. Alternatively the parameters (see #Parameters) `r` or `reevaluate` can be used in the dsd file to enable or disable the reevaluation during a specific action. | ||
|
|
||
| ## Sequence elements | ||
| class BallSeen(AbstractDecisionElement[Blackboard]): | ||
| def perform(self, reevaluate: bool = False) -> str: | ||
| return "YES" if self.blackboard.ball_seen else "NO" | ||
|
|
||
| Sequence elements can be used to perform multiple actions instead of a single one. | ||
| To use a sequence element, simply separate the actions with commas, like that: `@FirstAction, @SecondAction, @ThirdAction`. | ||
| When the sequence element is pushed on the stack, the first action of the sequence remains on top of the stack until it pops itself from the stack. | ||
| Then, the next action in the sequence is pushed to the stack. | ||
| def get_reevaluate(self) -> bool: | ||
| return True | ||
|
|
||
| ## Parameters | ||
|
|
||
| Sometimes it is helpful to pass parameters to actions or decisions. | ||
| The syntax is `@Action + parameter:value`, multiple parameters can be passed using further `+ parameter:value` pairs. | ||
| The parameters to an element are passed to the element's constructor as a dictionary. | ||
| The value of the parameter is passed through `yaml.safe_load`. | ||
| Therefore, you can use integers, floats, booleans or strings as values. | ||
| It is also possible to load parameters from the ROS parameter server using `parameter:%rosparam`, where `rosparam` is the name of the parameter on the parameter server. | ||
| class SearchBall(AbstractActionElement[Blackboard]): | ||
| def perform(self, reevaluate: bool = False) -> None: | ||
| # Start or monitor the application's search operation here. | ||
| pass | ||
|
|
||
| Here is an example: | ||
| ``` | ||
| --> | ||
| $Mode | ||
| BALL --> $BallSeen | ||
| YES --> @TrackBall + time:10 | ||
| NO --> @SearchBall | ||
| PATTERN --> @LookAround | ||
| def on_pop(self) -> None: | ||
| # Cancel outstanding work and release resources here. | ||
| pass | ||
| ``` | ||
|
|
||
| ## Subtrees | ||
| The constructor receives the shared `blackboard`, the owning `dsd`, and a | ||
| `parameters` dictionary. A subclass overriding it must call `super().__init__`. | ||
| Parameter values are YAML- or ROS-provided objects; narrow them with `isinstance` | ||
| before treating them as a specific type. Each activation receives its own copy. | ||
|
|
||
| To avoid duplication and deep nested structures in the DSD file, subtrees can be used. | ||
| Here is an example that is equivalent to the example above: | ||
| ``` | ||
| #BallMode | ||
| $BallSeen | ||
| YES --> @TrackBall | ||
| NO --> @SearchBall | ||
| ## Run a behavior | ||
|
|
||
| --> | ||
| $Role | ||
| BALL --> #BallMode | ||
| PATTERN --> @LookAround | ||
| ``` | ||
| Create a `DSD(blackboard)` instance, register actions and decisions, then load a | ||
| behavior. Registries map class names to their implementations; alternatively, | ||
| `register_actions(path)` and `register_decisions(path)` discover classes in a | ||
| Python file or package. The [runnable example](examples/demo.py) shows explicit | ||
| registries. | ||
|
|
||
| Subtrees can also receive parameters. | ||
| Here is another example: | ||
| ``` | ||
| Call `update()` regularly from one owner. It reevaluates eligible decisions from | ||
| the bottom of the active stack, then follows decision outcomes immediately until | ||
| an action runs. Actions in a comma-separated chain advance on completion; the | ||
| successor runs on the next update. | ||
|
|
||
| #BallMode | ||
| $BallSeen + tracktime | ||
| YES --> @TrackBall + time:*tracktime | ||
| NO --> @SearchBall | ||
| Use `on_pop()` for cleanup on completion, preemption, interruption, and behavior | ||
| replacement. Return immediately after calling `pop()` or `interrupt()`. | ||
| `dsd.last_error` exposes the first failure of the latest update; ordinary update | ||
| failures are logged. Applications must choose their recovery behavior. | ||
|
|
||
| --> | ||
| $Role | ||
| BALL --> #BallMode + tracktime:10 | ||
| PATTERN --> @LookAround | ||
| ``` | ||
| See the [execution contract](docs/runtime.md) for timing, cleanup, suppression, | ||
| root behavior, and error semantics. The [language reference](docs/language.md) | ||
| covers action chains, `ELSE`, comments, parameters, and reusable subtrees. Parser | ||
| errors include the source file and line. | ||
|
|
||
| As you can see, the parameters are specified in the same way as for actions or decisions. | ||
| In the subtree, the parameter has to be given a name using the `+` syntax and can be referenced with the `*` operator. | ||
| ## Inspect a running DSD | ||
|
|
||
| ## ELSE | ||
| Pass both a ROS node and a debug topic namespace: | ||
|
|
||
| `ELSE` is a catch-all decision result. | ||
| It can be used to map multiple results to a single action or decision. | ||
| For example, the `Role` decision could return the roles `BALL`, `GOAL`, `ROBOT`, and `PATTERN`, but the latter three should result in the same behavior: | ||
| ```python | ||
| dsd = DSD(blackboard, node=node, debug_topic="behavior/debug") | ||
| ``` | ||
| --> | ||
| $Mode | ||
| BALL --> $BallSeen | ||
| YES --> @TrackBall | ||
| NO --> @SearchBall | ||
| ELSE --> @LookAround | ||
|
|
||
| The RQT visualization discovers DSD instances through their debug topics and | ||
| shows the behavior tree, active path, and debug data. Start the installed | ||
| visualization package with: | ||
|
|
||
| ```sh | ||
| ros2 run dynamic_stack_decider_visualization dsd_gui | ||
| ``` | ||
|
|
||
| ## Interrupt | ||
| Debug topics use JSON in `std_msgs/msg/String`. The tree and current-action topics | ||
| use transient-local durability. Element IDs identify nodes within a loaded tree; | ||
| they are not persistent IDs across process restarts. | ||
|
|
||
| A stack element can call `self.interrupt()` to create an interrupt which removes all elements except for the start element from the stack (i.e., the DSD is restarted). | ||
| Elements can attach debug values with `publish_debug_data(label, value)` and clear | ||
| them with `clear_debug_data()`. Unsupported values are represented as strings. | ||
| Debug failures cannot prevent behavior execution. | ||
|
|
||
| ## Debugging | ||
| ## Install and develop | ||
|
|
||
| For debugging, the DSD visualization should be used. | ||
| It automatically discovers all running DSDs with a debug topic and visualizes them in RQT. | ||
| The visualization shows the DSD tree with the currently active actions and decisions as a tree on the left and the current stack on the right. | ||
| More information can be added to the right side by calling `self.publish_debug_data(label, data)` in any action or decision. | ||
| Both packages retain their ROS 2 ament Python layout and support source and wheel | ||
| builds. In a ROS workspace, use the workspace's `rosdep` and `colcon` workflow. | ||
| ROS dependencies are declared in `package.xml`; Python dependencies are declared | ||
| in `setup.py`. Installing a wheel alone does not install ROS. | ||
|
|
||
| ## DSD Development GUI | ||
| If you want to use a graphical user interface to define your DSD, we recommend [this repository](https://github.com/bit-bots/dsd-gui) | ||
| This repository's locked Pixi environments provide core testing, optional Qt/RQT | ||
| checks, and lightweight formatting tools. See [CONTRIBUTING.md](CONTRIBUTING.md) | ||
| for commands and dependency updates. The default test environment avoids the GUI | ||
| dependencies; the visualization CI job runs offscreen Qt and Graphviz tests. | ||
|
|
||
| ## VSCode Extension | ||
| If you use VSCode, you can use the [DSD extension](https://marketplace.visualstudio.com/items?itemName=Mastermori.dsd) which provides syntax highlighting. | ||
| ## Background and open decisions | ||
|
|
||
| ## Examples | ||
| The architecture is described in | ||
| [DSD — Dynamic Stack Decider](https://link.springer.com/article/10.1007/s12369-021-00768-8). | ||
| The [Bit-Bots workspace](https://github.com/bit-bots/bitbots_main) contains body | ||
| behavior, humanoid control, and localization examples. | ||
|
|
||
| Here are a few projects that use the DSD and can be used for reference: | ||
| * The [Bit-Bots Behavior](https://github.com/bit-bots/bitbots_main/tree/main/bitbots_behavior) | ||
| * The [Bit-Bots Humanoid Control Module](https://github.com/bit-bots/bitbots_main/tree/main/bitbots_motion/bitbots_hcm/bitbots_hcm/hcm_dsd) | ||
| * The [Parser Test](https://github.com/bit-bots/dynamic_stack_decider/tree/master/dynamic_stack_decider/test) in this repository | ||
| Architectural choices are tracked separately: | ||
|
|
||
| ## The Paper | ||
| - [Failure recovery and action outcomes](https://github.com/bit-bots/dynamic_stack_decider/issues/115) | ||
| - [ROS integration and registration boundaries](https://github.com/bit-bots/dynamic_stack_decider/issues/116) | ||
| - [Parameter grammar and outcome validation](https://github.com/bit-bots/dynamic_stack_decider/issues/117) | ||
| - [Root-chain completion and suppression scope](https://github.com/bit-bots/dynamic_stack_decider/issues/121) | ||
|
|
||
| The framework is also described in more depth in [DSD – Dynamic Stack Decider](https://link.springer.com/article/10.1007/s12369-021-00768-8) | ||
| The maintenance changes preserve existing chain timing and suppression behavior | ||
| while tightening invalid-input handling and lifecycle guarantees. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| Copyright (c) Hamburg Bit-Bots | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| this software and associated documentation files (the "Software"), to deal in | ||
| the Software without restriction, including without limitation the rights to | ||
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies | ||
| of the Software, and to permit persons to whom the Software is furnished to do | ||
| so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| include README.md | ||
| include README.md LICENSE | ||
| recursive-include test *.py *.dsd |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the new description