py_trees.blackboard module
Blackboards, for behaviours to write and read from.
Blackboards are not a necessary component of behaviour tree implementations, but are nonetheless, a fairly common mechanism for sharing data between behaviours in the tree. See, for example, the `design notes`_ for blackboards in Unreal Engine.
Implementations vary widely depending on the needs of the framework using them. The simplest implementations take the form of a key-value store with global access, while more rigorous implementations scope access or form a secondary graph overlaying the tree connecting data ports between behaviours.
The ‘Zen of PyTrees’ is to enable rapid development, yet be rich enough so that all of the magic is exposed for debugging purposes. The first implementation of a blackboard was merely a global key-value store with an api that lent itself to ease of use, but did not expose the data sharing between behaviours which meant any tooling used to introspect or visualise the tree, only told half the story.
The current implementation adopts a strategy similar to that of a filesystem. Each client (subsequently behaviour) registers itself for read/write access to keys on the blackboard. This is less to do with permissions and more to do with tracking users of keys on the blackboard - extremely helpful with debugging.
The alternative approach of layering a secondary data graph with parameter and input-output ports on each behaviour was discarded as being too heavy for the zen requirements of py_trees. This is in part due to the wiring costs, but also due to complexity arising from a tree’s partial graph execution (a feature which makes trees different from most computational graph frameworks) and not to regress on py_trees’ capability to dynamically insert and prune subtrees on the fly.
A high-level list of existing / planned features:
[+] Centralised key-value store
[+] Client connections with namespaced read/write access to the store
[+] Integration with behaviours for key-behaviour associations (debugging)
[+] Activity stream that logs read/write operations by clients
[+] Exclusive locks for writing
[+] Framework for key remappings
- class py_trees.blackboard.ActivityItem(key: str, client_name: str, client_id: UUID, activity_type: str, previous_value: Any | None = None, current_value: Any | None = None)
Bases:
object
Holds data pertaining to activity on the blackboard.
- Args:
key: name of the variable on the blackboard client_name: convenient name of the client performing the operation client_id: unique id of the client performing the operation activity_type: type of activity previous_value: of the given key (None if this field is not relevant) current_value: current value for the given key (None if this field is not relevant)
- class py_trees.blackboard.ActivityStream(maximum_size: int = 500)
Bases:
object
Stores the stream of events recording blackboard activity.
What got registered, unregistered, written, accessed? What operations failed due to incorrect permissions? What did the written variable change from? What did it change to? The activity stream captures all of these and more. It is a very useful mechanisms for debugging your tree from tick to tick.
- Attributes:
data (typing.List[ActivityItem]: list of activity items, earliest first maximum_size (int): pop items if this size is exceeded
- clear() None
Delete all activities from the stream.
- push(activity_item: ActivityItem) None
Push the next activity item to the stream.
- Args:
activity_item: new item to append to the stream
- class py_trees.blackboard.ActivityType(value)
Bases:
Enum
An enumerator representing the operation on a blackboard variable.
- ACCESSED = 'ACCESSED'
Key accessed, either for reading, or modification of the value’s internal attributes (e.g. foo.bar).
- ACCESS_DENIED = 'ACCESS_DENIED'
Client did not have access to read/write a key.
- INITIALISED = 'INITIALISED'
Initialised a key-value pair on the blackboard
- NO_KEY = 'NO_KEY'
Tried to access a key that does not yet exist on the blackboard.
- NO_OVERWRITE = 'NO_OVERWRITE'
Tried to write but variable already exists and a no-overwrite request was respected.
- READ = 'READ'
Read from the blackboard
- UNSET = 'UNSET'
Key was removed from the blackboard
- WRITE = 'WRITE'
Wrote to the blackboard.
- class py_trees.blackboard.Blackboard
Bases:
object
Centralised key-value store for sharing data between behaviours.
This class is a coat-hanger for the centralised data store, metadata for it’s administration and static methods for interacting with it.
This api is intended for authors of debugging and introspection tools on the blackboard. Users should make use of the
Client
.- Attributes:
Blackboard.clients (typing.Dict[uuid.UUID, str]): client uuid-name registry Blackboard.storage (typing.Dict[str, typing.Any]): key-value data store Blackboard.metadata (typing.Dict[str, KeyMetaData]): key associated metadata Blackboard.activity_stream (ActivityStream): logged activity Blackboard.separator (char): namespace separator character
- static absolute_name(namespace: str, key: str) str
Generate the fully qualified key name from namespace and name arguments.
Examples
'/' + 'foo' = '/foo' '/' + '/foo' = '/foo' '/foo' + 'bar' = '/foo/bar' '/foo/' + 'bar' = '/foo/bar' '/foo' + '/foo/bar' = '/foo/bar' '/foo' + '/bar' = '/bar' '/foo' + 'foo/bar' = '/foo/foo/bar'
- Args:
namespace: namespace the key should be embedded in key: key name (relative or absolute)
- Returns:
the absolute name
Warning
To expedite the method call (it’s used with high frequency in blackboard key lookups), no checks are made to ensure the namespace argument leads with a “/”. Nor does it check that a name in absolute form is actually embedded in the specified namespace, it just returns the given (absolute) name directly.
- activity_stream: ActivityStream | None = None
- static clear() None
Completely clear all key, value and client information from the blackboard.
This also deletes the activity stream, if it exists.
- clients: Dict[UUID, str] = {}
- static disable_activity_stream() None
Disable logging into the activity stream.
- static enable_activity_stream(maximum_size: int = 500) None
Enable logging into the activity stream.
- Args:
maximum_size: pop items from the stream if this size is exceeded
- Raises:
RuntimeError if the activity stream is already enabled
- static exists(name: str) bool
Check if the specified variable exists on the blackboard.
- Args:
name: name of the variable, can be nested, e.g. battery.percentage
- Raises:
AttributeError: if the client does not have read access to the variable
- static get(variable_name: str) Any
Get a variable from the blackboard.
Extract the value associated with the given a variable name, can be nested, e.g. battery.percentage. This differs from the client get method in that it doesn’t pass through the client access checks. Use for debugging / introspection tooling (e.g. display methods) only (prefer the clients for rigorous programmatic access).
- Args:
variable_name: of the variable to get, can be nested, e.g. battery.percentage
- Raises:
KeyError: if the variable or it’s nested attributes do not yet exist on the blackboard
- Return:
The stored value for the given variable
- static key(variable_name: str) str
Extract the key portion of an abitrary blackboard variable name.
Given a variable name that potentially also includes a reference to internal attributes of the variable stored on the blackboard, return the part that represents the blackboard key only.
Example: ‘/foo/bar.woohoo -> /foo/bar’.
- Args:
variable_name: blackboard variable name - can be nested, e.g. battery.percentage
- Returns:
name of the underlying key
- static key_with_attributes(variable_name: str) Tuple[str, str]
Separate key and attribrutes from a variable name.
Given a variable name that potentially also includes a reference to internal attributes of the variable stored on the blackboard, separate and return in tuple form.
Example: ‘/foo/bar.woohoo -> (/foo/bar’, ‘woohoo’)
- Args:
variable_name: blackboard variable name - can be nested, e.g. battery.percentage
- Returns:
a tuple consisting of the key and it’s attributes (in string form)
- static keys() Set[str]
Get the set of blackboard keys.
- Returns:
the complete set of keys registered by clients
- static keys_filtered_by_clients(client_ids: Set[UUID] | List[UUID]) Set[str]
Get the set of blackboard keys filtered by client unique identifiers.
- Args:
client_ids: set of client uuid’s.
- Returns:
subset of keys that have been registered by the specified clients
- static keys_filtered_by_regex(regex: str) Set[str]
Get the set of blackboard keys filtered by regex.
- Args:
regex: a python regex string
- Returns:
subset of keys that have been registered and match the pattern
- metadata: Dict[str, KeyMetaData] = {}
- static relative_name(namespace: str, key: str) str
Generate the abbreviated name for a key relative to the specified namespace.
Examples
'/' + 'foo' = 'foo' '/' + '/foo' = 'foo' '/foo' + 'bar' = 'bar' '/foo/' + 'bar' = 'bar' '/foo' + '/foo/bar' = 'bar' '/foo/' + '/foo/bar' = 'bar' '/foo' + 'foo/bar' = 'foo/bar' '/foo' + '/food/bar' => KeyError('/food/bar' is prefixed with a namespace conflicting with '/foo/')
- Args:
namespace: namespace the key should be embedded in key: key name (relative or absolute)
- Returns:
the absolute name
- Raises:
KeyError if the key is prefixed with a conflicting namespace
Warning
To expedite the method call (it’s used with high frequency in blackboard key lookups), no checks are made to ensure the namespace argument leads with a “/”. Be sure to lead with a “/”!
- separator: str = '/'
- static set(variable_name: str, value: Any) None
Set a variable on the blackboard.
Set the value associated with the given variable name. The name can be nested, e.g. battery.percentage. This differs from the client get method in that it doesn’t pass through the client access checks. Use for debugging / introspection tooling (e.g. display methods) only (prefer the clients for rigorous programmatic access).
- Args:
variable_name: of the variable to set, can be nested, e.g. battery.percentage
- Raises:
AttributeError: if it is attempting to set a nested attribute tha does not exist.
- storage: Dict[str, Any] = {}
- static unset(key: str) bool
Unset a variable on the blackboard.
- Args:
key: name of the variable to remove
- Returns:
True if the variable was removed, False if it was already absent
- class py_trees.blackboard.Client(*, name: str | None = None, namespace: str | None = None)
Bases:
object
Client to the key-value store for sharing data between behaviours.
Examples
Blackboard clients will accept a user-friendly name or create one for you if none is provided. Regardless of what name is chosen, clients are always uniquely identified via a uuid generated on construction.
provided = py_trees.blackboard.Client(name="Provided") print(provided) generated = py_trees.blackboard.Client() print(generated)
Register read/write access for keys on the blackboard. Note, registration is not initialisation.
blackboard = py_trees.blackboard.Client(name="Client") blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) blackboard.register_key(key="bar", access=py_trees.common.Access.READ) blackboard.foo = "foo" print(blackboard)
Keys and clients can make use of namespaces, designed by the ‘/’ char. Most methods permit a flexible expression of either relative or absolute names.
blackboard = py_trees.blackboard.Client(name="Global") parameters = py_trees.blackboard.Client(name="Parameters", namespace="parameters") blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) blackboard.register_key(key="/bar", access=py_trees.common.Access.WRITE) blackboard.register_key(key="/parameters/default_speed", access=py_trees.common.Access.WRITE) parameters.register_key(key="aggressive_speed", access=py_trees.common.Access.WRITE) blackboard.foo = "foo" blackboard.bar = "bar" blackboard.parameters.default_speed = 20.0 parameters.aggressive_speed = 60.0 miss_daisy = blackboard.parameters.default_speed van_diesel = parameters.aggressive_speed print(blackboard) print(parameters)
Disconnected instances will discover the centralised key-value store.
def check_foo(): blackboard = py_trees.blackboard.Client(name="Reader") blackboard.register_key(key="foo", access=py_trees.common.Access.READ) print("Foo: {}".format(blackboard.foo)) blackboard = py_trees.blackboard.Client(name="Writer") blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) blackboard.foo = "bar" check_foo()
To respect an already initialised key on the blackboard:
blackboard = Client(name="Writer") blackboard.register_key(key="foo", access=py_trees.common.Access.READ) result = blackboard.set("foo", "bar", overwrite=False)
Store complex objects on the blackboard:
class Nested(object): def __init__(self): self.foo = None self.bar = None def __str__(self): return str(self.__dict__) writer = py_trees.blackboard.Client(name="Writer") writer.register_key(key="nested", access=py_trees.common.Access.WRITE) reader = py_trees.blackboard.Client(name="Reader") reader.register_key(key="nested", access=py_trees.common.Access.READ) writer.nested = Nested() writer.nested.foo = "I am foo" writer.nested.bar = "I am bar" foo = reader.nested.foo print(writer) print(reader)
Log and display the activity stream:
py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100) reader = py_trees.blackboard.Client(name="Reader") reader.register_key(key="foo", access=py_trees.common.Access.READ) writer = py_trees.blackboard.Client(name="Writer") writer.register_key(key="foo", access=py_trees.common.Access.WRITE) writer.foo = "bar" writer.foo = "foobar" unused_result = reader.foo print(py_trees.display.unicode_blackboard_activity_stream()) py_trees.blackboard.Blackboard.activity_stream.clear()
Display the blackboard on the console, or part thereof:
writer = py_trees.blackboard.Client(name="Writer") for key in {"foo", "bar", "dude", "dudette"}: writer.register_key(key=key, access=py_trees.common.Access.WRITE) reader = py_trees.blackboard.Client(name="Reader") for key in {"foo", "bar"}: reader.register_key(key="key", access=py_trees.common.Access.READ) writer.foo = "foo" writer.bar = "bar" writer.dude = "bob" # all key-value pairs print(py_trees.display.unicode_blackboard()) # various filtered views print(py_trees.display.unicode_blackboard(key_filter={"foo"})) print(py_trees.display.unicode_blackboard(regex_filter="dud*")) print(py_trees.display.unicode_blackboard(client_filter={reader.unique_identifier})) # list the clients associated with each key print(py_trees.display.unicode_blackboard(display_only_key_metadata=True))
Behaviours are not automagically connected to the blackboard but you may manually attach one or more clients so that associations between behaviours and variables can be tracked - this is very useful for introspection and debugging.
Creating a custom behaviour with blackboard variables:
class Foo(py_trees.behaviour.Behaviour): def __init__(self, name): super().__init__(name=name) self.blackboard = self.attach_blackboard_client(name="Foo Global") self.parameters = self.attach_blackboard_client(name="Foo Params", namespace="foo_parameters_") self.state = self.attach_blackboard_client(name="Foo State", namespace="foo_state_") # create a key 'foo_parameters_init' on the blackboard self.parameters.register_key("init", access=py_trees.common.Access.READ) # create a key 'foo_state_number_of_noodles' on the blackboard self.state.register_key("number_of_noodles", access=py_trees.common.Access.WRITE) def initialise(self): self.state.number_of_noodles = self.parameters.init def update(self): self.state.number_of_noodles += 1 self.feedback_message = self.state.number_of_noodles if self.state.number_of_noodles > 5: return py_trees.common.Status.SUCCESS else: return py_trees.common.Status.RUNNING # could equivalently do directly via the Blackboard static methods if # not interested in tracking / visualising the application configuration configuration = py_trees.blackboard.Client(name="App Config") configuration.register_key("foo_parameters_init", access=py_trees.common.Access.WRITE) configuration.foo_parameters_init = 3 foo = Foo(name="The Foo") for i in range(1, 8): foo.tick_once() print("Number of Noodles: {}".format(foo.feedback_message))
Rendering a dot graph for a behaviour tree, complete with blackboard variables:
# in code py_trees.display.render_dot_tree(py_trees.demos.blackboard.create_root()) # command line tools py-trees-render --with-blackboard-variables py_trees.demos.blackboard.create_root
And to demonstrate that it doesn’t become a tangled nightmare at scale, an example of a more complex tree:
Debug deeper with judicious application of the tree, blackboard and activity stream display methods around the tree tick (refer to
py_trees.visitors.DisplaySnapshotVisitor
for examplar code):See also
py-trees-demo-blackboard
py-trees-demo-namespaces
py-trees-demo-remappings
- Attributes:
name (str): client’s convenient, but not necessarily unique identifier namespace (str): apply this as a prefix to any key/variable name operations unique_identifier (uuid.UUID): client’s unique identifier read (typing.Set[str]): set of absolute key names with read access write (typing.Set[str]): set of absolute key names with write access exclusive (typing.Set[str]): set of absolute key names with exclusive write access required (typing.Set[str]): set of absolute key names required to have data present remappings (typing.Dict[str, str]: client key names with blackboard remappings namespaces (typing.Set[str]: a cached list of namespaces this client accesses
- absolute_name(key: str) str
Generate the fully qualified key name for this key.
blackboard = Client(name="FooBar", namespace="foo") blackboard.register_key(key="bar", access=py_trees.common.Access.READ) print("{}".format(blackboard.absolute_name("bar"))) # "/foo/bar"
- Args:
key: name of the key
- Returns:
the absolute name
- Raises:
KeyError: if the key is not registered with this client
- exists(name: str) bool
Check if the specified variable exists on the blackboard.
- Args:
name: name of the variable to get, can be nested, e.g. battery.percentage
- Raises:
AttributeError: if the client does not have read access to the variable
- get(name: str) Any
Access via method a key on the blackboard.
This is the more cumbersome method (as opposed to simply using ‘.<name>’), but useful when the name is programatically generated.
- Args:
name: name of the variable to get, can be nested, e.g. battery.percentage
- Raises:
AttributeError: if the client does not have read access to the variable KeyError: if the variable or it’s nested attributes do not yet exist on the blackboard
- id() UUID
Access the unique identifier for this client.
- Returns:
The uuid.UUID object
- is_registered(key: str, access: None | Access = None) bool
Check to see if the specified key is registered.
- Args:
key: in either relative or absolute form access: access property, if None, just checks for registration, regardless of property
- Returns:
if registered, True otherwise False
- register_key(key: str, access: Access, required: bool = False, remap_to: str | None = None) None
Register a key on the blackboard to associate with this client.
- Args:
key: key to register access: access level (read, write, exclusive write) required: if true, check key exists when calling
remap_to: remap the key to this location on the blackboard
Note the remap simply changes the storage location. From the perspective of the client, access via the specified ‘key’ remains the same.
- Raises:
- AttributeError if exclusive write access is requested, but
write access has already been given to another client
TypeError if the access argument is of incorrect type
- set(name: str, value: Any, overwrite: bool = True) bool
Set, conditionally depending on whether the variable already exists or otherwise.
This is most useful when initialising variables and multiple elements seek to do so. A good policy to adopt for your applications in these situations is a first come, first served policy. Ensure global configuration has the first opportunity followed by higher priority behaviours in the tree and so forth. Lower priority behaviours would use this to respect the pre-configured setting and at most, just validate that it is acceptable to the functionality of it’s own behaviour.
- Args:
name: name of the variable to set value: value of the variable to set overwrite: do not set if the variable already exists on the blackboard
- Returns:
success or failure (overwrite is False and variable already set)
- Raises:
AttributeError: if the client does not have write access to the variable KeyError: if the variable does not yet exist on the blackboard
- unregister(clear: bool = True) None
Unregister this blackboard client.
If requested, clear key-value pairs if this client is the last user of those variables.
- Args:
clear: remove key-values pairs from the blackboard
- unregister_all_keys(clear: bool = True) None
Unregister all keys currently registered by this blackboard client.
If requested, clear key-value pairs if this client is the last user of those variables.
- Args:
clear: remove key-values pairs from the blackboard
- unregister_key(key: str, clear: bool = True, update_namespace_cache: bool = True) None
Unegister a key associated with this client.
- Args:
key: key to unregister clear: remove key-values pairs from the blackboard update_namespace_cache: disable if you are batching
A method that batches calls to this method is
unregister_all_keys()
.- Raises:
KeyError if the key has not been previously registered
- unset(key: str) bool
Unset a blackboard variable.
Use to completely remove a blackboard variable (key-value pair).
- Args:
key: name of the variable to remove
- Returns:
True if the variable was removed, False if it was already absent
- verify_required_keys_exist() None
Check for existence of all keys registered as ‘required’.
Raises: KeyError if any of the required keys do not exist on the blackboard
- class py_trees.blackboard.IntermediateVariableFetcher(blackboard: Client, namespace: str)
Bases:
object
Convenient attribute accessor constrained to (possibly nested) namespaces.
- class py_trees.blackboard.KeyMetaData
Bases:
object
Stores the aggregated metadata for a key on the blackboard.