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.

images/blackboard.jpg

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)
images/blackboard_client_instantiation.png

Client Instantiation

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)
images/blackboard_read_write.png

Variable Read/Write Registration

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)
images/blackboard_namespaces.png

Namespaces and Namespaced Clients

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)
images/blackboard_nested.png

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()
images/blackboard_activity_stream.png

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))
images/blackboard_display.png

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):

images/blackboard_trees.png

Tree level debugging

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.