├── .devcontainer ├── Dockerfile ├── README.md ├── devcontainer.json └── py312 │ └── .devcontainer │ └── devcontainer.json ├── .github └── workflows │ ├── pre-merge.yaml │ ├── push_poetry_container.yaml │ └── update-poetry-cache.yaml ├── .gitignore ├── .readthedocs.yaml ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── DEVELOPING.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── .gitignore ├── Makefile ├── behaviours.rst ├── blackboards.rst ├── changelog.rst ├── composites.rst ├── conf.py ├── decorators.rst ├── demos.rst ├── dot │ ├── blackboard-with-variables.dot │ ├── composites.dot │ ├── decorators.dot │ ├── demo-blackboard.dot │ ├── demo-context_switching.dot │ ├── demo-dot-graphs.dot │ ├── demo-either-or.dot │ ├── demo-eternal-guard.dot │ ├── demo-logging.dot │ ├── demo-selector.dot │ ├── demo-sequence.dot │ ├── demo-tree-stewardship.dot │ ├── demo_tree.dot │ ├── idiom-either-or.dot │ ├── naive_context_switching.dot │ ├── oneshot.dot │ ├── parallel.dot │ ├── pick_up_where_you_left_off.dot │ ├── selector.dot │ ├── selector_with_memory.dot │ ├── sequence.dot │ └── sequence_with_memory.dot ├── examples │ ├── blackboard_activity_stream.py │ ├── blackboard_behaviour.py │ ├── blackboard_disconnected.py │ ├── blackboard_display.py │ ├── blackboard_namespaces.py │ ├── blackboard_nested.py │ ├── blackboard_read_write.py │ ├── decorators.py │ ├── oneshot.py │ ├── parallel.py │ ├── pickup_where_you_left_off.py │ ├── selector.py │ ├── selector_with_memory.py │ ├── sequence.py │ ├── sequence_with_memory.py │ ├── skeleton_behaviour.py │ └── skeleton_tree.py ├── faq.rst ├── idioms.rst ├── images │ ├── action.gif │ ├── ascii_tree.png │ ├── ascii_tree_simple.png │ ├── blackboard.jpg │ ├── blackboard_activity_stream.png │ ├── blackboard_client_instantiation.png │ ├── blackboard_demo.png │ ├── blackboard_display.png │ ├── blackboard_namespaces.png │ ├── blackboard_nested.png │ ├── blackboard_read_write.png │ ├── blackboard_remappings.png │ ├── blackboard_trees.png │ ├── context_switching.gif │ ├── crazy_hospital.jpg │ ├── display_modes.png │ ├── either_or.gif │ ├── eternal_guard.gif │ ├── lifecycle.gif │ ├── logging.gif │ ├── many-hats.png │ ├── pick_up_where_you_left_off.gif │ ├── render.gif │ ├── selector.gif │ ├── sequence.gif │ ├── ticking_tree.jpg │ ├── tree_stewardship.gif │ └── yggdrasil.jpg ├── index.rst ├── introduction.rst ├── modules.rst ├── programs.rst ├── requirements.txt ├── terminology.rst ├── the_crazy_hospital.rst ├── trees.rst ├── visualisation.rst └── weblinks.rst ├── package.xml ├── poetry.lock ├── py_trees ├── __init__.py ├── behaviour.py ├── behaviours.py ├── blackboard.py ├── common.py ├── composites.py ├── console.py ├── decorators.py ├── demos │ ├── README.md │ ├── __init__.py │ ├── action.py │ ├── blackboard.py │ ├── blackboard_namespaces.py │ ├── blackboard_remappings.py │ ├── context_switching.py │ ├── display_modes.py │ ├── dot_graphs.py │ ├── either_or.py │ ├── eternal_guard.py │ ├── lifecycle.py │ ├── logging.py │ ├── pick_up_where_you_left_off.py │ ├── selector.py │ ├── sequence.py │ └── stewardship.py ├── display.py ├── idioms.py ├── logging.py ├── meta.py ├── programs │ ├── __init__.py │ └── render.py ├── py.typed ├── syntax_highlighting.py ├── tests.py ├── timers.py ├── trees.py ├── utilities.py ├── version.py └── visitors.py ├── pyproject.toml ├── setup.py ├── tests ├── README.md ├── __init__.py ├── benchmark_blackboard.py ├── profile_blackboard ├── test_blackboard.py ├── test_blackboard_behaviours.py ├── test_composites.py ├── test_console.py ├── test_decorators.py ├── test_display.py ├── test_either_or.py ├── test_eternal_guard.py ├── test_meta.py ├── test_oneshot.py ├── test_parallels.py ├── test_pickup.py ├── test_probabilistic_behaviour.py ├── test_selectors.py ├── test_sequences.py ├── test_timer.py ├── test_tip.py ├── test_tree.py ├── test_utilities.py └── test_visitors.py └── tox.ini /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Base 3 | # - a single-version python slim-bullseye image 4 | # Installs 5 | # - poetry in /opt/poetry 6 | # - adds a user called 'zen' 7 | # Size 8 | # - 300MB 9 | ################################################################################ 10 | 11 | ARG PYTHON_VERSION=3.8.15 12 | ARG DEBIAN_VERSION=bullseye 13 | 14 | FROM python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION} 15 | 16 | ARG NAME=poetry-zen 17 | ARG POETRY_VERSION=1.3.2 18 | ENV POETRY_HOME=/opt/poetry 19 | ENV PATH="${POETRY_HOME}/bin:${PATH}" 20 | 21 | ################################################################################ 22 | # Poetry 23 | ################################################################################ 24 | 25 | RUN apt-get update && apt-get install -y --no-install-recommends \ 26 | # For poetry 27 | curl \ 28 | # For pytrees 29 | graphviz \ 30 | make \ 31 | # For convenience 32 | bash \ 33 | bash-completion \ 34 | ca-certificates \ 35 | git \ 36 | less \ 37 | ssh \ 38 | vim \ 39 | wget \ 40 | && \ 41 | curl -sSL https://install.python-poetry.org | POETRY_VERSION=${POETRY_VERSION} python3 - && \ 42 | poetry config virtualenvs.create false && \ 43 | poetry completions bash >> ~/.bash_completion 44 | 45 | ################################################################################ 46 | # Login Shells for Debugging & Development 47 | ################################################################################ 48 | 49 | # In a login shell (below), the PATH env doesn't survive, configure it at ground zero 50 | RUN echo "export PATH=${POETRY_HOME}/bin:${PATH}" >> /etc/profile 51 | ENV TERM xterm-256color 52 | ENTRYPOINT ["/bin/bash", "--login", "-i"] 53 | 54 | ################################################################################ 55 | # Development with a user, e.g. for vscode devcontainers 56 | ################################################################################ 57 | 58 | ARG USERNAME=zen 59 | ARG USER_UID=1000 60 | ARG USER_GID=${USER_UID} 61 | 62 | RUN groupadd --gid $USER_GID $USERNAME && \ 63 | useradd --uid $USER_UID --gid $USER_GID -s "/bin/bash" -m $USERNAME && \ 64 | apt-get install -y sudo && \ 65 | echo "${USERNAME} ALL=NOPASSWD: ALL" > /etc/sudoers.d/${USERNAME} && \ 66 | chmod 0440 /etc/sudoers.d/${USERNAME} 67 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/${USERNAME}/.bashrc && \ 68 | echo "alias ll='ls --color=auto -alFNh'" >> /home/${USERNAME}/.bashrc && \ 69 | echo "alias ls='ls --color=auto -Nh'" >> /home/${USERNAME}/.bashrc && \ 70 | poetry completions bash >> /home/${USERNAME}/.bash_completion 71 | 72 | # touch /home/${USERNAME}/.bash_completion && chown ${USERNAME}:${USERNAME} /home/${USERNAME}/.bash_completion 73 | 74 | ################################################################################ 75 | # Debugging with root 76 | ################################################################################ 77 | 78 | RUN echo "export PS1='\[\033[01;36m\](docker)\[\033[00m\] \[\033[01;32m\]\u@${NAME}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> ${HOME}/.bashrc && \ 79 | echo "alias ll='ls --color=auto -alFNh'" >> ${HOME}/.bashrc && \ 80 | echo "alias ls='ls --color=auto -Nh'" >> ${HOME}/.bashrc 81 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | 3 | These VSCode devcontainers setup multiple environments for testing against 4 | different python versions. 5 | 6 | ## Setup 7 | 8 | ``` 9 | $ git clone git@github.com:splintered-reality/py_trees.git 10 | $ code ./py_trees 11 | ``` 12 | 13 | ## VSCode DevContainer 14 | 15 | At this point you can either "Re-open project in container" to develop against 16 | the default python version. 17 | 18 | Alternatively "Open Folder in Container" and point it at one of the 19 | `py` subfolders in this directory to develop against a different 20 | python version. 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "py_trees-310", 3 | 4 | "build": { 5 | "dockerfile": "./Dockerfile", 6 | "args": { 7 | "NAME": "py_trees-310", 8 | "POETRY_VERSION": "1.8.4", 9 | "PYTHON_VERSION": "3.10.15", 10 | "DEBIAN_VERSION": "bullseye" 11 | } 12 | }, 13 | "containerEnv": { 14 | "POETRY_HTTP_BASIC_PYPI_USERNAME": "${localEnv:POETRY_HTTP_BASIC_PYPI_USERNAME}", 15 | "POETRY_HTTP_BASIC_PYPI_PASSWORD": "${localEnv:POETRY_HTTP_BASIC_PYPI_PASSWORD}" 16 | }, 17 | "remoteUser": "zen", 18 | "customizations": { 19 | "vscode": { 20 | "extensions": [ 21 | "bierner.github-markdown-preview", 22 | "bungcip.better-toml", 23 | "streetsidesoftware.code-spell-checker", 24 | "lextudio.restructuredtext", 25 | "ms-python.python", 26 | "omnilib.ufmt" 27 | ] 28 | } 29 | }, 30 | "postCreateCommand": "poetry install", 31 | "workspaceMount": "source=${localWorkspaceFolder}/../..,target=/workspaces,type=bind", 32 | "workspaceFolder": "/workspaces" 33 | } 34 | -------------------------------------------------------------------------------- /.devcontainer/py312/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "py_trees-312", 3 | 4 | "build": { 5 | "dockerfile": "../../Dockerfile", 6 | "args": { 7 | "NAME": "py_trees-312", 8 | "POETRY_VERSION": "1.8.4", 9 | "PYTHON_VERSION": "3.12.4", 10 | "DEBIAN_VERSION": "bullseye" 11 | }, 12 | "context": ".." 13 | }, 14 | "containerEnv": { 15 | "POETRY_HTTP_BASIC_PYPI_USERNAME": "${localEnv:POETRY_HTTP_BASIC_PYPI_USERNAME}", 16 | "POETRY_HTTP_BASIC_PYPI_PASSWORD": "${localEnv:POETRY_HTTP_BASIC_PYPI_PASSWORD}" 17 | }, 18 | "remoteUser": "zen", 19 | "customizations": { 20 | "vscode": { 21 | "extensions": [ 22 | "bierner.github-markdown-preview", 23 | "bierner.markdown-preview-github-styles", 24 | "bungcip.better-toml", 25 | "eamodio.gitlens", 26 | "ms-python.python", 27 | "omnilib.ufmt", 28 | "redhat.vscode-yaml", 29 | "streetsidesoftware.code-spell-checker", 30 | "tht13.rst-vscode" 31 | ] 32 | } 33 | }, 34 | "postCreateCommand": "poetry install", 35 | // Breaks codespaces 36 | // "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces,type=bind", 37 | // "workspaceFolder": "/workspaces" 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/pre-merge.yaml: -------------------------------------------------------------------------------- 1 | name: pre-merge 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }} 6 | 7 | on: 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | pre-merge: 13 | runs-on: ubuntu-24.04 14 | strategy: 15 | matrix: 16 | python-version: ["3.10", "3.12"] 17 | include: 18 | - python-version: "3.10" 19 | python-py-version: "py310" 20 | - python-version: "3.12" 21 | python-py-version: "py312" 22 | container: 23 | image: ghcr.io/${{ github.repository }}-ci:${{ matrix.python-py-version }}-poetry-bullseye 24 | credentials: 25 | username: ${{ github.actor }} 26 | password: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Poetry Venv Dir 31 | run: | 32 | echo "VENV_DIR=$(poetry config virtualenvs.path)" >> $GITHUB_ENV 33 | 34 | - name: Restore the Cache 35 | id: cache-deps 36 | uses: actions/cache@v4 37 | with: 38 | path: ${{ env.VENV_DIR }} 39 | # bump the suffix if you need to force-refresh the cache 40 | key: py-trees-ci-cache-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock', '**/tox.ini') }}-1 41 | 42 | # Install all deps, sans the project (--no-root) 43 | - name: Poetry - Install Dependencies 44 | run: poetry install --no-interaction --no-root 45 | if: steps.cache-deps.outputs.cache-hit != 'true' 46 | 47 | # Project is installed separately to avoid always invalidating the cache 48 | - name: Poetry - Install Project 49 | run: poetry install --no-interaction 50 | 51 | # TODO: Caching above doesn't make much sense when tox effectively re-installs deps 52 | - name: Tox - Tests 53 | run: poetry run tox --workdir ${{ env.VENV_DIR }} -e ${{ matrix.python-py-version }} 54 | - name: Tox - Formatters, Linters 55 | run: poetry run tox --workdir ${{ env.VENV_DIR }} -e check 56 | - name: Tox - MyPy 57 | run: poetry run tox --workdir ${{ env.VENV_DIR }} -e my${{ matrix.python-py-version }} 58 | -------------------------------------------------------------------------------- /.github/workflows/push_poetry_container.yaml: -------------------------------------------------------------------------------- 1 | name: push-poetry-container 2 | 3 | env: 4 | REGISTRY: ghcr.io 5 | IMAGE_NAME: ${{ github.repository }}-ci 6 | POETRY_VERSION: 1.3.2 7 | PYTHON_PRIMARY_VERSION: 3.10.15 8 | PYTHON_PRIMARY_TAG: py310 9 | PYTHON_SECONDARY_VERSION: 3.12.4 10 | PYTHON_SECONDARY_TAG: py312 11 | DEBIAN_VERSION: bullseye 12 | 13 | on: 14 | push: 15 | paths: 16 | - .devcontainer/Dockerfile 17 | branches: 18 | - devel 19 | workflow_dispatch: 20 | 21 | jobs: 22 | push-poetry-container: 23 | runs-on: ubuntu-24.04 24 | permissions: 25 | contents: read 26 | packages: write 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | - name: Login to GCR 31 | uses: docker/login-action@v3 32 | with: 33 | registry: ghcr.io 34 | username: ${{ github.actor }} 35 | password: ${{ secrets.GITHUB_TOKEN }} 36 | - name: Metadata 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 41 | - name: Echo 42 | run: | 43 | echo "USER: ${{ github.actor }}" 44 | echo "REPOSITORY: ${{ github.repository }}" 45 | echo "POETRY_VERSION: ${POETRY_VERSION}" 46 | echo "PYTHON_PRIMARY_VERSION: ${PYTHON_PRIMARY_VERSION}" 47 | echo "PYTHON_SECONDARY_VERSION: ${PYTHON_SECONDARY_VERSION}" 48 | echo "TAGS: ${{ steps.meta.outputs.tags }}" 49 | echo "LABELS: ${{ steps.meta.outputs.labels }}" 50 | - name: Image - poetry${{ env.POETRY_VERSION }}-python${{ env.PYTHON_PRIMARY_VERSION }} 51 | uses: docker/build-push-action@v6 52 | with: 53 | file: ./.devcontainer/Dockerfile 54 | push: true 55 | build-args: | 56 | PYTHON_VERSION=${{ env.PYTHON_PRIMARY_VERSION }} 57 | POETRY_VERSION=${{ env.POETRY_VERSION }} 58 | DEBIAN_VERSION=${{ env.DEBIAN_VERSION }} 59 | tags: | 60 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.PYTHON_PRIMARY_TAG }}-poetry-${{ env.DEBIAN_VERSION }} 61 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:python${{ env.PYTHON_PRIMARY_VERSION }}-poetry${{ env.POETRY_VERSION }}-${{ env.DEBIAN_VERSION }} 62 | labels: ${{ steps.meta.outputs.labels }} 63 | - name: Image - poetry${{ env.POETRY_VERSION }}-python${{ env.PYTHON_SECONDARY_VERSION }} 64 | uses: docker/build-push-action@v6 65 | with: 66 | file: ./.devcontainer/Dockerfile 67 | push: true 68 | build-args: | 69 | PYTHON_VERSION=${{ env.PYTHON_SECONDARY_VERSION }} 70 | POETRY_VERSION=${{ env.POETRY_VERSION }} 71 | DEBIAN_VERSION=${{ env.DEBIAN_VERSION }} 72 | tags: | 73 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.PYTHON_SECONDARY_TAG }}-poetry-${{ env.DEBIAN_VERSION }} 74 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:python${{ env.PYTHON_SECONDARY_VERSION }}-poetry${{ env.POETRY_VERSION }}-${{ env.DEBIAN_VERSION }} 75 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/update-poetry-cache.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Ensure poetry and tox installed dependencies are in the cache. 3 | # 4 | # All PR's can reuse devel's caches, but a PR's cache cannot be reused from 5 | # one PR to the next. This jobs' sole purpose is to make sure every PR updates 6 | # devel's cache (if changes are needed) on merging. 7 | # 8 | # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows 9 | ################################################################################ 10 | name: update-poetry-cache 11 | 12 | on: 13 | push: 14 | branches: 15 | - devel 16 | workflow_dispatch: 17 | 18 | jobs: 19 | update-poetry-cache: 20 | runs-on: ubuntu-24.04 21 | strategy: 22 | matrix: 23 | python-version: ["3.10", "3.12"] 24 | include: 25 | - python-version: "3.10" 26 | python-py-version: "py310" 27 | - python-version: "3.12" 28 | python-py-version: "py312" 29 | container: 30 | image: ghcr.io/${{ github.repository }}-ci:${{ matrix.python-py-version }}-poetry-bullseye 31 | credentials: 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Poetry Venv Dir 38 | run: | 39 | echo "VENV_DIR=$(poetry config virtualenvs.path)" >> $GITHUB_ENV 40 | 41 | - name: Restore the Cache 42 | id: cache-deps 43 | uses: actions/cache@v4 44 | with: 45 | path: ${{ env.VENV_DIR }} 46 | # bump the suffix if you need to force-refresh the cache 47 | key: py-trees-ci-cache-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock', '**/tox.ini') }}-1 48 | 49 | # Install all deps, sans the project (--no-root) 50 | - name: Poetry - Install Dependencies 51 | run: poetry install --no-interaction --no-root 52 | if: steps.cache-deps.outputs.cache-hit != 'true' 53 | 54 | - name: Tox - Install Dependencies 55 | run: poetry run tox --workdir ${{ env.VENV_DIR }} --notest -e ${{ matrix.python-py-version }},check,my${{ matrix.python-py-version }} 56 | if: steps.cache-deps.outputs.cache-hit != 'true' 57 | 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .venv 3 | py_trees.egg-info 4 | */doc/html 5 | */doc/manifest.yaml 6 | *.pyc 7 | *.png 8 | *.svg 9 | build 10 | dist 11 | deb_dist 12 | py_trees*.tar.gz 13 | *~ 14 | \#* 15 | .#* 16 | .DS_Store 17 | .README.md.html 18 | .eggs 19 | tests/.README.md.html 20 | nosetests.html 21 | eclipse 22 | dump.json 23 | tests/blackboard.cprofile 24 | .tox 25 | testies 26 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | apt_packages: 14 | - graphviz 15 | 16 | # Build documentation in the docs/ directory with Sphinx 17 | sphinx: 18 | configuration: docs/conf.py 19 | fail_on_warning: true 20 | 21 | # If using Sphinx, optionally build your docs in additional formats such as PDF 22 | # formats: 23 | # - pdf 24 | 25 | # Optionally declare the Python requirements required to build your docs 26 | python: 27 | install: 28 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bierner.github-markdown-preview", 4 | "bungcip.better-toml", 5 | "streetsidesoftware.code-spell-checker", 6 | "lextudio.restructuredtext", 7 | "ms-python.python", 8 | "ms-vscode-remote.vscode-remote-extensionpack", 9 | "omnilib.ufmt" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": false, 3 | "python.formatting.provider": "none", 4 | "[python]": { 5 | "editor.defaultFormatter": "omnilib.ufmt", 6 | "editor.formatOnSave": true 7 | }, 8 | "cSpell.language": "en-GB", 9 | "cSpell.words": [ 10 | "backported", 11 | "behaviour", 12 | "behaviours", 13 | "bierner", 14 | "bungcip", 15 | "omnilib", 16 | "py_trees", 17 | "pydot", 18 | "pypi", 19 | "ufmt", 20 | "usort" 21 | ] 22 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Successfully collaborating on common problems is always an edifying experience that increases the desire to do more. 4 | 5 | ## Development Environment 6 | 7 | Short of having a poetry environment of your own, you can make use of github's codespaces. 8 | 9 | Refer to the [README - Getting Started](../README.md#geting-started) for details. 10 | 11 | ## Pull Requests 12 | 13 | ### Before Submitting 14 | 15 | Some recommendations to help align your contribution and minimise the eventual back and forth on a PR: 16 | 17 | * Engage in an issue thread or in the discussion section. 18 | * Actual code in the form of a minimal PR with a `status:do_not_merge` label helps generate useful dialogue. 19 | 20 | ### Which Branch? 21 | 22 | * If it's a new feature, or bugfix applicable to the latest code, `devel` 23 | * If it's a bugfix that can't be applied to `devel`, but critical for a release, point it at the release branch (e.g. `release/0.6.x`) 24 | 25 | If it is a feature or bugfix that you'd like to see backported to one of the release branches, open a parallel PR for that release branch or mention that you'd like to see it backported in the original PR's description. 26 | 27 | ### The Pull Request 28 | 29 | Be sure to state clearly in the pull request's **description** (this helps expedite review): 30 | 31 | * The motivation, i.e. what problem is this solving. 32 | * A concise summary of what was done (and why if relevant). 33 | 34 | ### Pre-Merge Checks 35 | 36 | CI get cranky on a variety of things - if it complains, make sure your PR is passing the following checks 37 | locally. 38 | 39 | * [Test-Lint-Format](./DEVELOPING.md#test-format-lint) 40 | * [Make Docs](./DEVELOPING#documentation) 41 | 42 | ### Changelog 43 | 44 | * Please update the `Forthcoming` section in the [Changelog](Changelog.rst) with your change and a link to your PR. 45 | * The style should be self-explanatory. 46 | * Do not worry about incrementing the version, releases are handled separately. 47 | 48 | ### Review 49 | 50 | Once submitted, a reviewer will be assigned. You do not need to select. If no-one has self-assigned in a reasonable time window, feel free to append a *friendly bump* comment to your PR. 51 | 52 | ### Merging 53 | 54 | Once the large button has gone `GREEN`, you or the reviewer may merge the pull request. 55 | 56 | ## Releasing 57 | 58 | If you are interested in seeing your changes make it into a release (sooner rather than later), please make the request via a comment in your PR or in an issue. 59 | 60 | ## Social Conduct 61 | 62 | Be prepared to be tickled by noodly appendages and at all times, be froody. 63 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | [[Test-Format-Lint](#test-format-lint)] [[Documentation](#documentation)] [[Packaging]](#packaging)] 4 | 5 | 6 | ## Test-Format-Lint 7 | 8 | Check against at least one of py310 / py312 [1]. 9 | 10 | ``` 11 | # Auto-format your code (if using VSCode, install the ufmt extension) 12 | $ poetry run tox -e format 13 | 14 | # Style, Format 15 | $ poetry run tox -e check 16 | 17 | # Type-Check 18 | $ poetry run tox -e mypy310 19 | 20 | # Tests 21 | $ poetry run tox -e py310 22 | ``` 23 | 24 | [1] CI will test against both python versions for you, but should you wish to do so locally, open up two VSCode windows, one with the project opened in the default [py310 devcontainer](.devcontainer) and the other with the [py312 devcontainer](.devcontainer/py312). 25 | 26 | ## Documentation 27 | 28 | Generate the docs, view them from `./docs/html` in a browser. 29 | 30 | ``` 31 | # Install dependencies 32 | $ poetry install --with docs 33 | 34 | # Build 35 | $ poetry run make -C docs html 36 | ``` 37 | 38 | On Doc dependency changes, export the requirements for ReadTheDocs 39 | 40 | ``` 41 | $ poetry export -f requirements.txt --with docs -o docs/requirements.txt 42 | ``` 43 | 44 | ## Packaging 45 | 46 | If you have permission to publish on pypi: 47 | 48 | ``` 49 | $ poetry config http-basic.pypi ${POETRY_HTTP_BASIC_PYPI_USERNAME} ${POETRY_HTTP_BASIC_PYPI_PASSWORD} 50 | $ poetry publish 51 | ``` 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Software License Agreement (BSD License) 2 | # 3 | # Copyright (c) 2020 Daniel Stonier 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions 8 | # are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # * Redistributions in binary form must reproduce the above 13 | # copyright notice, this list of conditions and the following 14 | # disclaimer in the documentation and/or other materials provided 15 | # with the distribution. 16 | # * Neither the name of the copyright holder nor the names of its 17 | # contributors may be used to endorse or promote products derived 18 | # from this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 25 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 30 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | # POSSIBILITY OF SUCH DAMAGE. 32 | 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################################# 2 | # Build Documentation 3 | ############################################################################################# 4 | 5 | help: 6 | @echo "Documentation" 7 | @echo " docs : buidl sphinx documentation" 8 | 9 | docs: 10 | PY_TREES_DISABLE_COLORS=1 sphinx-build -E -b html docs docs/html 11 | 12 | clean: 13 | -rm -rf docs/html 14 | 15 | .PHONY: docs clean 16 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | doctrees 2 | html 3 | manifest.yaml 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= -E 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = . 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @PY_TREES_DISABLE_COLORS=1 $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/blackboards.rst: -------------------------------------------------------------------------------- 1 | .. _blackboards-section: 2 | 3 | Blackboards 4 | =========== 5 | 6 | .. automodule:: py_trees.blackboard 7 | :noindex: 8 | 9 | The primary user-facing interface with the blackboard is via the Client. 10 | 11 | .. autoclass:: py_trees.blackboard.Client 12 | :noindex: 13 | 14 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/composites.rst: -------------------------------------------------------------------------------- 1 | .. _composites-section: 2 | 3 | Composites 4 | ========== 5 | 6 | .. automodule:: py_trees.composites 7 | :noindex: 8 | 9 | .. _selector-section: 10 | 11 | Selector 12 | -------- 13 | 14 | .. autoclass:: py_trees.composites.Selector 15 | :noindex: 16 | 17 | .. _sequence-section: 18 | 19 | Sequence 20 | -------- 21 | 22 | .. autoclass:: py_trees.composites.Sequence 23 | :noindex: 24 | 25 | .. _parallel-section: 26 | 27 | Parallel 28 | -------- 29 | 30 | .. autoclass:: py_trees.composites.Parallel 31 | :noindex: 32 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | ################################################################################ 10 | # Setup 11 | ################################################################################ 12 | 13 | import os 14 | import sys 15 | 16 | project_dir = os.path.abspath( 17 | os.path.join(os.path.abspath(__file__), os.pardir, os.pardir) 18 | ) 19 | version_file = os.path.join(project_dir, "py_trees", "version.py") 20 | with open(version_file) as f: 21 | exec(f.read()) # makes __version__ available 22 | 23 | ################################################################################ 24 | # Autodoc - help it find the project 25 | # https://sphinx-rtd-tutorial.readthedocs.io/en/latest/sphinx-config.html#autodoc-configuration 26 | ################################################################################ 27 | 28 | sys.path.insert(0, project_dir) 29 | 30 | ################################################################################ 31 | # Project Info 32 | ################################################################################ 33 | 34 | project = "py_trees" 35 | copyright = "2023, Daniel Stonier" 36 | author = "Daniel Stonier" 37 | 38 | version = __version__ 39 | release = version 40 | 41 | 42 | ################################################################################ 43 | # Extensions 44 | ################################################################################ 45 | 46 | extensions = [ 47 | "sphinx.ext.autodoc", 48 | "sphinx.ext.coverage", 49 | "sphinx.ext.doctest", 50 | "sphinx.ext.graphviz", 51 | "sphinx.ext.ifconfig", 52 | "sphinx.ext.intersphinx", 53 | "sphinx.ext.mathjax", 54 | "sphinx.ext.napoleon", 55 | "sphinx.ext.todo", 56 | "sphinx.ext.viewcode", 57 | "sphinxarg.ext", 58 | ] 59 | 60 | ################################################################################ 61 | # Extensions Configuration 62 | ################################################################################ 63 | 64 | # 'signature', 'description' or 'both' 65 | autodoc_typehints = "both" 66 | 67 | # If true, use the :ivar: role for instance variables, else .. attribute::. 68 | napoleon_use_ivar = True 69 | 70 | # If you don't add this, todos don't appear 71 | todo_include_todos = True 72 | 73 | ################################################################################ 74 | # General configuration 75 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 76 | ################################################################################ 77 | 78 | templates_path = ["_templates"] 79 | exclude_patterns = ["_build", "weblinks.rst"] 80 | language = "en" 81 | 82 | ################################################################################ 83 | # HTML 84 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 85 | ################################################################################ 86 | 87 | html_theme = "sphinx_rtd_theme" 88 | 89 | # Add any paths that contain custom static files (such as style sheets) here, 90 | # relative to this directory. They are copied after the builtin static files, 91 | # so a file named "default.css" will overwrite the builtin "default.css". 92 | html_static_path = [] 93 | 94 | # If true, “(C) Copyright …” is shown in the HTML footer. Default is True. 95 | html_show_copyright = False 96 | 97 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 98 | html_show_sphinx = False 99 | 100 | ################################################################################ 101 | # Intersphinx 102 | # https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration 103 | ################################################################################ 104 | 105 | # Refer to the Python standard library. 106 | intersphinx_mapping = { 107 | "python": ("https://docs.python.org/3", None), 108 | } 109 | -------------------------------------------------------------------------------- /docs/decorators.rst: -------------------------------------------------------------------------------- 1 | Decorators 2 | ========== 3 | 4 | .. automodule:: py_trees.decorators 5 | :noindex: 6 | -------------------------------------------------------------------------------- /docs/demos.rst: -------------------------------------------------------------------------------- 1 | .. _demos-section-label: 2 | 3 | Demos 4 | ===== 5 | 6 | .. _py-trees-demo-action-behaviour-program: 7 | 8 | py-trees-demo-action-behaviour 9 | ------------------------------ 10 | 11 | .. automodule:: py_trees.demos.action 12 | :members: 13 | :special-members: 14 | :show-inheritance: 15 | :synopsis: demo a complex action style behaviour 16 | 17 | .. literalinclude:: ../py_trees/demos/action.py 18 | :language: python 19 | :linenos: 20 | :caption: py_trees/demos/action.py 21 | 22 | .. _py-trees-demo-behaviour-lifecycle-program: 23 | 24 | py-trees-demo-behaviour-lifecycle 25 | --------------------------------- 26 | 27 | .. automodule:: py_trees.demos.lifecycle 28 | :members: 29 | :special-members: 30 | :show-inheritance: 31 | :synopsis: demo the lifecycle of a behaviour 32 | 33 | .. literalinclude:: ../py_trees/demos/lifecycle.py 34 | :language: python 35 | :linenos: 36 | :caption: py_trees/demos/lifecycle.py 37 | 38 | .. _py-trees-demo-blackboard-program: 39 | 40 | py-trees-demo-blackboard 41 | ------------------------ 42 | 43 | .. automodule:: py_trees.demos.blackboard 44 | :members: 45 | :special-members: 46 | :show-inheritance: 47 | :synopsis: demo blackboard set/get and related behaviours 48 | 49 | .. literalinclude:: ../py_trees/demos/blackboard.py 50 | :language: python 51 | :linenos: 52 | :caption: py_trees/demos/blackboard.py 53 | 54 | .. _py-trees-demo-blackboard-namespaces-program: 55 | 56 | py-trees-demo-blackboard-namespaces 57 | ----------------------------------- 58 | 59 | .. automodule:: py_trees.demos.blackboard_namespaces 60 | :members: 61 | :special-members: 62 | :show-inheritance: 63 | :synopsis: demo blackboard namespacing concepts 64 | 65 | .. literalinclude:: ../py_trees/demos/blackboard_namespaces.py 66 | :language: python 67 | :linenos: 68 | :caption: py_trees/demos/blackboard_namespaces.py 69 | 70 | .. _py-trees-demo-blackboard-remappings-program: 71 | 72 | py-trees-demo-blackboard-remappings 73 | ----------------------------------- 74 | 75 | .. automodule:: py_trees.demos.blackboard_remappings 76 | :members: 77 | :special-members: 78 | :show-inheritance: 79 | :synopsis: demo blackboard key remappings 80 | 81 | .. literalinclude:: ../py_trees/demos/blackboard_remappings.py 82 | :language: python 83 | :linenos: 84 | :caption: py_trees/demos/blackboard_remappings.py 85 | 86 | .. _py-trees-demo-context-switching-program: 87 | 88 | py-trees-demo-context-switching 89 | ------------------------------- 90 | 91 | .. automodule:: py_trees.demos.context_switching 92 | :members: 93 | :special-members: 94 | :show-inheritance: 95 | :synopsis: demo context switching use case with parallels 96 | 97 | .. literalinclude:: ../py_trees/demos/context_switching.py 98 | :language: python 99 | :linenos: 100 | :caption: py_trees/demos/contex_switching.py 101 | 102 | .. _py-trees-demo-dot-graphs-program: 103 | 104 | py-trees-demo-dot-graphs 105 | ------------------------ 106 | 107 | .. automodule:: py_trees.demos.dot_graphs 108 | :members: 109 | :special-members: 110 | :show-inheritance: 111 | :synopsis: demo dot graphs with varying visibility levels 112 | 113 | .. literalinclude:: ../py_trees/demos/dot_graphs.py 114 | :language: python 115 | :linenos: 116 | :caption: py_trees/demos/dot_graphs.py 117 | 118 | .. _py-trees-demo-either-or-program: 119 | 120 | py-trees-demo-either-or 121 | ----------------------- 122 | 123 | .. automodule:: py_trees.demos.either_or 124 | :members: 125 | :special-members: 126 | :show-inheritance: 127 | :synopsis: demo use of the either_or idiom 128 | 129 | .. literalinclude:: ../py_trees/demos/either_or.py 130 | :language: python 131 | :linenos: 132 | :caption: py_trees/demos/either_or.py 133 | 134 | .. _py-trees-demo-eternal-guard: 135 | 136 | py-trees-demo-eternal-guard 137 | --------------------------- 138 | 139 | .. automodule:: py_trees.demos.eternal_guard 140 | :members: 141 | :special-members: 142 | :show-inheritance: 143 | :synopsis: demo the 'eternal guard' concept 144 | 145 | .. literalinclude:: ../py_trees/demos/eternal_guard.py 146 | :language: python 147 | :linenos: 148 | :caption: py_trees/demos/eternal_guard.py 149 | 150 | .. _py-trees-demo-logging-program: 151 | 152 | py-trees-demo-logging 153 | --------------------- 154 | 155 | .. automodule:: py_trees.demos.logging 156 | :members: 157 | :special-members: 158 | :show-inheritance: 159 | :synopsis: demo tree logging to json files 160 | 161 | .. literalinclude:: ../py_trees/demos/logging.py 162 | :language: python 163 | :linenos: 164 | :caption: py_trees/demos/logging.py 165 | 166 | .. _py-trees-demo-selector-program: 167 | 168 | py-trees-demo-selector 169 | ---------------------- 170 | 171 | .. automodule:: py_trees.demos.selector 172 | :members: 173 | :special-members: 174 | :show-inheritance: 175 | :synopsis: demo priority switching in selectors 176 | 177 | .. literalinclude:: ../py_trees/demos/selector.py 178 | :language: python 179 | :linenos: 180 | :caption: py_trees/demos/selector.py 181 | 182 | .. _py-trees-demo-sequence-program: 183 | 184 | py-trees-demo-sequence 185 | ---------------------- 186 | 187 | .. automodule:: py_trees.demos.sequence 188 | :members: 189 | :special-members: 190 | :show-inheritance: 191 | :synopsis: demo sequences in action 192 | 193 | .. literalinclude:: ../py_trees/demos/sequence.py 194 | :language: python 195 | :linenos: 196 | :caption: py_trees/demos/sequence.py 197 | 198 | .. _py-trees-demo-tree-stewardship-program: 199 | 200 | py-trees-demo-tree-stewardship 201 | ------------------------------ 202 | 203 | .. automodule:: py_trees.demos.stewardship 204 | :members: 205 | :special-members: 206 | :show-inheritance: 207 | :synopsis: demo tree stewardship 208 | 209 | .. literalinclude:: ../py_trees/demos/stewardship.py 210 | :language: python 211 | :linenos: 212 | :caption: py_trees/demos/stewardship.py 213 | 214 | .. _py-trees-demo-pick-up-where-you-left-off-program: 215 | 216 | py-trees-demo-pick-up-where-you-left-off 217 | ---------------------------------------- 218 | 219 | .. automodule:: py_trees.demos.pick_up_where_you_left_off 220 | :members: 221 | :special-members: 222 | :show-inheritance: 223 | :synopsis: demo the 'pickup where you left off' idiom 224 | 225 | .. literalinclude:: ../py_trees/demos/pick_up_where_you_left_off.py 226 | :language: python 227 | :linenos: 228 | :caption: py_trees/demos/pick_up_where_you_left_off.py 229 | 230 | -------------------------------------------------------------------------------- /docs/dot/composites.dot: -------------------------------------------------------------------------------- 1 | digraph selector { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Sequence [fontcolor=black, shape=box, fontsize=11, style=filled, fillcolor=orange]; 6 | Selector [fontcolor=black, shape=octagon, fontsize=11, style=filled, fillcolor=cyan]; 7 | Parallel [fontcolor=black, shape=parallelogram, fontsize=11, style=filled, fillcolor=gold]; 8 | } 9 | -------------------------------------------------------------------------------- /docs/dot/decorators.dot: -------------------------------------------------------------------------------- 1 | digraph life { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Life [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 6 | Inverter [fillcolor=ghostwhite, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 7 | Life -> Inverter; 8 | "Busy?" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | Inverter -> "Busy?"; 10 | Timeout [fillcolor=ghostwhite, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | Life -> Timeout; 12 | "Have a Beer!" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 13 | Timeout -> "Have a Beer!"; 14 | } 15 | -------------------------------------------------------------------------------- /docs/dot/demo-blackboard.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | "Blackboard Demo" [label="Blackboard Demo", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 6 | "Set Nested" [label="Set Nested", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 7 | "Blackboard Demo" -> "Set Nested"; 8 | Writer [label=Writer, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 9 | "Blackboard Demo" -> Writer; 10 | "Check Nested Foo" [label="Check Nested Foo", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 11 | "Blackboard Demo" -> "Check Nested Foo"; 12 | ParamsAndState [label=ParamsAndState, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 13 | "Blackboard Demo" -> ParamsAndState; 14 | subgraph { 15 | label="children_of_Blackboard Demo"; 16 | rank=same; 17 | "Set Nested" [label="Set Nested", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 18 | Writer [label=Writer, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 19 | "Check Nested Foo" [label="Check Nested Foo", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 20 | ParamsAndState [label=ParamsAndState, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 21 | } 22 | 23 | Configuration [label=Configuration, shape=ellipse, style=filled, color=blue, fillcolor=gray, fontsize=7, fontcolor=blue]; 24 | "/dude" [label="/dude: Bob", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 25 | "/dude" -> Writer [color=blue, constraint=False]; 26 | Configuration -> "/dude" [color=blue, constraint=False]; 27 | "/parameters/default_speed" [label="/parameters/default_speed: 30.0", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 28 | "/parameters/default_speed" -> ParamsAndState [color=blue, constraint=False]; 29 | Configuration -> "/parameters/default_speed" [color=blue, constraint=False]; 30 | "/nested" [label="/nested: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 31 | "/nested" -> "Check Nested Foo" [color=blue, constraint=False]; 32 | "Set Nested" -> "/nested" [color=blue, constraint=True]; 33 | "/spaghetti" [label="/spaghetti: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 34 | Writer -> "/spaghetti" [color=blue, constraint=True]; 35 | "/state/current_speed" [label="/state/current_speed: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 36 | ParamsAndState -> "/state/current_speed" [color=blue, constraint=True]; 37 | } 38 | -------------------------------------------------------------------------------- /docs/dot/demo-context_switching.dot: -------------------------------------------------------------------------------- 1 | digraph parallel { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Parallel [fillcolor=gold, fontcolor=black, fontsize=11, shape=note, style=filled]; 6 | Context [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 7 | Parallel -> Context; 8 | Sequence [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 9 | Parallel -> Sequence; 10 | "Action 1" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | Sequence -> "Action 1"; 12 | "Action 2" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 13 | Sequence -> "Action 2"; 14 | } 15 | -------------------------------------------------------------------------------- /docs/dot/demo-dot-graphs.dot: -------------------------------------------------------------------------------- 1 | digraph demo_dot_graphs_fine_detail { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | "Demo Dot Graphs fine_detail" [fillcolor=cyan, fontcolor=black, fontsize=11, shape=octagon, style=filled]; 6 | "BlackBox 1" [fillcolor=gray20, fontcolor=white, fontsize=11, shape=box, style=filled]; 7 | "Demo Dot Graphs fine_detail" -> "BlackBox 1"; 8 | Worker [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | "BlackBox 1" -> Worker; 10 | "Worker*" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | "BlackBox 1" -> "Worker*"; 12 | "Worker**" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 13 | "BlackBox 1" -> "Worker**"; 14 | "Blackbox 3" [fillcolor=gray20, fontcolor=dodgerblue, fontsize=11, shape=box, style=filled]; 15 | "BlackBox 1" -> "Blackbox 3"; 16 | "Worker***" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 17 | "Blackbox 3" -> "Worker***"; 18 | "Worker****" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 19 | "Blackbox 3" -> "Worker****"; 20 | "Worker*****" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 21 | "Blackbox 3" -> "Worker*****"; 22 | "Blackbox 2" [fillcolor=gray20, fontcolor=lawngreen, fontsize=11, shape=box, style=filled]; 23 | "Demo Dot Graphs fine_detail" -> "Blackbox 2"; 24 | "Worker******" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 25 | "Blackbox 2" -> "Worker******"; 26 | "Worker*******" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 27 | "Blackbox 2" -> "Worker*******"; 28 | "Worker********" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 29 | "Blackbox 2" -> "Worker********"; 30 | } 31 | -------------------------------------------------------------------------------- /docs/dot/demo-either-or.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | ordering=out; 3 | graph [fontname="times-roman"]; 4 | node [fontname="times-roman"]; 5 | edge [fontname="times-roman"]; 6 | Root [label="Root\n--SuccessOnAll(-)--", shape=parallelogram, style=filled, fillcolor=gold, fontsize=9, fontcolor=black]; 7 | Reset [label=Reset, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 8 | Root -> Reset; 9 | "Joy1 - Disabled" [label="Joy1 - Disabled", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 10 | Reset -> "Joy1 - Disabled"; 11 | "Joy2 - Disabled" [label="Joy2 - Disabled", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 12 | Reset -> "Joy2 - Disabled"; 13 | "Joy1 Events" [label="Joy1 Events", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 14 | Root -> "Joy1 Events"; 15 | FisR [label=FisR, shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black]; 16 | "Joy1 Events" -> FisR; 17 | "Joystick 1" [label="Joystick 1", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 18 | FisR -> "Joystick 1"; 19 | "Joy1 - Enabled" [label="Joy1 - Enabled", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 20 | "Joy1 Events" -> "Joy1 - Enabled"; 21 | "Joy2 Events" [label="Joy2 Events", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 22 | Root -> "Joy2 Events"; 23 | "FisR*" [label="FisR*", shape=ellipse, style=filled, fillcolor=ghostwhite, fontsize=9, fontcolor=black]; 24 | "Joy2 Events" -> "FisR*"; 25 | "Joystick 2" [label="Joystick 2", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 26 | "FisR*" -> "Joystick 2"; 27 | "Joy2 - Enabled" [label="Joy2 - Enabled", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 28 | "Joy2 Events" -> "Joy2 - Enabled"; 29 | Tasks [label=Tasks, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black]; 30 | Root -> Tasks; 31 | EitherOr [label=EitherOr, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 32 | Tasks -> EitherOr; 33 | XOR [label=XOR, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 34 | EitherOr -> XOR; 35 | Chooser [label=Chooser, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black]; 36 | EitherOr -> Chooser; 37 | "Option 1" [label="Option 1", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 38 | Chooser -> "Option 1"; 39 | "Enabled?" [label="Enabled?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 40 | "Option 1" -> "Enabled?"; 41 | "Task 1" [label="Task 1", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 42 | "Option 1" -> "Task 1"; 43 | "Option 2" [label="Option 2", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 44 | Chooser -> "Option 2"; 45 | "Enabled?*" [label="Enabled?*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 46 | "Option 2" -> "Enabled?*"; 47 | "Task 2" [label="Task 2", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 48 | "Option 2" -> "Task 2"; 49 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 50 | Tasks -> Idle; 51 | } 52 | -------------------------------------------------------------------------------- /docs/dot/demo-eternal-guard.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | ordering=out; 3 | graph [fontname="times-roman"]; 4 | node [fontname="times-roman"]; 5 | edge [fontname="times-roman"]; 6 | "Eternal Guard" [fillcolor=orange, fontcolor=black, fontsize=9, label="Eternal Guard", shape=box, style=filled]; 7 | "Condition 1" [fillcolor=gray, fontcolor=black, fontsize=9, label="Condition 1", shape=ellipse, style=filled]; 8 | "Eternal Guard" -> "Condition 1"; 9 | "Condition 2" [fillcolor=gray, fontcolor=black, fontsize=9, label="Condition 2", shape=ellipse, style=filled]; 10 | "Eternal Guard" -> "Condition 2"; 11 | "Task Sequence" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Task Sequence", shape=box, style=filled]; 12 | "Eternal Guard" -> "Task Sequence"; 13 | "Worker 1" [fillcolor=gray, fontcolor=black, fontsize=9, label="Worker 1", shape=ellipse, style=filled]; 14 | "Task Sequence" -> "Worker 1"; 15 | "Worker 2" [fillcolor=gray, fontcolor=black, fontsize=9, label="Worker 2", shape=ellipse, style=filled]; 16 | "Task Sequence" -> "Worker 2"; 17 | } 18 | -------------------------------------------------------------------------------- /docs/dot/demo-logging.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Logging [label=Logging, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black]; 6 | EveryN [label=EveryN, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 7 | Logging -> EveryN; 8 | Sequence [label=Sequence, shape=box, style=filled, fillcolor=gray20, fontsize=9, fontcolor=lawngreen]; 9 | Logging -> Sequence; 10 | Guard [label=Guard, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 11 | Sequence -> Guard; 12 | Periodic [label=Periodic, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 13 | Sequence -> Periodic; 14 | Finisher [label=Finisher, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 15 | Sequence -> Finisher; 16 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 17 | Logging -> Idle; 18 | } 19 | -------------------------------------------------------------------------------- /docs/dot/demo-selector.dot: -------------------------------------------------------------------------------- 1 | digraph selector { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Selector [fillcolor=cyan, fontcolor=black, fontsize=11, shape=octagon, style=filled]; 6 | "After Two" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 7 | Selector -> "After Two"; 8 | Running [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | Selector -> Running; 10 | } 11 | -------------------------------------------------------------------------------- /docs/dot/demo-sequence.dot: -------------------------------------------------------------------------------- 1 | digraph sequence { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Sequence [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 6 | "Job 1" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 7 | Sequence -> "Job 1"; 8 | "Job 2" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | Sequence -> "Job 2"; 10 | "Job 3" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | Sequence -> "Job 3"; 12 | } 13 | -------------------------------------------------------------------------------- /docs/dot/demo-tree-stewardship.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | "Demo Tree" [label="Demo Tree", shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black]; 6 | EveryN [label=EveryN, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 7 | "Demo Tree" -> EveryN; 8 | Sequence [label=Sequence, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 9 | "Demo Tree" -> Sequence; 10 | Guard [label=Guard, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 11 | Sequence -> Guard; 12 | Periodic [label=Periodic, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 13 | Sequence -> Periodic; 14 | Finisher [label=Finisher, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 15 | Sequence -> Finisher; 16 | subgraph { 17 | label=children_of_Sequence; 18 | rank=same; 19 | Guard [label=Guard, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 20 | Periodic [label=Periodic, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 21 | Finisher [label=Finisher, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 22 | } 23 | 24 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 25 | "Demo Tree" -> Idle; 26 | subgraph { 27 | label="children_of_Demo Tree"; 28 | rank=same; 29 | EveryN [label=EveryN, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 30 | Sequence [label=Sequence, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 31 | Idle [label=Idle, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 32 | } 33 | 34 | count [label="count: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 35 | count -> Finisher [color=blue, constraint=False]; 36 | EveryN -> count [color=blue, constraint=True]; 37 | period [label="period: -", shape=box, style=filled, color=blue, fillcolor=white, fontsize=8, fontcolor=blue, width=0, height=0, fixedsize=False]; 38 | period -> Finisher [color=blue, constraint=False]; 39 | Periodic -> period [color=blue, constraint=True]; 40 | } 41 | -------------------------------------------------------------------------------- /docs/dot/demo_tree.dot: -------------------------------------------------------------------------------- 1 | digraph root { 2 | Root [shape=house, fontsize=11, style=filled, fillcolor=cyan]; 3 | EveryN [shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 4 | Root -> EveryN; 5 | Sequence [shape=box, fontsize=11, style=filled, fillcolor=orange]; 6 | Root -> Sequence; 7 | Guard [shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 8 | Sequence -> Guard; 9 | Periodic [shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 10 | Sequence -> Periodic; 11 | Finisher [shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 12 | Sequence -> Finisher; 13 | Idle [shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 14 | Root -> Idle; 15 | } 16 | -------------------------------------------------------------------------------- /docs/dot/idiom-either-or.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | ordering=out; 3 | graph [fontname="times-roman"]; 4 | node [fontname="times-roman"]; 5 | edge [fontname="times-roman"]; 6 | EitherOr [label=EitherOr, shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 7 | XOR [label=XOR, shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 8 | EitherOr -> XOR; 9 | Chooser [label=Chooser, shape=octagon, style=filled, fillcolor=cyan, fontsize=9, fontcolor=black]; 10 | EitherOr -> Chooser; 11 | "Option 1" [label="Option 1", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 12 | Chooser -> "Option 1"; 13 | "Enabled?" [label="Enabled?", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 14 | "Option 1" -> "Enabled?"; 15 | "Subtree 1" [label="Subtree 1", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 16 | "Option 1" -> "Subtree 1"; 17 | "Option 2" [label="Option 2", shape=box, style=filled, fillcolor=orange, fontsize=9, fontcolor=black]; 18 | Chooser -> "Option 2"; 19 | "Enabled?*" [label="Enabled?*", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 20 | "Option 2" -> "Enabled?*"; 21 | "Subtree 2" [label="Subtree 2", shape=ellipse, style=filled, fillcolor=gray, fontsize=9, fontcolor=black]; 22 | "Option 2" -> "Subtree 2"; 23 | } 24 | -------------------------------------------------------------------------------- /docs/dot/naive_context_switching.dot: -------------------------------------------------------------------------------- 1 | digraph naive_context_switching { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | "Naive Context Switching" [fontcolor=black, shape=box, fontsize=11, style=filled, fillcolor=orange]; 6 | "Switch Navi Context" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 7 | "Naive Context Switching" -> "Switch Navi Context"; 8 | "Move It" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 9 | "Naive Context Switching" -> "Move It"; 10 | "Restore Navi Context" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 11 | "Naive Context Switching" -> "Restore Navi Context"; 12 | } 13 | -------------------------------------------------------------------------------- /docs/dot/oneshot.dot: -------------------------------------------------------------------------------- 1 | digraph oneshot { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | OneShot [fillcolor=cyan, fontcolor=black, fontsize=11, shape=octagon, style=filled]; 6 | "Oneshot w/ Guard" [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 7 | OneShot -> "Oneshot w/ Guard"; 8 | "Not Completed?" [fillcolor=ghostwhite, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | "Oneshot w/ Guard" -> "Not Completed?"; 10 | "Completed?" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | "Not Completed?" -> "Completed?"; 12 | Sequence [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 13 | "Oneshot w/ Guard" -> Sequence; 14 | Guard [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 15 | Sequence -> Guard; 16 | "Action 1" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 17 | Sequence -> "Action 1"; 18 | "Action 2" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 19 | Sequence -> "Action 2"; 20 | "Action 3" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 21 | Sequence -> "Action 3"; 22 | "Mark Done\n[SUCCESS]" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 23 | Sequence -> "Mark Done\n[SUCCESS]"; 24 | "Oneshot Result" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 25 | OneShot -> "Oneshot Result"; 26 | } 27 | -------------------------------------------------------------------------------- /docs/dot/parallel.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | graph [fontname="times-roman", splines=curved]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Parallel [fillcolor=gold, fontcolor=black, fontsize=9, label="Parallel\n--SuccessOnSelected(⚡,[B1,B2])--", shape=parallelogram, style=filled]; 6 | B1 [fillcolor=gray, fontcolor=black, fontsize=9, label=B1, shape=ellipse, style=filled]; 7 | Parallel -> B1; 8 | B2 [fillcolor=gray, fontcolor=black, fontsize=9, label=B2, shape=ellipse, style=filled]; 9 | Parallel -> B2; 10 | B3 [fillcolor=gray, fontcolor=black, fontsize=9, label=B3, shape=ellipse, style=filled]; 11 | Parallel -> B3; 12 | } 13 | -------------------------------------------------------------------------------- /docs/dot/pick_up_where_you_left_off.dot: -------------------------------------------------------------------------------- 1 | digraph pick_up_where_you_left_off { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | "Pick Up Where You Left Off" [shape=octagon, style=filled, fillcolor=cyan, fontsize=11, fontcolor=black]; 6 | "High Priority" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 7 | "Pick Up Where You Left Off" -> "High Priority"; 8 | Tasks [shape=box, style=filled, fillcolor=orange, fontsize=11, fontcolor=black]; 9 | "Pick Up Where You Left Off" -> Tasks; 10 | "Do or Don't" [shape=octagon, style=filled, fillcolor=cyan, fontsize=11, fontcolor=black]; 11 | Tasks -> "Do or Don't"; 12 | "Done?" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 13 | "Do or Don't" -> "Done?"; 14 | Worker [shape=box, style=filled, fillcolor=orange, fontsize=11, fontcolor=black]; 15 | "Do or Don't" -> Worker; 16 | "Task 1" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 17 | Worker -> "Task 1"; 18 | "Mark\ntask_1_done" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 19 | Worker -> "Mark\ntask_1_done"; 20 | "Do or Don't*" [shape=octagon, style=filled, fillcolor=cyan, fontsize=11, fontcolor=black]; 21 | Tasks -> "Do or Don't*"; 22 | "Done?*" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 23 | "Do or Don't*" -> "Done?*"; 24 | "Worker*" [shape=box, style=filled, fillcolor=orange, fontsize=11, fontcolor=black]; 25 | "Do or Don't*" -> "Worker*"; 26 | "Task 2" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 27 | "Worker*" -> "Task 2"; 28 | "Mark\ntask_2_done" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 29 | "Worker*" -> "Mark\ntask_2_done"; 30 | "Clear\ntask_1_done" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 31 | Tasks -> "Clear\ntask_1_done"; 32 | "Clear\ntask_2_done" [shape=ellipse, style=filled, fillcolor=gray, fontsize=11, fontcolor=black]; 33 | Tasks -> "Clear\ntask_2_done"; 34 | } 35 | -------------------------------------------------------------------------------- /docs/dot/selector.dot: -------------------------------------------------------------------------------- 1 | digraph selector { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Selector [fontcolor=black, shape=octagon, fontsize=11, style=filled, fillcolor=cyan]; 6 | "High Priority" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 7 | Selector -> "High Priority"; 8 | "Med Priority" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 9 | Selector -> "Med Priority"; 10 | "Low Priority" [fontcolor=black, shape=ellipse, fontsize=11, style=filled, fillcolor=gray]; 11 | Selector -> "Low Priority"; 12 | } 13 | -------------------------------------------------------------------------------- /docs/dot/selector_with_memory.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | ordering=out; 3 | graph [fontname="times-roman"]; 4 | node [fontname="times-roman"]; 5 | edge [fontname="times-roman"]; 6 | "Selector With Memory" [fillcolor=cyan, fontcolor=black, fontsize=9, label="Ⓜ Selector With Memory", shape=octagon, style=filled]; 7 | "High Priority" [fillcolor=gray, fontcolor=black, fontsize=9, label="High Priority", shape=ellipse, style=filled]; 8 | "Selector With Memory" -> "High Priority"; 9 | "Med Priority" [fillcolor=gray, fontcolor=black, fontsize=9, label="Med Priority", shape=ellipse, style=filled]; 10 | "Selector With Memory" -> "Med Priority"; 11 | "Low Priority" [fillcolor=gray, fontcolor=black, fontsize=9, label="Low Priority", shape=ellipse, style=filled]; 12 | "Selector With Memory" -> "Low Priority"; 13 | } 14 | -------------------------------------------------------------------------------- /docs/dot/sequence.dot: -------------------------------------------------------------------------------- 1 | digraph sequence { 2 | graph [fontname="times-roman"]; 3 | node [fontname="times-roman"]; 4 | edge [fontname="times-roman"]; 5 | Sequence [fillcolor=orange, fontcolor=black, fontsize=11, shape=box, style=filled]; 6 | Guard [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 7 | Sequence -> Guard; 8 | "Action 1" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 9 | Sequence -> "Action 1"; 10 | "Action 2" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 11 | Sequence -> "Action 2"; 12 | "Action 3" [fillcolor=gray, fontcolor=black, fontsize=11, shape=ellipse, style=filled]; 13 | Sequence -> "Action 3"; 14 | } 15 | -------------------------------------------------------------------------------- /docs/dot/sequence_with_memory.dot: -------------------------------------------------------------------------------- 1 | digraph pastafarianism { 2 | ordering=out; 3 | graph [fontname="times-roman"]; 4 | node [fontname="times-roman"]; 5 | edge [fontname="times-roman"]; 6 | "Sequence with Memory" [fillcolor=orange, fontcolor=black, fontsize=9, label="Ⓜ Sequence with Memory", shape=box, style=filled]; 7 | Guard [fillcolor=gray, fontcolor=black, fontsize=9, label=Guard, shape=ellipse, style=filled]; 8 | "Sequence with Memory" -> Guard; 9 | "Action 1" [fillcolor=gray, fontcolor=black, fontsize=9, label="Action 1", shape=ellipse, style=filled]; 10 | "Sequence with Memory" -> "Action 1"; 11 | "Action 2" [fillcolor=gray, fontcolor=black, fontsize=9, label="Action 2", shape=ellipse, style=filled]; 12 | "Sequence with Memory" -> "Action 2"; 13 | "Action 3" [fillcolor=gray, fontcolor=black, fontsize=9, label="Action 3", shape=ellipse, style=filled]; 14 | "Sequence with Memory" -> "Action 3"; 15 | } 16 | -------------------------------------------------------------------------------- /docs/examples/blackboard_activity_stream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example showing how to display the blackboard activity stream.""" 3 | 4 | import py_trees 5 | 6 | py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100) 7 | reader = py_trees.blackboard.Client(name="Reader") 8 | reader.register_key(key="foo", access=py_trees.common.Access.READ) 9 | writer = py_trees.blackboard.Client(name="Writer") 10 | writer.register_key(key="foo", access=py_trees.common.Access.WRITE) 11 | writer.foo = "bar" 12 | writer.foo = "foobar" 13 | unused_result = reader.foo 14 | print(py_trees.display.unicode_blackboard_activity_stream()) 15 | assert py_trees.blackboard.Blackboard.activity_stream is not None 16 | py_trees.blackboard.Blackboard.activity_stream.clear() 17 | -------------------------------------------------------------------------------- /docs/examples/blackboard_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example showing how to access the blackboard within a behaviour.""" 3 | 4 | import py_trees 5 | 6 | 7 | class Foo(py_trees.behaviour.Behaviour): 8 | """Example behaviour that reads from and writes to the blackboard.""" 9 | 10 | def __init__(self, name: str) -> None: 11 | """Construct a new behaviour instance and sets up blackboard access.""" 12 | super().__init__(name=name) 13 | self.blackboard = self.attach_blackboard_client(name="Foo Global") 14 | self.parameters = self.attach_blackboard_client( 15 | name="Foo Params", namespace="foo_parameters_" 16 | ) 17 | self.state = self.attach_blackboard_client( 18 | name="Foo State", namespace="foo_state_" 19 | ) 20 | 21 | # create a key 'foo_parameters_init' on the blackboard 22 | self.parameters.register_key("init", access=py_trees.common.Access.READ) 23 | # create a key 'foo_state_number_of_noodles' on the blackboard 24 | self.state.register_key( 25 | "number_of_noodles", access=py_trees.common.Access.WRITE 26 | ) 27 | 28 | def initialise(self) -> None: 29 | """Initialise blackboard variables based on parameters.""" 30 | self.state.number_of_noodles = self.parameters.init 31 | 32 | def update(self) -> py_trees.common.Status: 33 | """Update blackboard variables as the behaviour ticks.""" 34 | self.state.number_of_noodles += 1 35 | self.feedback_message = self.state.number_of_noodles 36 | if self.state.number_of_noodles > 5: 37 | return py_trees.common.Status.SUCCESS 38 | else: 39 | return py_trees.common.Status.RUNNING 40 | 41 | 42 | # could equivalently do directly via the Blackboard static methods if 43 | # not interested in tracking / visualising the application configuration 44 | # Register and set up the configuration on the blackboard 45 | configuration = py_trees.blackboard.Client( 46 | name="App Config", namespace="foo_parameters_" 47 | ) # Added namespace 48 | configuration.register_key( 49 | "init", access=py_trees.common.Access.WRITE 50 | ) # Register key with correct namespace 51 | configuration.init = 3 # Set the initial value for 'foo_parameters_init' 52 | 53 | foo = Foo(name="The Foo") 54 | for i in range(1, 8): 55 | foo.tick_once() 56 | print("Number of Noodles: {}".format(foo.feedback_message)) 57 | -------------------------------------------------------------------------------- /docs/examples/blackboard_disconnected.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example showing disconnected blackboards.""" 3 | 4 | import py_trees 5 | 6 | 7 | def check_foo() -> None: 8 | """Read the value of a blackboard variable in a different scope.""" 9 | blackboard = py_trees.blackboard.Client(name="Reader") 10 | blackboard.register_key(key="foo", access=py_trees.common.Access.READ) 11 | print(f"Foo: {blackboard.foo}") 12 | 13 | 14 | blackboard = py_trees.blackboard.Client(name="Writer") 15 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 16 | blackboard.foo = "bar" 17 | check_foo() 18 | -------------------------------------------------------------------------------- /docs/examples/blackboard_display.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example showing how to display a blackboard.""" 3 | 4 | import py_trees 5 | 6 | writer = py_trees.blackboard.Client(name="Writer") 7 | for key in {"foo", "bar", "dude", "dudette"}: 8 | writer.register_key(key=key, access=py_trees.common.Access.WRITE) 9 | 10 | reader = py_trees.blackboard.Client(name="Reader") 11 | for key in {"foo", "bar"}: 12 | reader.register_key(key=key, access=py_trees.common.Access.READ) 13 | 14 | writer.foo = "foo" 15 | writer.bar = "bar" 16 | writer.dude = "bob" 17 | 18 | # all key-value pairs 19 | print(py_trees.display.unicode_blackboard()) 20 | # various filtered views 21 | print(py_trees.display.unicode_blackboard(key_filter={"foo"})) 22 | print(py_trees.display.unicode_blackboard(regex_filter="dud*")) 23 | print(py_trees.display.unicode_blackboard(client_filter={reader.unique_identifier})) 24 | # list the clients associated with each key 25 | print(py_trees.display.unicode_blackboard(display_only_key_metadata=True)) 26 | -------------------------------------------------------------------------------- /docs/examples/blackboard_namespaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example demonstrating the use of namespaces in a blackboard.""" 3 | 4 | import py_trees 5 | 6 | blackboard = py_trees.blackboard.Client(name="Global") 7 | parameters = py_trees.blackboard.Client(name="Parameters", namespace="parameters") 8 | 9 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 10 | blackboard.register_key(key="/bar", access=py_trees.common.Access.WRITE) 11 | blackboard.register_key( 12 | key="/parameters/default_speed", access=py_trees.common.Access.WRITE 13 | ) 14 | parameters.register_key(key="aggressive_speed", access=py_trees.common.Access.WRITE) 15 | 16 | blackboard.foo = "foo" 17 | blackboard.bar = "bar" 18 | blackboard.parameters.default_speed = 20.0 19 | parameters.aggressive_speed = 60.0 20 | 21 | miss_daisy = blackboard.parameters.default_speed 22 | van_diesel = parameters.aggressive_speed 23 | 24 | print(blackboard) 25 | print(parameters) 26 | -------------------------------------------------------------------------------- /docs/examples/blackboard_nested.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example showing how to use objects to create nested blackboard keys.""" 3 | 4 | import py_trees 5 | 6 | 7 | class Nested(object): 8 | """Simple object that contains a few attributes.""" 9 | 10 | def __init__(self) -> None: 11 | self.foo: str | None = None 12 | self.bar: str | None = None 13 | 14 | def __str__(self) -> str: 15 | return str(self.__dict__) 16 | 17 | 18 | writer = py_trees.blackboard.Client(name="Writer") 19 | writer.register_key(key="nested", access=py_trees.common.Access.WRITE) 20 | reader = py_trees.blackboard.Client(name="Reader") 21 | reader.register_key(key="nested", access=py_trees.common.Access.READ) 22 | 23 | writer.nested = Nested() 24 | writer.nested.foo = "I am foo" 25 | writer.nested.bar = "I am bar" 26 | 27 | foo = reader.nested.foo 28 | print(writer) 29 | print(reader) 30 | -------------------------------------------------------------------------------- /docs/examples/blackboard_read_write.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Example demonstrating basic read/write operations on the blackboard.""" 3 | 4 | import py_trees 5 | 6 | 7 | blackboard = py_trees.blackboard.Client(name="Client") 8 | blackboard.register_key(key="foo", access=py_trees.common.Access.WRITE) 9 | blackboard.register_key(key="bar", access=py_trees.common.Access.READ) 10 | blackboard.foo = "foo" 11 | print(blackboard) 12 | -------------------------------------------------------------------------------- /docs/examples/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of some simple decorator nodes.""" 4 | 5 | import py_trees.decorators 6 | import py_trees.display 7 | 8 | if __name__ == "__main__": 9 | root = py_trees.composites.Sequence(name="Life", memory=False) 10 | timeout = py_trees.decorators.Timeout( 11 | name="Timeout", child=py_trees.behaviours.Success(name="Have a Beer!") 12 | ) 13 | failure_is_success = py_trees.decorators.Inverter( 14 | name="Inverter", child=py_trees.behaviours.Success(name="Busy?") 15 | ) 16 | root.add_children([failure_is_success, timeout]) 17 | py_trees.display.render_dot_tree(root) 18 | -------------------------------------------------------------------------------- /docs/examples/oneshot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of one-shot decorator nodes.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | sequence = py_trees.composites.Sequence("Sequence", memory=False) 9 | guard = py_trees.behaviours.Success(name="Guard") 10 | a1 = py_trees.behaviours.Success(name="Action 1") 11 | a2 = py_trees.behaviours.Success(name="Action 2") 12 | a3 = py_trees.behaviours.Success(name="Action 3") 13 | sequence.add_children([guard, a1, a2, a3]) 14 | root = py_trees.idioms.oneshot( 15 | name="OneShot", 16 | variable_name="oneshot", 17 | behaviour=sequence, 18 | policy=py_trees.common.OneShotPolicy.ON_COMPLETION, 19 | ) 20 | py_trees.display.render_dot_tree( 21 | root, py_trees.common.string_to_visibility_level("all") 22 | ) 23 | -------------------------------------------------------------------------------- /docs/examples/parallel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of a parallel control node.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | b1 = py_trees.behaviours.Success(name="B1") 9 | b2 = py_trees.behaviours.Success(name="B2") 10 | b3 = py_trees.behaviours.Success(name="B3") 11 | root = py_trees.composites.Parallel( 12 | name="Parallel", 13 | policy=py_trees.common.ParallelPolicy.SuccessOnSelected( 14 | synchronise=True, children=[b1, b2] 15 | ), 16 | ) 17 | root.add_children([b1, b2, b3]) 18 | py_trees.display.render_dot_tree( 19 | root, py_trees.common.string_to_visibility_level("all") 20 | ) 21 | -------------------------------------------------------------------------------- /docs/examples/pickup_where_you_left_off.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of the "pick up where you left off" idiom.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | task_one = py_trees.behaviours.StatusQueue( 9 | name="Task 1", 10 | queue=[ 11 | py_trees.common.Status.RUNNING, 12 | py_trees.common.Status.RUNNING, 13 | ], 14 | eventually=py_trees.common.Status.SUCCESS, 15 | ) 16 | task_two = py_trees.behaviours.StatusQueue( 17 | name="Task 2", 18 | queue=[ 19 | py_trees.common.Status.RUNNING, 20 | py_trees.common.Status.RUNNING, 21 | ], 22 | eventually=py_trees.common.Status.SUCCESS, 23 | ) 24 | high_priority_interrupt = py_trees.decorators.RunningIsFailure( 25 | name="High Priority Interrupt", 26 | child=py_trees.behaviours.Periodic(name="High Priority", n=3), 27 | ) 28 | piwylo = py_trees.idioms.pick_up_where_you_left_off( 29 | name="Tasks", tasks=[task_one, task_two] 30 | ) 31 | root = py_trees.composites.Selector( 32 | name="Pick Up\nWhere You\nLeft Off", memory=False 33 | ) 34 | root.add_children([high_priority_interrupt, piwylo]) 35 | py_trees.display.render_dot_tree( 36 | root, py_trees.common.string_to_visibility_level("all") 37 | ) 38 | -------------------------------------------------------------------------------- /docs/examples/selector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of a selector control node (without memory).""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | root = py_trees.composites.Selector("Selector", memory=False) 9 | high = py_trees.behaviours.Success(name="High Priority") 10 | med = py_trees.behaviours.Success(name="Med Priority") 11 | low = py_trees.behaviours.Success(name="Low Priority") 12 | root.add_children([high, med, low]) 13 | py_trees.display.render_dot_tree( 14 | root, py_trees.common.string_to_visibility_level("all") 15 | ) 16 | -------------------------------------------------------------------------------- /docs/examples/selector_with_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of a selector control node with memory.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | root = py_trees.composites.Selector("Selector With Memory", memory=True) 9 | high = py_trees.behaviours.Success(name="High Priority") 10 | med = py_trees.behaviours.Success(name="Med Priority") 11 | low = py_trees.behaviours.Success(name="Low Priority") 12 | root.add_children([high, med, low]) 13 | py_trees.display.render_dot_tree( 14 | root, py_trees.common.string_to_visibility_level("all") 15 | ) 16 | -------------------------------------------------------------------------------- /docs/examples/sequence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of a sequence control node.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | root = py_trees.composites.Sequence("Sequence", memory=False) 9 | guard = py_trees.behaviours.Success(name="Guard") 10 | a1 = py_trees.behaviours.Success(name="Action 1") 11 | a2 = py_trees.behaviours.Success(name="Action 2") 12 | a3 = py_trees.behaviours.Success(name="Action 3") 13 | root.add_children([guard, a1, a2, a3]) 14 | py_trees.display.render_dot_tree( 15 | root, py_trees.common.string_to_visibility_level("all") 16 | ) 17 | -------------------------------------------------------------------------------- /docs/examples/sequence_with_memory.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example demonstrating the use of a sequence control node with memory.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | root = py_trees.composites.Sequence(name="Sequence with Memory", memory=True) 9 | guard = py_trees.behaviours.Success(name="Guard") 10 | a1 = py_trees.behaviours.Success(name="Action 1") 11 | a2 = py_trees.behaviours.Success(name="Action 2") 12 | a3 = py_trees.behaviours.Success(name="Action 3") 13 | root.add_children([guard, a1, a2, a3]) 14 | py_trees.display.render_dot_tree( 15 | root, py_trees.common.string_to_visibility_level("all") 16 | ) 17 | -------------------------------------------------------------------------------- /docs/examples/skeleton_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example showing how to create a skeleton behaviour.""" 4 | 5 | import random 6 | import typing 7 | 8 | import py_trees 9 | 10 | 11 | class Foo(py_trees.behaviour.Behaviour): 12 | """A skeleton behaviour that inherits from the PyTrees Behaviour class.""" 13 | 14 | def __init__(self, name: str) -> None: 15 | """ 16 | Minimal one-time initialisation. 17 | 18 | A good rule of thumb is to only include the initialisation relevant 19 | for being able to insert this behaviour in a tree for offline rendering to dot graphs. 20 | 21 | Other one-time initialisation requirements should be met via 22 | the setup() method. 23 | """ 24 | super(Foo, self).__init__(name) 25 | 26 | def setup(self, **kwargs: typing.Any) -> None: 27 | """ 28 | Minimal setup implementation. 29 | 30 | When is this called? 31 | This function should be either manually called by your program 32 | to setup this behaviour alone, or more commonly, via 33 | :meth:`~py_trees.behaviour.Behaviour.setup_with_descendants` 34 | or :meth:`~py_trees.trees.BehaviourTree.setup`, both of which 35 | will iterate over this behaviour, it's children (it's children's 36 | children ...) calling :meth:`~py_trees.behaviour.Behaviour.setup` 37 | on each in turn. 38 | 39 | If you have vital initialisation necessary to the success 40 | execution of your behaviour, put a guard in your 41 | :meth:`~py_trees.behaviour.Behaviour.initialise` method 42 | to protect against entry without having been setup. 43 | 44 | What to do here? 45 | Delayed one-time initialisation that would otherwise interfere 46 | with offline rendering of this behaviour in a tree to dot graph 47 | or validation of the behaviour's configuration. 48 | 49 | Good examples include: 50 | 51 | - Hardware or driver initialisation 52 | - Middleware initialisation (e.g. ROS pubs/subs/services) 53 | - A parallel checking for a valid policy configuration after 54 | children have been added or removed 55 | """ 56 | self.logger.debug(" %s [Foo::setup()]" % self.name) 57 | 58 | def initialise(self) -> None: 59 | """ 60 | Minimal initialisation implementation. 61 | 62 | When is this called? 63 | The first time your behaviour is ticked and anytime the 64 | status is not RUNNING thereafter. 65 | 66 | What to do here? 67 | Any initialisation you need before putting your behaviour 68 | to work. 69 | """ 70 | self.logger.debug(" %s [Foo::initialise()]" % self.name) 71 | 72 | def update(self) -> py_trees.common.Status: 73 | """ 74 | Minimal update implementation. 75 | 76 | When is this called? 77 | Every time your behaviour is ticked. 78 | 79 | What to do here? 80 | - Triggering, checking, monitoring. Anything...but do not block! 81 | - Set a feedback message 82 | - return a py_trees.common.Status.[RUNNING, SUCCESS, FAILURE] 83 | """ 84 | self.logger.debug(" %s [Foo::update()]" % self.name) 85 | ready_to_make_a_decision = random.choice([True, False]) 86 | decision = random.choice([True, False]) 87 | if not ready_to_make_a_decision: 88 | return py_trees.common.Status.RUNNING 89 | elif decision: 90 | self.feedback_message = "We are not bar!" 91 | return py_trees.common.Status.SUCCESS 92 | else: 93 | self.feedback_message = "Uh oh" 94 | return py_trees.common.Status.FAILURE 95 | 96 | def terminate(self, new_status: py_trees.common.Status) -> None: 97 | """ 98 | Minimal termination implementation. 99 | 100 | When is this called? 101 | Whenever your behaviour switches to a non-running state. 102 | - SUCCESS || FAILURE : your behaviour's work cycle has finished 103 | - INVALID : a higher priority branch has interrupted, or shutting down 104 | """ 105 | self.logger.debug( 106 | " %s [Foo::terminate().terminate()][%s->%s]" 107 | % (self.name, self.status, new_status) 108 | ) 109 | -------------------------------------------------------------------------------- /docs/examples/skeleton_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """Example showing how to create and tick a simple behaviour tree.""" 4 | 5 | import py_trees 6 | 7 | if __name__ == "__main__": 8 | root = py_trees.composites.Selector("Selector", memory=False) 9 | high = py_trees.behaviours.Success(name="High Priority") 10 | med = py_trees.behaviours.Success(name="Med Priority") 11 | low = py_trees.behaviours.Success(name="Low Priority") 12 | root.add_children([high, med, low]) 13 | 14 | behaviour_tree = py_trees.trees.BehaviourTree(root=root) 15 | print(py_trees.display.unicode_tree(root=root)) 16 | behaviour_tree.setup(timeout=15) 17 | 18 | def print_tree(tree: py_trees.trees.BehaviourTree) -> None: 19 | """Print the behaviour tree and its current status.""" 20 | print(py_trees.display.unicode_tree(root=tree.root, show_status=True)) 21 | 22 | try: 23 | behaviour_tree.tick_tock( 24 | period_ms=500, 25 | number_of_iterations=py_trees.trees.CONTINUOUS_TICK_TOCK, 26 | pre_tick_handler=None, 27 | post_tick_handler=print_tree, 28 | ) 29 | except KeyboardInterrupt: 30 | behaviour_tree.interrupt() 31 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq-section-label: 2 | 3 | FAQ 4 | === 5 | 6 | .. tip:: For hints and guidelines, you might also like to browse :ref:`crazy-hospital-section`. 7 | 8 | **Will there be a c++ implementation?** 9 | 10 | Certainly feasible and if there's a need. If such a things should come to pass though, the 11 | c++ implementation should compliment this one. That is, it should focus on decision making 12 | for systems with low latency and reactive requirements. It would use triggers to tick 13 | the tree instead of tick-tock and a few other tricks that have evolved in the gaming 14 | industry over the last few years. Having a c++ implementation for use in the control 15 | layer of a robotics system would be a driving use case. 16 | 17 | -------------------------------------------------------------------------------- /docs/idioms.rst: -------------------------------------------------------------------------------- 1 | .. _idioms-section: 2 | 3 | Idioms 4 | ====== 5 | 6 | .. automodule:: py_trees.idioms 7 | :noindex: 8 | 9 | Common decision making patterns can often be realised using a specific 10 | combination of fundamental behaviours and the blackboard. Even if this 11 | somewhat verbosely populates the tree, this is preferable to creating 12 | new composites types or overriding existing composites since this will 13 | increase tree logic complexity and/or bury details under the hood (both 14 | of which add an exponential cost to introspection/visualisation). 15 | 16 | In this package these patterns will be referred to as **PyTree Idioms** 17 | and in this module you will find convenience functions that assist in 18 | creating them. 19 | 20 | The subsections below introduce each composite briefly. For a full listing of each 21 | composite's methods, visit the :ref:`py-trees-idioms-module` module api documentation. 22 | 23 | .. _either-or-section: 24 | 25 | Either Or 26 | --------- 27 | 28 | .. automethod:: py_trees.idioms.either_or 29 | :noindex: 30 | 31 | .. _oneshot-section: 32 | 33 | Oneshot 34 | ------- 35 | 36 | .. automethod:: py_trees.idioms.oneshot 37 | :noindex: 38 | 39 | .. _pick-up-where-you-left-off-section: 40 | 41 | Pickup Where You left Off 42 | ------------------------- 43 | 44 | .. automethod:: py_trees.idioms.pick_up_where_you_left_off 45 | :noindex: 46 | -------------------------------------------------------------------------------- /docs/images/action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/action.gif -------------------------------------------------------------------------------- /docs/images/ascii_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/ascii_tree.png -------------------------------------------------------------------------------- /docs/images/ascii_tree_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/ascii_tree_simple.png -------------------------------------------------------------------------------- /docs/images/blackboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard.jpg -------------------------------------------------------------------------------- /docs/images/blackboard_activity_stream.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_activity_stream.png -------------------------------------------------------------------------------- /docs/images/blackboard_client_instantiation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_client_instantiation.png -------------------------------------------------------------------------------- /docs/images/blackboard_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_demo.png -------------------------------------------------------------------------------- /docs/images/blackboard_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_display.png -------------------------------------------------------------------------------- /docs/images/blackboard_namespaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_namespaces.png -------------------------------------------------------------------------------- /docs/images/blackboard_nested.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_nested.png -------------------------------------------------------------------------------- /docs/images/blackboard_read_write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_read_write.png -------------------------------------------------------------------------------- /docs/images/blackboard_remappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_remappings.png -------------------------------------------------------------------------------- /docs/images/blackboard_trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/blackboard_trees.png -------------------------------------------------------------------------------- /docs/images/context_switching.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/context_switching.gif -------------------------------------------------------------------------------- /docs/images/crazy_hospital.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/crazy_hospital.jpg -------------------------------------------------------------------------------- /docs/images/display_modes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/display_modes.png -------------------------------------------------------------------------------- /docs/images/either_or.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/either_or.gif -------------------------------------------------------------------------------- /docs/images/eternal_guard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/eternal_guard.gif -------------------------------------------------------------------------------- /docs/images/lifecycle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/lifecycle.gif -------------------------------------------------------------------------------- /docs/images/logging.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/logging.gif -------------------------------------------------------------------------------- /docs/images/many-hats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/many-hats.png -------------------------------------------------------------------------------- /docs/images/pick_up_where_you_left_off.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/pick_up_where_you_left_off.gif -------------------------------------------------------------------------------- /docs/images/render.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/render.gif -------------------------------------------------------------------------------- /docs/images/selector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/selector.gif -------------------------------------------------------------------------------- /docs/images/sequence.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/sequence.gif -------------------------------------------------------------------------------- /docs/images/ticking_tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/ticking_tree.jpg -------------------------------------------------------------------------------- /docs/images/tree_stewardship.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/tree_stewardship.gif -------------------------------------------------------------------------------- /docs/images/yggdrasil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/docs/images/yggdrasil.jpg -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. py_trees documentation master file, created by 2 | sphinx-quickstart on Thu Jul 30 16:43:58 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. _index-section: 7 | 8 | Py Trees 9 | ======== 10 | 11 | .. toctree:: 12 | :maxdepth: 1 13 | :caption: Guide 14 | 15 | introduction 16 | behaviours 17 | composites 18 | decorators 19 | blackboards 20 | idioms 21 | trees 22 | visualisation 23 | the_crazy_hospital 24 | terminology 25 | faq 26 | 27 | .. toctree:: 28 | :maxdepth: 1 29 | :caption: Reference 30 | 31 | demos 32 | programs 33 | modules 34 | changelog 35 | 36 | Indices and tables 37 | ================== 38 | 39 | * :ref:`genindex` 40 | * :ref:`modindex` 41 | * :ref:`search` 42 | 43 | .. _`operator module`: https://docs.python.org/3/library/operator.html 44 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Quick Start 5 | ----------- 6 | 7 | If you'd like to fast forward to some action, browse the :ref:`demos-section-label` or 8 | read through the `ROS2 Robotics Tutorials`_ which incrementally create a significantly 9 | more complex behaviour tree for a robotics scenario (ROS2 knowledge not needed). 10 | 11 | .. _`ROS2 Robotics Tutorials`: https://py-trees-ros-tutorials.readthedocs.io/en/release-2.0.x/tutorials.html 12 | 13 | .. _background-section: 14 | 15 | Background 16 | ---------- 17 | 18 | .. note:: 19 | 20 | Behaviour trees are a decision making engine often used in the gaming and robotics industries. 21 | 22 | Other decision making engines include hierarchical finite state machines, task networks, and scripting 23 | engines, all of which have various pros and cons. Behaviour trees sit somewhere in the middle 24 | of these allowing a bend of purposeful planning towards goals with enough reactivity 25 | to shift in the presence of important events. Some standout features: 26 | 27 | * **Ticking** - the ability to :term:`tick` allows for work between executions without multi-threading 28 | * **Priority Handling** - switching mechanisms that allow higher priority interruptions is very natural 29 | * **Simplicity** - very few core components, making it easy for designers to work with it 30 | * **Scalable** - do not suffer from combinatorial explosion as nodes increase (as state machines do) 31 | * **Dynamic** - change the graph on the fly, between ticks or from parent behaviours themselves 32 | 33 | In some texts, 'priority handling' is often referred to as 'reactivity'. There's much 34 | information already covering behaviour trees, in particulary you may like to get started with: 35 | 36 | * `Introduction to Behavior Trees`_ - a gentle, practical introduction (2021) 37 | * `Simon Jones' Thesis`_ (Ch3) - a computer scientist's attention to detail, (2020) 38 | 39 | Other readings are listed at the bottom of this page. 40 | 41 | .. _motivation-section: 42 | 43 | Motivation 44 | ---------- 45 | 46 | The use case that drove the early development of py_trees was robotics. In particular, the higher level 47 | decision making for a single robot, i.e. the scenario / application layer. For example, the scenario 48 | that enables a robot to navigate through a building to deliver a parcel and return to it's 49 | homebase safely. 50 | 51 | In scope was any decision making that did not need a low-latency response (e.g. reactive safety 52 | controls). This included docking/undocking processes, the initial localisation dance, 53 | topological path planning, navigation context switching, LED and sound interactions, elevator 54 | entry/exit decisions. 55 | 56 | Also driving requirements was the need to offload scenario development to non-control engineers 57 | (juniors, interns, SWE's) and ensure they could develop and debug as rapidly as possible. 58 | 59 | Behaviour trees turned out to be a perfect fit after attempts with finite state machines 60 | became entangled in wiring complexity as the problem grew in scope. 61 | 62 | .. _design-section: 63 | 64 | Design 65 | ------ 66 | 67 | The requirements for the previously discussed robotics use case match that of the more general: 68 | 69 | .. note:: **Rapid development** of **medium scale** decision engines that do **not need to be real time reactive**. 70 | 71 | **Rapid Development**: Python was chosen as the language of choice since it enables a faster a cycle of development as 72 | well as a shorter learning curve (critical if you would like to shift the burden away from c++ control engineers 73 | to juniors/interns/software engineers). 74 | 75 | **Medium Scale**: Robotic scenarios for a single robot tend to be, maximally in the order of hundreds of behaviours. This is 76 | in contrast to game NPC's which need to be executing thousands of behaviours and/or trees and consequently, frequently 77 | run into problems of scale. This tends to influence the language of choice (c++) and the tree design. Our requirements 78 | are somewhat more modest, so this permits some flexibility in the design, e.g. python as a language of choice. 79 | 80 | **Not Real Time Reactive**: If low latency control measures, particularly for safety are needed, they are best handled 81 | directly inside the control layer, or even better, at an embedded level. This is not dissimilar to the way the 82 | human nervous system operates. All other decision making needs only to operate at a latency of ~50-200ms 83 | to negate any discernable delay observed by humans interacting with the robot. 84 | 85 | .. hint:: 86 | 87 | If you wish to handle this requirement, you're likely looking for a c++ implementation with mechanisms 88 | for on-demand (not periodic) triggering of ticks to keep latency as minimal as can be. 89 | 90 | **Python**: This implementation uses all the whizbang tricks (generators, decorators) that python has to offer. 91 | Some design constraints that have been assumed to enable a practical, easy to use (and lock-free) framework: 92 | 93 | * No interaction or sharing of data between tree instances 94 | * No parallelisation of tree execution (only one behaviour initialising or executing at a time) 95 | 96 | .. _readings-section: 97 | 98 | Readings 99 | -------- 100 | 101 | * `Introduction to Behavior Trees`_ - a gentle, practical introduction (2021) 102 | * `Simon Jones' Thesis`_ - a computer scientist's treatise, nice attention to detail w.r.t. tree algorithms (2020, Ch.3) 103 | * `Behaviour Trees in Robotics and AI`_ - a complete text, many examples and comparisons with other approaches. 104 | * `Youtube - Second Generation of Behaviour Trees`_ - from a gaming expert, in depth c++ walkthrough (on github). 105 | * `A Curious Course on Coroutines and Concurrency`_ - generators and coroutines in python. 106 | 107 | Alternatives 108 | ------------ 109 | 110 | * `BehaviorTreeCPP`_ - a c++ open source implementation 111 | * `UE4 Behavior Trees`_ - a closed implementation for use with unreal engine 112 | 113 | .. _UE4 Behavior Trees: https://docs.unrealengine.com/4.26/en-US/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/ 114 | .. _BehaviorTreeCPP: https://github.com/BehaviorTree/BehaviorTree.CPP 115 | .. _Owyl: https://github.com/eykd/owyl 116 | .. _Youtube - Second Generation of Behaviour Trees: https://www.youtube.com/watch?v=n4aREFb3SsU 117 | .. _Introduction to Behavior Trees: https://roboticseabass.com/2021/05/08/introduction-to-behavior-trees/ 118 | .. _Behaviour Trees in Robotics and AI: https://btirai.github.io/index 119 | .. _A Curious Course on Coroutines and Concurrency: http://www.dabeaz.com/coroutines/Coroutines.pdf 120 | .. _Behaviour Designer: https://forum.unity3d.com/threads/behavior-designer-behavior-trees-for-everyone.227497/ 121 | .. _Simon Jones' Thesis: https://research-information.bris.ac.uk/ws/portalfiles/portal/225580708/simon_jones_thesis_final_accepted.pdf 122 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | .. _modules-section-label: 2 | 3 | Module API 4 | ========== 5 | 6 | py_trees 7 | -------- 8 | 9 | .. automodule:: py_trees 10 | :synopsis: are your behaviour trees misbehaving? 11 | 12 | py_trees.behaviour 13 | ------------------ 14 | 15 | .. automodule:: py_trees.behaviour 16 | :members: 17 | :show-inheritance: 18 | :synopsis: core template from which all behaviours are derived 19 | 20 | py_trees.behaviours 21 | ------------------- 22 | 23 | .. automodule:: py_trees.behaviours 24 | :members: 25 | :show-inheritance: 26 | :synopsis: library of useful behaviours 27 | 28 | py_trees.blackboard 29 | ------------------- 30 | 31 | .. automodule:: py_trees.blackboard 32 | :members: 33 | :special-members: 34 | :show-inheritance: 35 | :synopsis: shared data store and related behaviours 36 | 37 | py_trees.common 38 | --------------- 39 | 40 | .. automodule:: py_trees.common 41 | :synopsis: common definitions, methods and enumerations 42 | 43 | .. autoclass:: py_trees.common.Access 44 | :members: READ, WRITE, EXCLUSIVE_WRITE 45 | :show-inheritance: 46 | 47 | .. autoclass:: py_trees.common.BlackBoxLevel 48 | :members: BIG_PICTURE, COMPONENT, DETAIL, NOT_A_BLACKBOX 49 | :show-inheritance: 50 | 51 | .. autoclass:: py_trees.common.ClearingPolicy 52 | :members: ON_INITIALISE, ON_SUCCESS, NEVER 53 | :show-inheritance: 54 | 55 | .. autoclass:: py_trees.common.Duration 56 | :members: INFINITE, UNTIL_THE_BATTLE_OF_ALFREDO 57 | :show-inheritance: 58 | 59 | .. autoclass:: py_trees.common.Name 60 | :members: AUTO_GENERATED 61 | :show-inheritance: 62 | 63 | .. autoclass:: py_trees.common.ParallelPolicy 64 | :members: SuccessOnAll, SuccessOnOne, SuccessOnSelected 65 | 66 | .. autoclass:: py_trees.common.Status 67 | :members: SUCCESS, FAILURE, RUNNING, INVALID 68 | :show-inheritance: 69 | 70 | .. autoclass:: py_trees.common.VisibilityLevel 71 | :members: ALL, DETAIL, COMPONENT, BIG_PICTURE 72 | :show-inheritance: 73 | 74 | .. automethod:: py_trees.common.string_to_visibility_level 75 | 76 | .. _py-trees-composites-module: 77 | 78 | py_trees.composites 79 | ------------------- 80 | 81 | .. automodule:: py_trees.composites 82 | :members: 83 | :special-members: 84 | :show-inheritance: 85 | :synopsis: behaviours that have children 86 | 87 | py_trees.console 88 | ---------------- 89 | 90 | .. automodule:: py_trees.console 91 | :members: 92 | :synopsis: colour definitions and syntax highlighting for the console 93 | 94 | py_trees.decorators 95 | ------------------- 96 | 97 | .. automodule:: py_trees.decorators 98 | :members: 99 | :show-inheritance: 100 | :synopsis: hats for behaviours 101 | 102 | py_trees.display 103 | ---------------- 104 | 105 | .. automodule:: py_trees.display 106 | :members: 107 | :show-inheritance: 108 | :synopsis: visualising trees with dot graphs, strings or on stdout 109 | 110 | .. _py-trees-idioms-module: 111 | 112 | py_trees.idioms 113 | --------------- 114 | 115 | .. automodule:: py_trees.idioms 116 | :members: 117 | :special-members: 118 | :show-inheritance: 119 | :synopsis: creators of common behaviour tree patterns 120 | 121 | py_trees.meta 122 | ------------- 123 | 124 | .. automodule:: py_trees.meta 125 | :members: 126 | :special-members: 127 | :show-inheritance: 128 | :synopsis: factories for behaviours 129 | 130 | py_trees.timers 131 | --------------- 132 | 133 | .. automodule:: py_trees.timers 134 | :members: 135 | :special-members: 136 | :show-inheritance: 137 | :synopsis: timer related behaviours 138 | 139 | py_trees.trees 140 | -------------- 141 | 142 | .. automodule:: py_trees.trees 143 | :members: 144 | :show-inheritance: 145 | :synopsis: tree managers - they make your life easier! 146 | 147 | py_trees.utilities 148 | ------------------ 149 | 150 | .. automodule:: py_trees.utilities 151 | :members: 152 | :show-inheritance: 153 | :synopsis: assorted utility functions 154 | 155 | py_trees.visitors 156 | ----------------- 157 | 158 | .. automodule:: py_trees.visitors 159 | :members: 160 | :show-inheritance: 161 | :synopsis: entities that visit behaviours as a tree is traversed 162 | 163 | 164 | -------------------------------------------------------------------------------- /docs/programs.rst: -------------------------------------------------------------------------------- 1 | .. _py-trees-program-section: 2 | 3 | Programs 4 | ======== 5 | 6 | .. _py-trees-render: 7 | 8 | py-trees-render 9 | --------------- 10 | 11 | .. automodule:: py_trees.programs.render 12 | :synopsis: 13 | 14 | -------------------------------------------------------------------------------- /docs/terminology.rst: -------------------------------------------------------------------------------- 1 | .. _terminology-section: 2 | 3 | Terminology 4 | =========== 5 | 6 | .. glossary:: 7 | 8 | 9 | block 10 | blocking 11 | A behaviour is sometimes referred to as a 'blocking' behaviour. Technically, the execution 12 | of a behaviour should be non-blocking (i.e. the tick part), however when it's progress from 13 | 'RUNNING' to 'FAILURE/SUCCESS' takes more than one tick, we say that the behaviour itself 14 | is blocking. In short, `blocking == RUNNING`. 15 | 16 | data gathering 17 | Caching events, notifications, or incoming data arriving asynchronously on the blackboard. 18 | This is a fairly common practice for behaviour trees which exist inside a complex system. 19 | 20 | In most cases, data gathering is done either outside the tree, or at the front end of your 21 | tree under a parallel preceding the rest of the tree tick so that the ensuing behaviours 22 | work on a constant, consistent set of data. Even if the incoming data is not arriving 23 | asynchronously, this is useful conceptually and organisationally. 24 | 25 | fsm 26 | flying spaghetti monster 27 | Whilst a serious religous entity in his own right (see `pastafarianism`_), it's also 28 | very easy to imagine your code become a spiritual flying spaghetti monster if left 29 | unchecked:: 30 | 31 | _ _(o)_(o)_ _ 32 | ._\`:_ F S M _:' \_, 33 | / (`---'\ `-. 34 | ,-` _) (_, 35 | 36 | guard 37 | A guard is a behaviour at the start of a sequence that checks for a particular condition 38 | (e.g. is battery low?). If the check succeeds, then the door is opened to the rest of the 39 | work sequence. 40 | 41 | tick 42 | ticks 43 | ticking 44 | A key feature of behaviours and their trees is in the way they *tick*. A tick 45 | is merely an execution slice, similar to calling a function once, or executing 46 | a loop in a control program once. 47 | 48 | When a **behaviour** ticks, it is executing a small, non-blocking chunk of code 49 | that checks a variable or triggers/monitors/returns the result of an external action. 50 | 51 | When a **behaviour tree** ticks, it traverses the behaviours (starting at the root of 52 | the tree), ticking each behaviour, catching its result and then using that result to 53 | make decisions on the direction the tree traversal will take. This is the decision part 54 | of the tree. Once the traversal ends back at the root, the tick is over. 55 | 56 | Once a tick is done..you can stop for breath! In this space you can pause to avoid 57 | eating the cpu, send some statistics out to a monitoring program, manipulate the 58 | underlying blackboard (data), ... At no point does the traversal of the tree get mired in 59 | execution - it's just in and out and then stop for a coffee. This is absolutely awesome 60 | - without this it would be a concurrent mess of locks and threads. 61 | 62 | Always keep in mind that your behaviours' executions must be light. There is no 63 | parallelising here and your tick time needs to remain small. The tree should be solely 64 | about decision making, not doing any actual blocking work. Any blocking work should be 65 | happening somewhere else with a behaviour simply in charge of starting/monitoring and 66 | catching the result of that work. 67 | 68 | .. image:: images/ticking_tree.jpg 69 | :width: 300px 70 | :align: center 71 | 72 | 73 | .. _pastafarianism: http://www.venganza.org/ 74 | -------------------------------------------------------------------------------- /docs/the_crazy_hospital.rst: -------------------------------------------------------------------------------- 1 | .. _crazy-hospital-section: 2 | 3 | Surviving the Crazy Hospital 4 | ============================ 5 | 6 | Your behaviour trees are misbehaving or your subtree designs seem overly 7 | obtuse? This page can help you stay focused on what is important...staying out 8 | of the padded room. 9 | 10 | .. image:: images/crazy_hospital.jpg 11 | :width: 300px 12 | :align: center 13 | 14 | .. note:: 15 | Many of these guidelines we've evolved from trial and error and are almost 16 | entirely driven by a need to avoid a burgeoning complexity (aka 17 | :term:`flying spaghetti monster`). Feel free to experiment and provide us with 18 | your insights here as well! 19 | 20 | 21 | Behaviours 22 | ---------- 23 | 24 | * Keep the constructor minimal so you can instantiate the behaviour for offline rendering 25 | * Put hardware or other runtime specific initialisation in :meth:`~py_trees.behaviour.Behaviour.setup` 26 | * The :meth:`~py_trees.behaviour.Behaviour.update` method must be light and non-blocking so a tree can keep ticking over 27 | * Keep the scope of a single behaviour tight and focused, deploy larger reusable concepts as subtrees (idioms) 28 | 29 | Composites 30 | ---------- 31 | 32 | * Avoid creating new composites, this increases the decision complexity by an order of magnitude 33 | * Don't subclass merely to auto-populate it, build a :meth:`create__subtree` library instead 34 | 35 | Trees 36 | ----- 37 | 38 | * When designing your tree, stub them out with nonsense behaviours. 39 | Focus on descriptive names, composite types and render dot graphs 40 | to accelerate the design process (especially when collaborating). 41 | * Make sure your pre/post tick handlers and visitors are all very light. 42 | * A good tick-tock rate for higher level decision making is around 1-500ms. 43 | -------------------------------------------------------------------------------- /docs/trees.rst: -------------------------------------------------------------------------------- 1 | .. _trees-section: 2 | 3 | Trees 4 | ===== 5 | 6 | .. automodule:: py_trees.trees 7 | :noindex: 8 | 9 | .. _behaviour-tree-section: 10 | 11 | The Behaviour Tree 12 | ------------------ 13 | 14 | .. autoclass:: py_trees.trees.BehaviourTree 15 | :noindex: 16 | 17 | .. _skeleton-section: 18 | 19 | Skeleton 20 | -------- 21 | 22 | The most basic feature of the behaviour tree is it's automatic tick-tock. You can 23 | :meth:`~py_trees.trees.BehaviourTree.tick_tock` for a specific number of iterations, 24 | or indefinitely and use the :meth:`~py_trees.trees.BehaviourTree.interrupt` method to stop it. 25 | 26 | .. literalinclude:: examples/skeleton_tree.py 27 | :language: python 28 | :linenos: 29 | 30 | or create your own loop and tick at your own leisure with 31 | the :meth:`~py_trees.trees.BehaviourTree.tick` method. 32 | 33 | .. _pre-post-tick-handlers-section: 34 | 35 | Pre/Post Tick Handlers 36 | ---------------------- 37 | 38 | Pre and post tick handlers can be used to perform some activity on or with the tree 39 | immediately before and after ticking. This is mostly useful with the continuous 40 | :meth:`~py_trees.trees.BehaviourTree.tick_tock` mechanism. 41 | 42 | This is useful for a variety of purposes: 43 | 44 | * logging 45 | * doing introspection on the tree to make reports 46 | * extracting data from the blackboard 47 | * triggering on external conditions to modify the tree (e.g. new plan arrived) 48 | 49 | This can be done of course, without locking since the tree won't be ticking while these 50 | handlers run. This does however, mean that your handlers should be light. They will be 51 | consuming time outside the regular tick period. 52 | 53 | The :ref:`py-trees-demo-tree-stewardship-program` program demonstrates a very simple 54 | pre-tick handler that just prints a line to stdout notifying the user of the current run. 55 | The relevant code: 56 | 57 | .. literalinclude:: ../py_trees/demos/stewardship.py 58 | :language: python 59 | :linenos: 60 | :lines: 82-92 61 | :caption: pre-tick-handler-function 62 | 63 | .. literalinclude:: ../py_trees/demos/stewardship.py 64 | :language: python 65 | :linenos: 66 | :lines: 135-136 67 | :caption: pre-tick-handler-adding 68 | 69 | .. _visitors-section: 70 | 71 | Visitors 72 | -------- 73 | 74 | .. automodule:: py_trees.visitors 75 | :noindex: 76 | 77 | The :ref:`py-trees-demo-tree-stewardship-program` program demonstrates the two reference 78 | visitor implementations: 79 | 80 | * :class:`~py_trees.visitors.DebugVisitor` prints debug logging messages to stdout and 81 | * :class:`~py_trees.visitors.SnapshotVisitor` collects runtime data to be used by visualisations 82 | 83 | Adding visitors to a tree: 84 | 85 | .. code-block:: python 86 | 87 | behaviour_tree = py_trees.trees.BehaviourTree(root) 88 | behaviour_tree.visitors.append(py_trees.visitors.DebugVisitor()) 89 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 90 | behaviour_tree.visitors.append(snapshot_visitor) 91 | 92 | These visitors are automatically run inside the tree's :class:`~py_trees.trees.BehaviourTree.tick` method. 93 | The former immediately logs to screen, the latter collects information which is then used to display an 94 | ascii tree: 95 | 96 | .. code-block:: python 97 | 98 | behaviour_tree.tick() 99 | ascii_tree = py_trees.display.ascii_tree( 100 | behaviour_tree.root, 101 | snapshot_information=snapshot_visitor) 102 | ) 103 | print(ascii_tree) 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /docs/visualisation.rst: -------------------------------------------------------------------------------- 1 | .. _visualisation-section: 2 | 3 | Visualisation 4 | ============= 5 | 6 | .. automodule:: py_trees.display 7 | :noindex: 8 | 9 | .. _ascii-trees-section: 10 | 11 | Ascii/Unicode Trees 12 | ------------------- 13 | 14 | You can obtain an ascii/unicode art representation of the tree on stdout 15 | via :func:`py_trees.display.ascii_tree` or :func:`py_trees.display.unicode_tree`: 16 | 17 | .. autofunction:: py_trees.display.ascii_tree 18 | :noindex: 19 | 20 | .. _render-to-file-section: 21 | 22 | XHTML Trees 23 | ----------- 24 | 25 | Similarly, :func:`py_trees.display.xhtml_tree` generates a static or runtime 26 | representation of the tree as an embeddeble XHTML snippet. 27 | 28 | DOT Trees 29 | --------- 30 | 31 | **API** 32 | 33 | A static representation of the tree as a dot graph is obtained via 34 | :func:`py_trees.display.dot_tree`. Should you wish to render the dot graph to 35 | dot/png/svg images, make use of :meth:`py_trees.display.render_dot_tree`. Note that 36 | the dot graph representation does not generate runtime information for the tree 37 | (visited paths, status, ...). 38 | 39 | **Command Line Utility** 40 | 41 | You can also render any exposed method in your python packages that creates 42 | a tree and returns the root of the tree from the command line using the 43 | :ref:`py-trees-render` program. This is extremely useful when either designing your 44 | trees or auto-rendering dot graphs for documentation on CI. 45 | 46 | **Blackboxes and Visibility Levels** 47 | 48 | There is also an experimental feature that allows you to flag behaviours as 49 | blackboxes with multiple levels of granularity. This is purely for the 50 | purposes of showing different levels of detail in rendered dot graphs. 51 | A fullly rendered dot graph with hundreds of behaviours is not of much 52 | use when wanting to visualise the big picture. 53 | 54 | The :ref:`py-trees-demo-dot-graphs-program` program serves as a self-contained 55 | example of this feature. 56 | -------------------------------------------------------------------------------- /docs/weblinks.rst: -------------------------------------------------------------------------------- 1 | .. 2 | This file contains a common collection of web links. Include it 3 | wherever these links are needed. 4 | It is explicitly excluded in ``conf.py``, because it does not 5 | appear anywhere in the TOC tree. 6 | 7 | .. _`design notes`: https://forums.unrealengine.com/showthread.php?2004-Blackboard-Documentation 8 | .. _`operator module`: https://docs.python.org/2/library/operator.html 9 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | py_trees 5 | 6 | 2.3.0 7 | 8 | Pythonic implementation of behaviour trees. 9 | 10 | 11 | Daniel Stonier 12 | Michal Staniaszek 13 | Naveed Usmani 14 | 15 | Daniel Stonier 16 | Sebastian Castro 17 | 18 | BSD 19 | 20 | https://py-trees.readthedocs.io/en/devel/ 21 | https://github.com/splintered-reality/py_trees 22 | https://github.com/splintered-reality/py_trees/issues 23 | 24 | python3-setuptools 25 | 26 | python3-pydot 27 | 28 | 29 | ament_python 30 | 31 | 32 | -------------------------------------------------------------------------------- /py_trees/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """This is the top-level namespace of the py_trees package.""" 10 | 11 | ############################################################################## 12 | # Imports 13 | ############################################################################## 14 | 15 | # fmt: off 16 | from . import behaviour # usort:skip 17 | from . import behaviours # usort:skip 18 | from . import blackboard # usort:skip 19 | from . import common # usort:skip 20 | from . import composites # usort:skip 21 | from . import console # usort:skip 22 | from . import decorators # usort:skip 23 | from . import display # usort:skip 24 | from . import idioms # usort:skip 25 | from . import logging # usort:skip 26 | from . import meta # usort:skip 27 | from . import syntax_highlighting # usort:skip 28 | from . import tests # usort:skip 29 | from . import timers # usort:skip 30 | from . import trees # usort:skip 31 | from . import utilities # usort:skip 32 | from . import version # usort:skip 33 | from . import visitors # usort:skip 34 | 35 | from . import demos # usort:skip 36 | from . import programs # usort:skip 37 | # fmt: on 38 | -------------------------------------------------------------------------------- /py_trees/demos/README.md: -------------------------------------------------------------------------------- 1 | # Guidelines 2 | 3 | Each module here is a self-contained code sample for one of the demo scripts. 4 | That means there is a fair bit of copy and paste happening, but that is an 5 | intentional decision to ensure each demo script is self-contained and easy 6 | for beginners to follow and/or copy-paste from. 7 | 8 | Keep this in mind when adding additional programs. -------------------------------------------------------------------------------- /py_trees/demos/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """This package contains py_trees demo script code.""" 10 | 11 | ############################################################################## 12 | # Imports 13 | ############################################################################## 14 | 15 | # fmt: off 16 | from . import action # usort:skip # noqa: F401 17 | from . import blackboard # usort:skip # noqa: F401 18 | from . import blackboard_namespaces # usort:skip # noqa: F401 19 | from . import blackboard_remappings # usort:skip # noqa: F401 20 | from . import context_switching # usort:skip # noqa: F401 21 | from . import display_modes # usort:skip # noqa: F401 22 | from . import dot_graphs # usort:skip # noqa: F401 23 | from . import either_or # usort:skip # noqa: F401 24 | from . import lifecycle # usort:skip # noqa: F401 25 | from . import selector # usort:skip # noqa: F401 26 | from . import sequence # usort:skip # noqa: F401 27 | from . import stewardship # usort:skip # noqa: F401 28 | # fmt: on 29 | -------------------------------------------------------------------------------- /py_trees/demos/blackboard_namespaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.blackboard_namespaces 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-blackboard-namespaces 17 | 18 | .. figure:: images/blackboard_namespaces.png 19 | :align: center 20 | 21 | Console Screenshot 22 | """ 23 | 24 | ############################################################################## 25 | # Imports 26 | ############################################################################## 27 | 28 | import argparse 29 | import typing 30 | 31 | import py_trees 32 | import py_trees.console as console 33 | 34 | ############################################################################## 35 | # Classes 36 | ############################################################################## 37 | 38 | 39 | def description() -> str: 40 | """ 41 | Print description and usage information about the program. 42 | 43 | Returns: 44 | the program description string 45 | """ 46 | content = "Demonstrates usage of blackboard namespaces.\n" 47 | content += "\n" 48 | 49 | if py_trees.console.has_colours: 50 | banner_line = console.green + "*" * 79 + "\n" + console.reset 51 | s = banner_line 52 | s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset 53 | s += banner_line 54 | s += "\n" 55 | s += content 56 | s += "\n" 57 | s += banner_line 58 | else: 59 | s = content 60 | return s 61 | 62 | 63 | def epilog() -> typing.Optional[str]: 64 | """ 65 | Print a noodly epilog for --help. 66 | 67 | Returns: 68 | the noodly message 69 | """ 70 | if py_trees.console.has_colours: 71 | return ( 72 | console.cyan 73 | + "And his noodly appendage reached forth to tickle the blessed...\n" 74 | + console.reset 75 | ) 76 | else: 77 | return None 78 | 79 | 80 | def command_line_argument_parser() -> argparse.ArgumentParser: 81 | """ 82 | Process command line arguments. 83 | 84 | Returns: 85 | the argument parser 86 | """ 87 | parser = argparse.ArgumentParser( 88 | description=description(), 89 | epilog=epilog(), 90 | formatter_class=argparse.RawDescriptionHelpFormatter, 91 | ) 92 | return parser 93 | 94 | 95 | ############################################################################## 96 | # Main 97 | ############################################################################## 98 | 99 | 100 | def main() -> None: 101 | """Entry point for the demo script.""" 102 | _ = ( 103 | command_line_argument_parser().parse_args() 104 | ) # configuration only, no args to process 105 | print(description()) 106 | print( 107 | "-------------------------------------------------------------------------------" 108 | ) 109 | print("$ py_trees.blackboard.Client(name='Blackboard')") 110 | print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)") 111 | print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)") 112 | print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)") 113 | print( 114 | "-------------------------------------------------------------------------------" 115 | ) 116 | blackboard = py_trees.blackboard.Client(name="Blackboard") 117 | blackboard.register_key(key="dude", access=py_trees.common.Access.WRITE) 118 | blackboard.register_key(key="/dudette", access=py_trees.common.Access.WRITE) 119 | blackboard.register_key(key="/foo/bar/wow", access=py_trees.common.Access.WRITE) 120 | print(blackboard) 121 | print( 122 | "-------------------------------------------------------------------------------" 123 | ) 124 | print("$ blackboard.dude = 'Bob'") 125 | print("$ blackboard.dudette = 'Jade'") 126 | print( 127 | "-------------------------------------------------------------------------------" 128 | ) 129 | blackboard.dude = "Bob" 130 | blackboard.dudette = "Jade" 131 | print(py_trees.display.unicode_blackboard()) 132 | print( 133 | "-------------------------------------------------------------------------------" 134 | ) 135 | print("$ blackboard.foo.bar.wow = 'foobar'") 136 | print( 137 | "-------------------------------------------------------------------------------" 138 | ) 139 | blackboard.foo.bar.wow = "foobar" 140 | print(py_trees.display.unicode_blackboard()) 141 | print( 142 | "-------------------------------------------------------------------------------" 143 | ) 144 | print("$ py_trees.blackboard.Client(name='Foo', namespace='foo')") 145 | print("$ foo.register_key(key='awesome', access=py_trees.common.Access.WRITE)") 146 | print("$ foo.register_key(key='/brilliant', access=py_trees.common.Access.WRITE)") 147 | print("$ foo.register_key(key='/foo/clever', access=py_trees.common.Access.WRITE)") 148 | print( 149 | "-------------------------------------------------------------------------------" 150 | ) 151 | foo = py_trees.blackboard.Client(name="Foo", namespace="foo") 152 | foo.register_key(key="awesome", access=py_trees.common.Access.WRITE) 153 | # TODO: should /brilliant be namespaced or go directly to root? 154 | foo.register_key(key="/brilliant", access=py_trees.common.Access.WRITE) 155 | # absolute names are ok, so long as they include the namespace 156 | foo.register_key(key="/foo/clever", access=py_trees.common.Access.WRITE) 157 | print(foo) 158 | print( 159 | "-------------------------------------------------------------------------------" 160 | ) 161 | print("$ foo.awesome = True") 162 | print("$ foo.set('/brilliant', False)") 163 | print("$ foo.clever = True") 164 | print( 165 | "-------------------------------------------------------------------------------" 166 | ) 167 | foo.awesome = True 168 | # Only accessable via set since it's not in the namespace 169 | foo.set("/brilliant", False) 170 | # This will fail since it looks for the namespaced /foo/brilliant key 171 | # foo.brilliant = False 172 | foo.clever = True 173 | print(py_trees.display.unicode_blackboard()) 174 | -------------------------------------------------------------------------------- /py_trees/demos/blackboard_remappings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.blackboard_remappings 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-blackboard-remappings 17 | 18 | .. figure:: images/blackboard_remappings.png 19 | :align: center 20 | 21 | Console Screenshot 22 | """ 23 | 24 | ############################################################################## 25 | # Imports 26 | ############################################################################## 27 | 28 | import argparse 29 | import typing 30 | 31 | import py_trees 32 | import py_trees.console as console 33 | 34 | ############################################################################## 35 | # Classes 36 | ############################################################################## 37 | 38 | 39 | def description() -> str: 40 | """ 41 | Print description and usage information about the program. 42 | 43 | Returns: 44 | the program description string 45 | """ 46 | content = "Demonstrates usage of blackbord remappings.\n" 47 | content += "\n" 48 | content += "Demonstration is via an exemplar behaviour making use of remappings..\n" 49 | 50 | if py_trees.console.has_colours: 51 | banner_line = console.green + "*" * 79 + "\n" + console.reset 52 | s = banner_line 53 | s += console.bold_white + "Blackboard".center(79) + "\n" + console.reset 54 | s += banner_line 55 | s += "\n" 56 | s += content 57 | s += "\n" 58 | s += banner_line 59 | else: 60 | s = content 61 | return s 62 | 63 | 64 | def epilog() -> typing.Optional[str]: 65 | """ 66 | Print a noodly epilog for --help. 67 | 68 | Returns: 69 | the noodly message 70 | """ 71 | if py_trees.console.has_colours: 72 | return ( 73 | console.cyan 74 | + "And his noodly appendage reached forth to tickle the blessed...\n" 75 | + console.reset 76 | ) 77 | else: 78 | return None 79 | 80 | 81 | def command_line_argument_parser() -> argparse.ArgumentParser: 82 | """ 83 | Process command line arguments. 84 | 85 | Returns: 86 | the argument parser 87 | """ 88 | parser = argparse.ArgumentParser( 89 | description=description(), 90 | epilog=epilog(), 91 | formatter_class=argparse.RawDescriptionHelpFormatter, 92 | ) 93 | return parser 94 | 95 | 96 | class Remap(py_trees.behaviour.Behaviour): 97 | """Custom writer that submits a more complicated variable to the blackboard.""" 98 | 99 | def __init__(self, name: str, remap_to: typing.Dict[str, str]): 100 | """ 101 | Set up the blackboard and remap variables. 102 | 103 | Args: 104 | name: behaviour name 105 | remap_to: remappings (from variable name to variable name) 106 | """ 107 | super().__init__(name=name) 108 | self.logger.debug("%s.__init__()" % (self.__class__.__name__)) 109 | self.blackboard = self.attach_blackboard_client() 110 | self.blackboard.register_key( 111 | key="/foo/bar/wow", 112 | access=py_trees.common.Access.WRITE, 113 | remap_to=remap_to["/foo/bar/wow"], 114 | ) 115 | 116 | def update(self) -> py_trees.common.Status: 117 | """Write a dictionary to the blackboard. 118 | 119 | This beaviour always returns :data:`~py_trees.common.Status.SUCCESS`. 120 | """ 121 | self.logger.debug("%s.update()" % (self.__class__.__name__)) 122 | self.blackboard.foo.bar.wow = "colander" 123 | 124 | return py_trees.common.Status.SUCCESS 125 | 126 | 127 | ############################################################################## 128 | # Main 129 | ############################################################################## 130 | 131 | 132 | def main() -> None: 133 | """Entry point for the demo script.""" 134 | _ = ( 135 | command_line_argument_parser().parse_args() 136 | ) # configuration only, no arg processing 137 | print(description()) 138 | py_trees.logging.level = py_trees.logging.Level.DEBUG 139 | py_trees.blackboard.Blackboard.enable_activity_stream(maximum_size=100) 140 | root = Remap(name="Remap", remap_to={"/foo/bar/wow": "/parameters/wow"}) 141 | 142 | #################### 143 | # Execute 144 | #################### 145 | root.tick_once() 146 | print(root.blackboard) 147 | print(py_trees.display.unicode_blackboard()) 148 | print(py_trees.display.unicode_blackboard_activity_stream()) 149 | -------------------------------------------------------------------------------- /py_trees/demos/display_modes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.display_modes 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-display-modes 17 | 18 | .. figure:: images/display_modes.png 19 | :align: center 20 | 21 | Console Screenshot 22 | """ 23 | 24 | ############################################################################## 25 | # Imports 26 | ############################################################################## 27 | 28 | import argparse 29 | import itertools 30 | import typing 31 | 32 | import py_trees 33 | import py_trees.console as console 34 | 35 | ############################################################################## 36 | # Classes 37 | ############################################################################## 38 | 39 | 40 | def description() -> str: 41 | """ 42 | Print description and usage information about the program. 43 | 44 | Returns: 45 | the program description string 46 | """ 47 | content = "Demonstrates usage of the ascii/unicode display modes.\n" 48 | content += "\n" 49 | content += "...\n" 50 | content += "...\n" 51 | 52 | if py_trees.console.has_colours: 53 | banner_line = console.green + "*" * 79 + "\n" + console.reset 54 | s = banner_line 55 | s += console.bold_white + "Display Modes".center(79) + "\n" + console.reset 56 | s += banner_line 57 | s += "\n" 58 | s += content 59 | s += "\n" 60 | s += banner_line 61 | else: 62 | s = content 63 | return s 64 | 65 | 66 | def epilog() -> typing.Optional[str]: 67 | """ 68 | Print a noodly epilog for --help. 69 | 70 | Returns: 71 | the noodly message 72 | """ 73 | if py_trees.console.has_colours: 74 | return ( 75 | console.cyan 76 | + "And his noodly appendage reached forth to tickle the blessed...\n" 77 | + console.reset 78 | ) 79 | else: 80 | return None 81 | 82 | 83 | def command_line_argument_parser() -> argparse.ArgumentParser: 84 | """ 85 | Process command line arguments. 86 | 87 | Returns: 88 | the argument parser 89 | """ 90 | parser = argparse.ArgumentParser( 91 | description=description(), 92 | epilog=epilog(), 93 | formatter_class=argparse.RawDescriptionHelpFormatter, 94 | ) 95 | return parser 96 | 97 | 98 | def create_root() -> py_trees.behaviour.Behaviour: 99 | """ 100 | Create the tree to be ticked/displayed. 101 | 102 | Returns: 103 | the root of the tree 104 | """ 105 | root = py_trees.composites.Sequence(name="root", memory=True) 106 | child = py_trees.composites.Sequence(name="child1", memory=True) 107 | child2 = py_trees.composites.Sequence(name="child2", memory=True) 108 | child3 = py_trees.composites.Sequence(name="child3", memory=True) 109 | root.add_child(child) 110 | root.add_child(child2) 111 | root.add_child(child3) 112 | queue = [py_trees.common.Status.RUNNING] 113 | eventually = py_trees.common.Status.SUCCESS 114 | child.add_child( 115 | py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually) 116 | ) 117 | child2.add_child( 118 | py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually) 119 | ) 120 | child2_child1 = py_trees.composites.Sequence(name="Child2_child1", memory=True) 121 | child2_child1.add_child( 122 | py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually) 123 | ) 124 | child2.add_child(child2_child1) 125 | child3.add_child( 126 | py_trees.behaviours.StatusQueue(name="RS", queue=queue, eventually=eventually) 127 | ) 128 | return root 129 | 130 | 131 | ############################################################################## 132 | # Main 133 | ############################################################################## 134 | 135 | 136 | def main() -> None: 137 | """Entry point for the demo script.""" 138 | _ = ( 139 | command_line_argument_parser().parse_args() 140 | ) # configuration only, no args to process 141 | print(description()) 142 | print( 143 | "-------------------------------------------------------------------------------" 144 | ) 145 | print("$ py_trees.blackboard.Client(name='Blackboard')") 146 | print("$ foo.register_key(key='dude', access=py_trees.common.Access.WRITE)") 147 | print("$ foo.register_key(key='/dudette', access=py_trees.common.Access.WRITE)") 148 | print("$ foo.register_key(key='/foo/bar/wow', access=py_trees.common.Access.WRITE)") 149 | print( 150 | "-------------------------------------------------------------------------------" 151 | ) 152 | 153 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 154 | tree = py_trees.trees.BehaviourTree(create_root()) 155 | tree.add_visitor(snapshot_visitor) 156 | 157 | for tick in range(2): 158 | tree.tick() 159 | for show_visited, show_status in itertools.product( 160 | [False, True], [False, True] 161 | ): 162 | console.banner( 163 | "Tick {} / show_only_visited=={} / show_status=={}".format( 164 | tick, show_visited, show_status 165 | ) 166 | ) 167 | print( 168 | py_trees.display.unicode_tree( 169 | tree.root, 170 | show_status=show_status, 171 | show_only_visited=show_visited, 172 | visited=snapshot_visitor.visited, 173 | previously_visited=snapshot_visitor.previously_visited, 174 | ) 175 | ) 176 | print() 177 | -------------------------------------------------------------------------------- /py_trees/demos/lifecycle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | . argparse:: 14 | :module: py_trees.demos.lifecycle 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-behaviour-lifecycle 17 | 18 | .. image:: images/lifecycle.gif 19 | """ 20 | 21 | ############################################################################## 22 | # Imports 23 | ############################################################################## 24 | 25 | import argparse 26 | import time 27 | import typing 28 | 29 | import py_trees 30 | import py_trees.console as console 31 | 32 | ############################################################################## 33 | # Classes 34 | ############################################################################## 35 | 36 | 37 | def description() -> str: 38 | """ 39 | Print description and usage information about the program. 40 | 41 | Returns: 42 | the program description string 43 | """ 44 | content = "Demonstrates a typical day in the life of a behaviour.\n\n" 45 | content += ( 46 | "This behaviour will count from 1 to 3 and then reset and repeat. As it does\n" 47 | ) 48 | content += "so, it logs and displays the methods as they are called - construction, setup,\n" 49 | content += "initialisation, ticking and termination.\n" 50 | if py_trees.console.has_colours: 51 | banner_line = console.green + "*" * 79 + "\n" + console.reset 52 | s = banner_line 53 | s += ( 54 | console.bold_white + "Behaviour Lifecycle".center(79) + "\n" + console.reset 55 | ) 56 | s += banner_line 57 | s += "\n" 58 | s += content 59 | s += "\n" 60 | s += banner_line 61 | else: 62 | s = content 63 | return s 64 | 65 | 66 | def epilog() -> typing.Optional[str]: 67 | """ 68 | Print a noodly epilog for --help. 69 | 70 | Returns: 71 | the noodly message 72 | """ 73 | if py_trees.console.has_colours: 74 | return ( 75 | console.cyan 76 | + "And his noodly appendage reached forth to tickle the blessed...\n" 77 | + console.reset 78 | ) 79 | else: 80 | return None 81 | 82 | 83 | def command_line_argument_parser() -> argparse.ArgumentParser: 84 | """ 85 | Process command line arguments. 86 | 87 | Returns: 88 | the argument parser 89 | """ 90 | return argparse.ArgumentParser( 91 | description=description(), 92 | epilog=epilog(), 93 | formatter_class=argparse.RawDescriptionHelpFormatter, 94 | ) 95 | 96 | 97 | class Counter(py_trees.behaviour.Behaviour): 98 | """Simple counting behaviour. 99 | 100 | * Increments a counter from zero at each tick 101 | * Finishes with success if the counter reaches three 102 | * Resets the counter in the initialise() method. 103 | """ 104 | 105 | def __init__(self, name: str = "Counter"): 106 | """Configure the name of the behaviour.""" 107 | super(Counter, self).__init__(name) 108 | self.logger.debug("%s.__init__()" % (self.__class__.__name__)) 109 | 110 | def setup(self, **kwargs: int) -> None: 111 | """No delayed initialisation required for this example.""" 112 | self.logger.debug("%s.setup()" % (self.__class__.__name__)) 113 | 114 | def initialise(self) -> None: 115 | """Reset a counter variable.""" 116 | self.logger.debug("%s.initialise()" % (self.__class__.__name__)) 117 | self.counter = 0 118 | 119 | def update(self) -> py_trees.common.Status: 120 | """Increment the counter and decide on a new status.""" 121 | self.counter += 1 122 | new_status = ( 123 | py_trees.common.Status.SUCCESS 124 | if self.counter == 3 125 | else py_trees.common.Status.RUNNING 126 | ) 127 | if new_status == py_trees.common.Status.SUCCESS: 128 | self.feedback_message = ( 129 | "counting...{0} - phew, thats enough for today".format(self.counter) 130 | ) 131 | else: 132 | self.feedback_message = "still counting" 133 | self.logger.debug( 134 | "%s.update()[%s->%s][%s]" 135 | % (self.__class__.__name__, self.status, new_status, self.feedback_message) 136 | ) 137 | return new_status 138 | 139 | def terminate(self, new_status: py_trees.common.Status) -> None: 140 | """Nothing to clean up in this example.""" 141 | self.logger.debug( 142 | "%s.terminate()[%s->%s]" 143 | % (self.__class__.__name__, self.status, new_status) 144 | ) 145 | 146 | 147 | ############################################################################## 148 | # Main 149 | ############################################################################## 150 | 151 | 152 | def main() -> None: 153 | """Entry point for the demo script.""" 154 | command_line_argument_parser().parse_args() 155 | 156 | print(description()) 157 | 158 | py_trees.logging.level = py_trees.logging.Level.DEBUG 159 | 160 | counter = Counter() 161 | counter.setup() 162 | try: 163 | for _unused_i in range(0, 7): 164 | counter.tick_once() 165 | time.sleep(0.5) 166 | print("\n") 167 | except KeyboardInterrupt: 168 | print("") 169 | pass 170 | -------------------------------------------------------------------------------- /py_trees/demos/selector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.selector 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-selector 17 | 18 | .. graphviz:: dot/demo-selector.dot 19 | 20 | .. image:: images/selector.gif 21 | 22 | """ 23 | ############################################################################## 24 | # Imports 25 | ############################################################################## 26 | 27 | import argparse 28 | import sys 29 | import time 30 | import typing 31 | 32 | import py_trees 33 | import py_trees.console as console 34 | 35 | ############################################################################## 36 | # Classes 37 | ############################################################################## 38 | 39 | 40 | def description() -> str: 41 | """ 42 | Print description and usage information about the program. 43 | 44 | Returns: 45 | the program description string 46 | """ 47 | content = ( 48 | "Higher priority switching and interruption in the children of a selector.\n" 49 | ) 50 | content += "\n" 51 | content += "In this example the higher priority child is setup to fail initially,\n" 52 | content += "falling back to the continually running second child. On the third\n" 53 | content += ( 54 | "tick, the first child succeeds and cancels the hitherto running child.\n" 55 | ) 56 | if py_trees.console.has_colours: 57 | banner_line = console.green + "*" * 79 + "\n" + console.reset 58 | s = banner_line 59 | s += console.bold_white + "Selectors".center(79) + "\n" + console.reset 60 | s += banner_line 61 | s += "\n" 62 | s += content 63 | s += "\n" 64 | s += banner_line 65 | else: 66 | s = content 67 | return s 68 | 69 | 70 | def epilog() -> typing.Optional[str]: 71 | """ 72 | Print a noodly epilog for --help. 73 | 74 | Returns: 75 | the noodly message 76 | """ 77 | if py_trees.console.has_colours: 78 | return ( 79 | console.cyan 80 | + "And his noodly appendage reached forth to tickle the blessed...\n" 81 | + console.reset 82 | ) 83 | else: 84 | return None 85 | 86 | 87 | def command_line_argument_parser() -> argparse.ArgumentParser: 88 | """ 89 | Process command line arguments. 90 | 91 | Returns: 92 | the argument parser 93 | """ 94 | parser = argparse.ArgumentParser( 95 | description=description(), 96 | epilog=epilog(), 97 | formatter_class=argparse.RawDescriptionHelpFormatter, 98 | ) 99 | parser.add_argument( 100 | "-r", "--render", action="store_true", help="render dot tree to file" 101 | ) 102 | return parser 103 | 104 | 105 | def create_root() -> py_trees.behaviour.Behaviour: 106 | """ 107 | Create the root behaviour and it's subtree. 108 | 109 | Returns: 110 | the root behaviour 111 | """ 112 | root = py_trees.composites.Selector(name="Selector", memory=False) 113 | ffs = py_trees.behaviours.StatusQueue( 114 | name="FFS", 115 | queue=[ 116 | py_trees.common.Status.FAILURE, 117 | py_trees.common.Status.FAILURE, 118 | py_trees.common.Status.SUCCESS, 119 | ], 120 | eventually=py_trees.common.Status.SUCCESS, 121 | ) 122 | always_running = py_trees.behaviours.Running(name="Running") 123 | root.add_children([ffs, always_running]) 124 | return root 125 | 126 | 127 | ############################################################################## 128 | # Main 129 | ############################################################################## 130 | 131 | 132 | def main() -> None: 133 | """Entry point for the demo script.""" 134 | args = command_line_argument_parser().parse_args() 135 | print(description()) 136 | py_trees.logging.level = py_trees.logging.Level.DEBUG 137 | 138 | root = create_root() 139 | 140 | #################### 141 | # Rendering 142 | #################### 143 | if args.render: 144 | py_trees.display.render_dot_tree(root) 145 | sys.exit() 146 | 147 | #################### 148 | # Execute 149 | #################### 150 | root.setup_with_descendants() 151 | for i in range(1, 4): 152 | try: 153 | print("\n--------- Tick {0} ---------\n".format(i)) 154 | root.tick_once() 155 | print("\n") 156 | print(py_trees.display.unicode_tree(root=root, show_status=True)) 157 | time.sleep(1.0) 158 | except KeyboardInterrupt: 159 | break 160 | print("\n") 161 | -------------------------------------------------------------------------------- /py_trees/demos/sequence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A py_trees demo. 12 | 13 | .. argparse:: 14 | :module: py_trees.demos.sequence 15 | :func: command_line_argument_parser 16 | :prog: py-trees-demo-sequence 17 | 18 | .. graphviz:: dot/demo-sequence.dot 19 | 20 | .. image:: images/sequence.gif 21 | """ 22 | 23 | ############################################################################## 24 | # Imports 25 | ############################################################################## 26 | 27 | import argparse 28 | import sys 29 | import time 30 | import typing 31 | 32 | import py_trees 33 | import py_trees.console as console 34 | 35 | ############################################################################## 36 | # Classes 37 | ############################################################################## 38 | 39 | 40 | def description() -> str: 41 | """ 42 | Print description and usage information about the program. 43 | 44 | Returns: 45 | the program description string 46 | """ 47 | content = "Demonstrates sequences in action.\n\n" 48 | content += ( 49 | "A sequence is populated with 2-tick jobs that are allowed to run through to\n" 50 | ) 51 | content += "completion.\n" 52 | 53 | if py_trees.console.has_colours: 54 | banner_line = console.green + "*" * 79 + "\n" + console.reset 55 | s = banner_line 56 | s += console.bold_white + "Sequences".center(79) + "\n" + console.reset 57 | s += banner_line 58 | s += "\n" 59 | s += content 60 | s += "\n" 61 | s += banner_line 62 | else: 63 | s = content 64 | return s 65 | 66 | 67 | def epilog() -> typing.Optional[str]: 68 | """ 69 | Print a noodly epilog for --help. 70 | 71 | Returns: 72 | the noodly message 73 | """ 74 | if py_trees.console.has_colours: 75 | return ( 76 | console.cyan 77 | + "And his noodly appendage reached forth to tickle the blessed...\n" 78 | + console.reset 79 | ) 80 | else: 81 | return None 82 | 83 | 84 | def command_line_argument_parser() -> argparse.ArgumentParser: 85 | """ 86 | Process command line arguments. 87 | 88 | Returns: 89 | the argument parser 90 | """ 91 | parser = argparse.ArgumentParser( 92 | description=description(), 93 | epilog=epilog(), 94 | formatter_class=argparse.RawDescriptionHelpFormatter, 95 | ) 96 | parser.add_argument( 97 | "-r", "--render", action="store_true", help="render dot tree to file" 98 | ) 99 | return parser 100 | 101 | 102 | def create_root() -> py_trees.behaviour.Behaviour: 103 | """ 104 | Create the root behaviour and it's subtree. 105 | 106 | Returns: 107 | the root behaviour 108 | """ 109 | root = py_trees.composites.Sequence(name="Sequence", memory=True) 110 | for action in ["Action 1", "Action 2", "Action 3"]: 111 | rssss = py_trees.behaviours.StatusQueue( 112 | name=action, 113 | queue=[ 114 | py_trees.common.Status.RUNNING, 115 | py_trees.common.Status.SUCCESS, 116 | ], 117 | eventually=py_trees.common.Status.SUCCESS, 118 | ) 119 | root.add_child(rssss) 120 | return root 121 | 122 | 123 | ############################################################################## 124 | # Main 125 | ############################################################################## 126 | 127 | 128 | def main() -> None: 129 | """Entry point for the demo script.""" 130 | args = command_line_argument_parser().parse_args() 131 | print(description()) 132 | py_trees.logging.level = py_trees.logging.Level.DEBUG 133 | 134 | root = create_root() 135 | 136 | #################### 137 | # Rendering 138 | #################### 139 | if args.render: 140 | py_trees.display.render_dot_tree(root) 141 | sys.exit() 142 | 143 | #################### 144 | # Execute 145 | #################### 146 | root.setup_with_descendants() 147 | for i in range(1, 6): 148 | try: 149 | print("\n--------- Tick {0} ---------\n".format(i)) 150 | root.tick_once() 151 | print("\n") 152 | print(py_trees.display.unicode_tree(root=root, show_status=True)) 153 | time.sleep(1.0) 154 | except KeyboardInterrupt: 155 | break 156 | print("\n") 157 | -------------------------------------------------------------------------------- /py_trees/logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | A (very) simple logging module. 12 | 13 | .. module:: loggers 14 | :synopsis: Logging facilities in py_trees. 15 | 16 | Oh my spaghettified magnificence, 17 | Bless my noggin with a tickle from your noodly appendages! 18 | """ 19 | 20 | ############################################################################## 21 | # Imports 22 | ############################################################################## 23 | 24 | import enum 25 | import typing 26 | 27 | from . import console 28 | 29 | ############################################################################## 30 | # Logging 31 | ############################################################################## 32 | 33 | # I'd really prefer to use python logging facilities, but rospy logging 34 | # on top of python logging kills it. 35 | # 36 | # Could still use it here, and would actually be useful if I could 37 | # integrate it with testing, but for now, this will do. 38 | # Note, you can get colour with python logging, but its tricky; 39 | # 40 | # http://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output 41 | # 42 | # python way: 43 | # 44 | # import logging 45 | # logging.getLogger("py_trees.Behaviour") 46 | # logging.basicConfig(level=logging.DEBUG) 47 | # 48 | ############################################################################## 49 | # Level 50 | ############################################################################## 51 | 52 | 53 | # levels 54 | class Level(enum.IntEnum): 55 | """ 56 | An enumerator representing the logging level. 57 | 58 | Not valid if you override with your own loggers. 59 | """ 60 | 61 | DEBUG = 0 62 | INFO = 1 63 | WARN = 2 64 | ERROR = 3 65 | 66 | 67 | # module variable 68 | level = Level.INFO 69 | 70 | ############################################################################## 71 | # Logger Class 72 | ############################################################################## 73 | 74 | 75 | class Logger(object): 76 | """ 77 | Simple logger object. 78 | 79 | :cvar override: whether or not the default python logger has been overridden. 80 | :vartype override: bool 81 | """ 82 | 83 | def __init__(self, name: typing.Optional[str] = None): 84 | self.prefix = "{:<20}".format(name.replace("\n", " ")) + " : " if name else "" 85 | 86 | def debug(self, msg: str) -> None: 87 | """ 88 | Log a debug message. 89 | 90 | Args: 91 | msg: the message to log 92 | """ 93 | global level 94 | if level < Level.INFO: 95 | console.logdebug(self.prefix + msg) 96 | 97 | def info(self, msg: str) -> None: 98 | """ 99 | Log a message. 100 | 101 | Args: 102 | msg: the message to log 103 | """ 104 | global level 105 | if level < Level.WARN: 106 | console.loginfo(self.prefix + msg) 107 | 108 | def warning(self, msg: str) -> None: 109 | """ 110 | Log an warning message. 111 | 112 | Args: 113 | msg: the message to log 114 | """ 115 | global level 116 | if level < Level.ERROR: 117 | console.logwarn(self.prefix + msg) 118 | 119 | def error(self, msg: str) -> None: 120 | """ 121 | Log an error message. 122 | 123 | Args: 124 | msg: the message to log 125 | """ 126 | console.logerror(self.prefix + msg) 127 | -------------------------------------------------------------------------------- /py_trees/meta.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """Meta methods to create behaviours without creating behaviours themselves.""" 11 | 12 | ############################################################################## 13 | # Imports 14 | ############################################################################## 15 | 16 | import typing 17 | 18 | from . import behaviour, common 19 | 20 | ############################################################################## 21 | # Utility Methods 22 | ############################################################################## 23 | 24 | 25 | # BehaviourUpdateMethod = typing.Callable[[BehaviourSubClass], common.Status] 26 | # BehaviourUpdateMethod = typing.TypeVar( 27 | # 'BehaviourUpdateMethod', 28 | # bound=typing.Callable[[behaviour.BehaviourSubClass], common.Status] 29 | # ) 30 | 31 | BehaviourUpdateMethod = typing.TypeVar("BehaviourUpdateMethod", bound=typing.Callable) 32 | 33 | 34 | def create_behaviour_from_function( 35 | func: BehaviourUpdateMethod, module: typing.Optional[str] = None 36 | ) -> "typing.Type[behaviour.Behaviour]": 37 | """ 38 | Create a behaviour from the specified function. 39 | 40 | This takes the specified function and drops it in to serve as the 41 | the Behaviour :meth:`~py_trees.behaviour.Behaviour.update` method. 42 | 43 | The user provided fucntion must include the `self` 44 | argument and return a :class:`~py_trees.behaviours.common.Status` value. 45 | 46 | It also automatically registers a method for the :meth:`~py_trees.behaviour.Behaviour.terminate` 47 | method that clears the feedback message. Other methods are left untouched. 48 | 49 | Args: 50 | func: a drop-in for the :meth:`~py_trees.behaviour.Behaviour.update` method 51 | module: suppliment it with a __module__ name if required (otherwise it will default to 'abc.') 52 | """ 53 | class_name = func.__name__.capitalize() 54 | 55 | def init(self: behaviour.Behaviour, name: str = class_name) -> None: 56 | behaviour.Behaviour.__init__(self, name=name) 57 | 58 | def terminate(self: behaviour.Behaviour, new_status: common.Status) -> None: 59 | if new_status == common.Status.INVALID: 60 | self.feedback_message = "" 61 | 62 | class_type = type( 63 | class_name, 64 | (behaviour.Behaviour,), 65 | dict(__init__=init, update=func, terminate=terminate), 66 | ) 67 | 68 | # When module is None, it will default to 'abc.' since behaviour.Behaviour is an ABC. 69 | # If that does matter (e.g. you're creating a class for an actual module, not a script), then 70 | # use the module argument. NB: this is better than relying on magic inspect methods that aren't 71 | # consistently available across different python implementations. 72 | if module is not None: 73 | class_type.__module__ = module 74 | return class_type 75 | -------------------------------------------------------------------------------- /py_trees/programs/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """This package contains py_trees program script code.""" 10 | 11 | ############################################################################## 12 | # Imports 13 | ############################################################################## 14 | 15 | from . import render # noqa 16 | -------------------------------------------------------------------------------- /py_trees/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splintered-reality/py_trees/94afed8cb7aeef5fefa41b8e7b5bc2f4a52e8e51/py_trees/py.typed -------------------------------------------------------------------------------- /py_trees/syntax_highlighting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | Syntax highlighting hints for core py_tree types/enums. 12 | 13 | .. module:: syntax_highlighting 14 | :synopsis: Syntax highlighting for various py_trees objects 15 | 16 | Oh my spaghettified magnificence, 17 | Bless my noggin with a tickle from your noodly appendages! 18 | """ 19 | 20 | ############################################################################## 21 | # Imports 22 | ############################################################################## 23 | 24 | from . import common, console 25 | 26 | ############################################################################## 27 | # Syntax Highlighting 28 | ############################################################################## 29 | 30 | 31 | _status_colour_strings = { 32 | common.Status.SUCCESS: console.green + str(common.Status.SUCCESS) + console.reset, 33 | common.Status.RUNNING: console.blue + str(common.Status.RUNNING) + console.reset, 34 | common.Status.FAILURE: console.red + str(common.Status.FAILURE) + console.reset, 35 | common.Status.INVALID: console.yellow + str(common.Status.INVALID) + console.reset, 36 | } 37 | 38 | _status_colour_codes = { 39 | common.Status.SUCCESS: console.green, 40 | common.Status.RUNNING: console.cyan, 41 | common.Status.FAILURE: console.red, 42 | common.Status.INVALID: console.yellow, 43 | } 44 | 45 | 46 | def status(status: common.Status) -> str: 47 | """ 48 | Retrieve a coloured string representing a :py:class:`~py_trees.common.Status`. 49 | 50 | This is used for syntax highlighting in various modes. 51 | 52 | Args: 53 | status: behaviour status 54 | 55 | Returns: 56 | syntax highlighted string representation of the status 57 | """ 58 | return _status_colour_strings[status] 59 | 60 | 61 | def status_colour_code(status: common.Status) -> str: 62 | """ 63 | Retrieve the colour code associated with a :py:class:`~py_trees.common.Status`. 64 | 65 | This is used for syntax highlighting in various modes. 66 | 67 | Args: 68 | Status: behaviour status 69 | 70 | Returns: 71 | console colour code associated with the status 72 | """ 73 | return _status_colour_codes[status] 74 | -------------------------------------------------------------------------------- /py_trees/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """ 11 | Library of common methods for the tests. 12 | 13 | Oh my spaghettified magnificence, 14 | Bless my noggin with a tickle from your noodly appendages! 15 | """ 16 | 17 | ############################################################################## 18 | # Imports 19 | ############################################################################## 20 | 21 | import typing 22 | 23 | from . import behaviour, blackboard, console, display, trees, visitors 24 | 25 | ############################################################################## 26 | # Methods 27 | ############################################################################## 28 | 29 | 30 | def print_assert_banner() -> None: 31 | """Print an assertion banner on stdout to indicate asserts will ensue.""" 32 | print(console.green + "\n--------- Assertions ---------\n" + console.reset) 33 | 34 | 35 | AssertResultType = typing.TypeVar("AssertResultType") 36 | 37 | 38 | def print_assert_details( 39 | text: str, expected: AssertResultType, result: AssertResultType 40 | ) -> None: 41 | """ 42 | Pretty print the expected and actual results for an assertion. 43 | 44 | Args: 45 | text: human readable info about the assertion 46 | expected: expected result 47 | result: actual result 48 | """ 49 | print( 50 | console.green 51 | + text 52 | + "." * (70 - len(text)) 53 | + console.cyan 54 | + "{}".format(expected) 55 | + console.yellow 56 | + " [{}]".format(result) 57 | + console.reset 58 | ) 59 | 60 | 61 | def pre_tick_visitor(behaviour_tree: trees.BehaviourTree) -> None: 62 | """ 63 | Tree tick banner. 64 | 65 | Args: 66 | behavior_tree: unused 67 | """ 68 | print("\n--------- Run %s ---------\n" % behaviour_tree.count) 69 | 70 | 71 | def tick_tree( 72 | root: behaviour.Behaviour, 73 | from_tick: int, 74 | to_tick: int, 75 | *, 76 | visitors: typing.Optional[typing.List[visitors.VisitorBase]] = None, 77 | print_snapshot: bool = False, 78 | print_blackboard: bool = False 79 | ) -> None: 80 | """ 81 | Tick the tree for a specified # ticks and run a variety of debugging helpers. 82 | 83 | Args: 84 | root: the root of the tree to tick from 85 | from_tick: needed only to help provide accurate stdout information 86 | to_tick: with from_tick, used to determine the # ticks required 87 | visitors: a list of visitors to run on each tree tick 88 | print_snapshot: print ascii/unicode snapshots after each tick 89 | print_blackboard: display the blackboard and it's update after each tick 90 | """ 91 | if visitors is None: 92 | visitors = [] 93 | print( 94 | "\n================== Iteration {}-{} ==================\n".format( 95 | from_tick, to_tick 96 | ) 97 | ) 98 | for i in range(from_tick, to_tick + 1): 99 | for visitor in visitors: 100 | visitor.initialise() 101 | print(("\n--------- Run %s ---------\n" % i)) 102 | for node in root.tick(): 103 | for visitor in visitors: 104 | node.visit(visitor) 105 | if print_snapshot: 106 | print(console.green + "\nTree Snapshot" + console.reset) 107 | print(display.unicode_tree(root=root, show_status=True)) 108 | if print_blackboard: 109 | print(display.unicode_blackboard()) 110 | 111 | 112 | def clear_blackboard() -> None: 113 | """Clear the blackboard, useful between tests.""" 114 | blackboard.Blackboard.storage = {} 115 | blackboard.Blackboard.clients = {} 116 | blackboard.Blackboard.metadata = {} 117 | 118 | 119 | def print_summary(nodes: typing.List[behaviour.Behaviour]) -> None: 120 | """Print status details for a list of behaviours. 121 | 122 | Args: 123 | nodes: a list of behaviours to print information about. 124 | """ 125 | print("\n--------- Summary ---------\n") 126 | for node in nodes: 127 | print("%s" % node) 128 | -------------------------------------------------------------------------------- /py_trees/timers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """Time related behaviours.""" 11 | 12 | ############################################################################## 13 | # Imports 14 | ############################################################################## 15 | 16 | import time 17 | 18 | from . import behaviour, common 19 | 20 | ############################################################################## 21 | # Behaviours 22 | ############################################################################## 23 | 24 | 25 | class Timer(behaviour.Behaviour): 26 | """ 27 | A simple, blocking timer behaviour running off python time.time(). 28 | 29 | This behaviour is :py:data:`~py_trees.common.Status.RUNNING` until the timer 30 | runs out, at which point it is :data:`~py_trees.common.Status.SUCCESS`. This can be 31 | used in a wide variety of situations - pause, duration, timeout depending on how 32 | it is wired into the tree (e.g. pause in a sequence, duration/timeout in 33 | a parallel). 34 | 35 | The timer gets reset either upon entry (:meth:`~py_trees.behaviour.Behaviour.initialise`) 36 | if it hasn't already been set and gets cleared when it either runs out, or the behaviour is 37 | interrupted by a higher priority or parent cancelling it. 38 | 39 | Args: 40 | name: name of the behaviour 41 | duration: length of time to run (in seconds) 42 | 43 | Raises: 44 | TypeError: if the provided duration is not a real number 45 | 46 | .. note:: 47 | This succeeds the first time the behaviour is ticked **after** the expected 48 | finishing time. 49 | 50 | .. tip:: 51 | Use the :func:`~py_trees.decorators.RunningIsFailure` decorator if you need 52 | :data:`~py_trees.common.Status.FAILURE` until the timer finishes. 53 | """ 54 | 55 | def __init__(self, name: str = "Timer", duration: float = 5.0): 56 | super(Timer, self).__init__(name) 57 | if not isinstance(duration, (int, float)): 58 | raise TypeError( 59 | "Timer: duration should be int or float, but you passed in {}".format( 60 | type(duration) 61 | ) 62 | ) 63 | self.duration: float = duration 64 | self.finish_time: float = 0.0 65 | self.feedback_message: str = "duration set to '{0}'s".format(self.duration) 66 | 67 | def initialise(self) -> None: 68 | """Store the expected finishing time.""" 69 | self.logger.debug("%s.initialise()" % self.__class__.__name__) 70 | self.finish_time = time.time() + self.duration 71 | self.feedback_message = "configured to fire in '{0}' seconds".format( 72 | self.duration 73 | ) 74 | 75 | def update(self) -> common.Status: 76 | """ 77 | Check the timer and update the behaviour result accordingly. 78 | 79 | Returns: 80 | :data:`~py_trees.common.Status.RUNNING` until timer expires, then 81 | :data:`~py_trees.common.Status.SUCCESS`. 82 | 83 | """ 84 | self.logger.debug("%s.update()" % self.__class__.__name__) 85 | current_time = time.time() 86 | if current_time > self.finish_time: 87 | self.feedback_message = "timer ran out [{0}]".format(self.duration) 88 | return common.Status.SUCCESS 89 | else: 90 | # do not show the time, it causes the tree to be 'changed' every tick 91 | # and we don't want to spam visualisations with almost meaningless updates 92 | self.feedback_message = ( 93 | "still running" # (%s)" % (self.finish_time - current_time) 94 | ) 95 | return common.Status.RUNNING 96 | -------------------------------------------------------------------------------- /py_trees/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Documentation 8 | ############################################################################## 9 | 10 | """Assorted utility functions.""" 11 | 12 | ############################################################################## 13 | # Imports 14 | ############################################################################## 15 | 16 | from __future__ import annotations 17 | 18 | import multiprocessing 19 | import os 20 | import re 21 | import traceback 22 | import typing 23 | 24 | ############################################################################## 25 | # Python Helpers 26 | ############################################################################## 27 | 28 | C = typing.TypeVar("C", bound=typing.Callable) 29 | 30 | # TODO: This currently doesn't work well with mypy - dynamic typing 31 | # is not its thing. Need to find a way to make this work without 32 | # creating errors on the user side. In the docstring's example, usage 33 | # of the static 'counter' variable results in: 34 | # 35 | # error: "Callable[[], Any]" has no attribute "counter" [attr-defined] 36 | 37 | 38 | def static_variables(**kwargs: typing.Any) -> typing.Callable[[C], C]: 39 | """ 40 | Attach initialised static variables to a python method. 41 | 42 | .. code-block:: python 43 | 44 | @static_variables(counter=0) 45 | def foo(): 46 | foo.counter += 1 47 | print("Counter: {}".format(foo.counter)) 48 | """ 49 | 50 | def decorate(func: C) -> C: 51 | for k in kwargs: 52 | setattr(func, k, kwargs[k]) 53 | return func 54 | 55 | return decorate 56 | 57 | 58 | @static_variables(primitives={bool, str, int, float}) 59 | def is_primitive(incoming: typing.Any) -> bool: 60 | """ 61 | Check if an incoming argument is a primitive type with no esoteric accessors. 62 | 63 | That is, it has no class attributes or container style [] accessors. 64 | 65 | Args: 66 | incoming: the instance to check 67 | Returns: 68 | True or false, depending on the check against the reserved primitives 69 | """ 70 | return type(incoming) in is_primitive.primitives # type: ignore[attr-defined] 71 | 72 | 73 | def truncate(original: str, length: int) -> str: 74 | """ 75 | Provide an elided (...) version of a string if it is longer than desired. 76 | 77 | Args: 78 | original: string to elide 79 | length: constrain the elided string to this 80 | """ 81 | s = (original[: length - 3] + "...") if len(original) > length else original 82 | return s 83 | 84 | 85 | ############################################################################## 86 | # System Tools 87 | ############################################################################## 88 | 89 | 90 | class Process(multiprocessing.Process): 91 | """Convenience wrapper around multiprocessing.Process.""" 92 | 93 | def __init__(self, *args: typing.Any, **kwargs: typing.Any): 94 | multiprocessing.Process.__init__(self, *args, **kwargs) 95 | self._pconn, self._cconn = multiprocessing.Pipe() 96 | self._exception = None 97 | 98 | def run(self) -> None: 99 | """Start the process, handle exceptions if needed.""" 100 | try: 101 | multiprocessing.Process.run(self) 102 | self._cconn.send(None) 103 | except Exception as e: 104 | tb = traceback.format_exc() 105 | self._cconn.send((e, tb)) 106 | 107 | @property 108 | def exception(self) -> typing.Any: 109 | """ 110 | Check the connection, if there is an error, reflect it as an exception. 111 | 112 | Returns: 113 | The exception. 114 | """ 115 | if self._pconn.poll(): 116 | self._exception = self._pconn.recv() 117 | return self._exception 118 | 119 | 120 | def which(program: str) -> typing.Optional[str]: 121 | """ 122 | Call the command line 'which' tool (convenience wrapper). 123 | 124 | Args: 125 | program: name of the program to find. 126 | 127 | Returns: 128 | path to the program or None if it doesnt exist. 129 | """ 130 | 131 | def is_exe(fpath: str) -> bool: 132 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 133 | 134 | fpath, unused_fname = os.path.split(program) 135 | if fpath: 136 | if is_exe(program): 137 | return program 138 | else: 139 | for path in os.environ["PATH"].split(os.pathsep): 140 | path = path.strip('"') 141 | exe_file = os.path.join(path, program) 142 | if is_exe(exe_file): 143 | return exe_file 144 | 145 | return None 146 | 147 | 148 | def get_valid_filename(s: str) -> str: 149 | """ 150 | Clean up and style a string so that it can be used as a filename. 151 | 152 | This is valid only from the perspective of the py_trees package. It does 153 | place a few extra constraints on strings to keep string handling and 154 | manipulation complexities to a minimum so that sanity prevails. 155 | 156 | * Removes leading and trailing spaces 157 | * Convert other spaces and newlines to underscores 158 | * Remove anything that is not an alphanumeric, dash, underscore, or dot 159 | 160 | .. code-block:: python 161 | 162 | >>> utilities.get_valid_filename("john's portrait in 2004.jpg") 163 | 'johns_portrait_in_2004.jpg' 164 | 165 | Args: 166 | program (:obj:`str`): string to convert to a valid filename 167 | 168 | Returns: 169 | :obj:`str`: a representation of the specified string as a valid filename 170 | """ 171 | s = str(s).strip().lower().replace(" ", "_").replace("\n", "_") 172 | return re.sub(r"(?u)[^-\w.]", "", s) 173 | 174 | 175 | def get_fully_qualified_name(instance: object) -> str: 176 | """ 177 | Retrieve the fully qualified name of an object. 178 | 179 | For example, an instance of 180 | :class:`~py_trees.composites.Sequence` becomes 'py_trees.composites.Sequence'. 181 | 182 | Args: 183 | instance (:obj:`object`): an instance of any class 184 | 185 | Returns: 186 | :obj:`str`: the fully qualified name 187 | """ 188 | module = instance.__class__.__module__ 189 | # if there is no module, it will report builtin, get that 190 | # string via what should remain constant, the 'str' class 191 | # and check against that. 192 | builtin = str.__class__.__module__ 193 | if module is None or module == builtin: 194 | return instance.__class__.__name__ 195 | else: 196 | return module + "." + instance.__class__.__name__ 197 | -------------------------------------------------------------------------------- /py_trees/version.py: -------------------------------------------------------------------------------- 1 | # 2 | # License: BSD 3 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 4 | # 5 | ############################################################################## 6 | # Documentation 7 | ############################################################################## 8 | 9 | """Package version number.""" 10 | 11 | ############################################################################## 12 | # Version 13 | ############################################################################## 14 | 15 | # When changing, Also update setup.py and package.xml 16 | # TODO: use pkg_resources to fetch the version from setup.py 17 | __version__ = "2.3.0" 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "py_trees" 3 | version = "2.3.0" 4 | description = "pythonic implementation of behaviour trees" 5 | authors = ["Daniel Stonier", "Naveed Usmani", "Michal Staniaszek"] 6 | maintainers = ["Daniel Stonier ", "Sebastian Castro "] 7 | readme = "README.md" 8 | license = "BSD" 9 | homepage = "https://github.com/splintered-reality/py_trees" 10 | repository = "https://github.com/splintered-reality/py_trees" 11 | documentation = "https://py-trees.readthedocs.io/en/devel/" 12 | packages = [ 13 | { include = "py_trees" }, 14 | ] 15 | classifiers = [ 16 | 'Environment :: Console', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: BSD License', 19 | 'Programming Language :: Python', 20 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 21 | 'Topic :: Software Development :: Libraries' 22 | ] 23 | keywords=["py_trees", "py-trees", "behaviour-trees"] 24 | 25 | [tool.poetry.dependencies] 26 | python = ">=3.9,<4.0" 27 | pydot = ">=1.4" 28 | 29 | [tool.poetry.group.dev.dependencies] 30 | tox = ">=3.26" 31 | tox-poetry-installer = {extras = ["poetry"], version = ">=0.9.0"} 32 | pytest = [ 33 | { version = ">=7.1", python = ">=3.9,<4.0" } 34 | ] 35 | pytest-console-scripts = ">=1.3" 36 | pytest-cov = ">=3.0.0" # transitively depends on coverage[toml] 37 | 38 | [tool.poetry.group.format.dependencies] 39 | ufmt = ">=2.0" # black (style) + usort (import order) 40 | 41 | [tool.poetry.group.static.dependencies] 42 | mypy = ">=0.991" 43 | 44 | [tool.poetry.group.lint.dependencies] 45 | # strongly recommended 46 | flake8 = ">=5.0" # combines pyflakes (errors) & pycodestyle (pep8 style) 47 | flake8-docstrings = ">=1.6" # docstrings (integrates pydocstyle) 48 | darglint = ">=1.8" # checks docstrings match implementation 49 | # optional, these go above and beyond 50 | flake8-bugbear = ">=22.9" # bugs & design not strictly pep8 51 | 52 | [tool.poetry.group.docs.dependencies] 53 | sphinx = "<6" 54 | sphinx-rtd-theme = ">=1.1" # not yet ready for sphinx 6 55 | sphinx-argparse = ">=0.4" 56 | 57 | [tool.poetry.scripts] 58 | py-trees-render = "py_trees.programs.render:main" 59 | py-trees-demo-action-behaviour = "py_trees.demos.action:main" 60 | py-trees-demo-behaviour-lifecycle = "py_trees.demos.lifecycle:main" 61 | py-trees-demo-blackboard = "py_trees.demos.blackboard:main" 62 | py-trees-demo-blackboard-namespaces = "py_trees.demos.blackboard_namespaces:main" 63 | py-trees-demo-blackboard-remappings = "py_trees.demos.blackboard_remappings:main" 64 | py-trees-demo-context-switching = "py_trees.demos.context_switching:main" 65 | py-trees-demo-display-modes = "py_trees.demos.display_modes:main" 66 | py-trees-demo-dot-graphs = "py_trees.demos.dot_graphs:main" 67 | py-trees-demo-either-or = "py_trees.demos.either_or:main" 68 | py-trees-demo-eternal-guard = "py_trees.demos.eternal_guard:main" 69 | py-trees-demo-logging = "py_trees.demos.logging:main" 70 | py-trees-demo-pick-up-where-you-left-off = "py_trees.demos.pick_up_where_you_left_off:main" 71 | py-trees-demo-selector = "py_trees.demos.selector:main" 72 | py-trees-demo-sequence = "py_trees.demos.sequence:main" 73 | py-trees-demo-tree-stewardship = "py_trees.demos.stewardship:main" 74 | 75 | [build-system] 76 | requires = ["poetry-core>=1.0.0"] 77 | build-backend = "poetry.core.masonry.api" 78 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ################################################################################ 4 | # This is a minimal setup.py for enabling ROS builds. 5 | # 6 | # For all other modes of development, use poetry and pyproject.toml 7 | ################################################################################ 8 | 9 | from setuptools import find_packages, setup 10 | 11 | install_requires = ["setuptools", "pydot"] 12 | 13 | # Some duplication of properties in: 14 | # - setup.py, (ros / legacy) 15 | # - package.xml (ros) 16 | # - pyproject.toml (poetry) 17 | # - py_trees/version.py (common) 18 | # Keep them in sync. 19 | d = setup( 20 | name="py_trees", 21 | version="2.3.0", 22 | packages=find_packages(exclude=["tests*", "docs*"]), 23 | package_data={"py_trees": ["py.typed"]}, 24 | install_requires=install_requires, 25 | author="Daniel Stonier, Naveed Usmani, Michal Staniaszek", 26 | maintainer="Daniel Stonier , Sebastian Castro ", 27 | url="https://github.com/splintered-reality/py_trees", 28 | keywords="behaviour-trees", 29 | zip_safe=True, 30 | classifiers=[ 31 | "Environment :: Console", 32 | "Intended Audience :: Developers", 33 | "License :: OSI Approved :: BSD License", 34 | "Programming Language :: Python", 35 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 36 | "Topic :: Software Development :: Libraries", 37 | ], 38 | description="pythonic implementation of behaviour trees", 39 | long_description="A behaviour tree implementation for rapid development of small scale decision making engines that don't need to be real time reactive.", 40 | license="BSD", 41 | entry_points={ 42 | "console_scripts": [ 43 | "py-trees-render = py_trees.programs.render:main", 44 | "py-trees-demo-action-behaviour = py_trees.demos.action:main", 45 | "py-trees-demo-behaviour-lifecycle = py_trees.demos.lifecycle:main", 46 | "py-trees-demo-blackboard = py_trees.demos.blackboard:main", 47 | "py-trees-demo-blackboard-namespaces = py_trees.demos.blackboard_namespaces:main", 48 | "py-trees-demo-blackboard-remappings = py_trees.demos.blackboard_remappings:main", 49 | "py-trees-demo-context-switching = py_trees.demos.context_switching:main", 50 | "py-trees-demo-display-modes = py_trees.demos.display_modes:main", 51 | "py-trees-demo-dot-graphs = py_trees.demos.dot_graphs:main", 52 | "py-trees-demo-either-or = py_trees.demos.either_or:main", 53 | "py-trees-demo-eternal-guard = py_trees.demos.eternal_guard:main", 54 | "py-trees-demo-logging = py_trees.demos.logging:main", 55 | "py-trees-demo-pick-up-where-you-left-off = py_trees.demos.pick_up_where_you_left_off:main", 56 | "py-trees-demo-selector = py_trees.demos.selector:main", 57 | "py-trees-demo-sequence = py_trees.demos.sequence:main", 58 | "py-trees-demo-tree-stewardship = py_trees.demos.stewardship:main", 59 | ], 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Make sure you have an sourced the appropriate environment 4 | (virtual or colcon) before executing. 5 | 6 | # Executing Tests 7 | 8 | ```bash 9 | # run all tests in the current directory 10 | $ pytest-3 11 | 12 | # All tests with full stdout (-s / --capture=no) 13 | $ pytest-3 -s 14 | 15 | # A single test module 16 | $ pytest-3 -s test_alakazam.py 17 | 18 | # A single test 19 | $ pytest-3 -s test_action_clients.py::test_success 20 | 21 | # Using tox from the root dir 22 | $ tox -l # list runnable contexts 23 | $ tox # everything 24 | $ tox -e py38 # tests only 25 | $ tox -e flake8 # lint only 26 | ``` 27 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import unittest 3 | 4 | 5 | class ImportTest(unittest.TestCase): 6 | def test_import(self) -> None: 7 | """ 8 | This test serves to make the buildfarm happy in Python 3.12 and later. 9 | See https://github.com/colcon/colcon-core/issues/678 for more information. 10 | """ 11 | assert importlib.util.find_spec("py_trees") 12 | -------------------------------------------------------------------------------- /tests/profile_blackboard: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m cProfile -s cumtime -o blackboard.cprofile benchmark_blackboard.py 4 | pyprof2calltree -k -i blackboard.cprofile 5 | -------------------------------------------------------------------------------- /tests/test_composites.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import typing 12 | 13 | import py_trees 14 | import py_trees.console as console 15 | import pytest 16 | 17 | ############################################################################## 18 | # Logging Level 19 | ############################################################################## 20 | 21 | py_trees.logging.level = py_trees.logging.Level.DEBUG 22 | logger = py_trees.logging.Logger("Tests") 23 | 24 | ############################################################################## 25 | # Helpers 26 | ############################################################################## 27 | 28 | 29 | def assert_banner() -> None: 30 | print(console.green + "----- Asserts -----" + console.reset) 31 | 32 | 33 | AssertResultType = typing.TypeVar("AssertResultType") 34 | 35 | 36 | def assert_details( 37 | text: str, expected: AssertResultType, result: AssertResultType 38 | ) -> None: 39 | print( 40 | console.green 41 | + text 42 | + "." * (70 - len(text)) 43 | + console.cyan 44 | + "{}".format(expected) 45 | + console.yellow 46 | + " [{}]".format(result) 47 | + console.reset 48 | ) 49 | 50 | 51 | ############################################################################## 52 | # Tests 53 | ############################################################################## 54 | 55 | 56 | def test_replacing_children() -> None: 57 | console.banner("Replacing Children") 58 | parent = py_trees.composites.Sequence(name="Parent", memory=True) 59 | front = py_trees.behaviours.Success(name="Front") 60 | back = py_trees.behaviours.Success(name="Back") 61 | old_child = py_trees.behaviours.Success(name="Old Child") 62 | new_child = py_trees.behaviours.Success(name="New Child") 63 | parent.add_children([front, old_child, back]) 64 | print(py_trees.display.unicode_tree(parent, show_status=True)) 65 | parent.replace_child(old_child, new_child) 66 | print(py_trees.display.unicode_tree(parent, show_status=True)) 67 | print("\n--------- Assertions ---------\n") 68 | print("old_child.parent is None") 69 | assert old_child.parent is None 70 | print("new_child.parent is parent") 71 | assert new_child.parent is parent 72 | 73 | 74 | def test_removing_children() -> None: 75 | console.banner("Removing Children") 76 | parent = py_trees.composites.Sequence(name="Parent", memory=True) 77 | child = py_trees.behaviours.Success(name="Child") 78 | print("\n--------- Assertions ---------\n") 79 | print("child.parent is None after removing by instance") 80 | parent.add_child(child) 81 | parent.remove_child(child) 82 | assert child.parent is None 83 | print("child.parent is None after removing by id") 84 | parent.add_child(child) 85 | parent.remove_child_by_id(child.id) 86 | assert child.parent is None 87 | print("child.parent is None after removing all children") 88 | parent.add_child(child) 89 | parent.remove_all_children() 90 | assert child.parent is None 91 | 92 | 93 | def test_composite_add_child_exception() -> None: 94 | console.banner("Composite Add Child Exception - Invalid Type") 95 | root = py_trees.composites.Selector(name="Selector", memory=False) 96 | with pytest.raises(TypeError) as context: # if raised, context survives 97 | # intentional error - silence mypy 98 | root.add_child(5.0) # type: ignore[arg-type] 99 | py_trees.tests.print_assert_details("TypeError raised", "raised", "not raised") 100 | py_trees.tests.print_assert_details("TypeError raised", "yes", "yes") 101 | assert "TypeError" == context.typename 102 | py_trees.tests.print_assert_details( 103 | "Substring match", "behaviours", f"{context.value}" 104 | ) 105 | assert "behaviours" in str(context.value) 106 | 107 | 108 | def test_protect_against_multiple_parents() -> None: 109 | console.banner("Protect Against Multiple Parents") 110 | child = py_trees.behaviours.Success(name="Success") 111 | first_parent = py_trees.composites.Selector(name="Selector", memory=False) 112 | second_parent = py_trees.composites.Sequence(name="Sequence", memory=True) 113 | with pytest.raises(RuntimeError) as context: # if raised, context survives 114 | # Adding a child to two parents - expecting a RuntimeError 115 | for parent in [first_parent, second_parent]: 116 | parent.add_child(child) 117 | py_trees.tests.print_assert_details( 118 | "RuntimeError raised", "raised", "not raised" 119 | ) 120 | py_trees.tests.print_assert_details("RuntimeError raised", "yes", "yes") 121 | assert "RuntimeError" == context.typename 122 | py_trees.tests.print_assert_details("Substring match", "parent", f"{context.value}") 123 | assert "parent" in str(context.value) 124 | 125 | 126 | def test_remove_nonexistant_child() -> None: 127 | console.banner("Remove non-existant child") 128 | root = py_trees.composites.Sequence(name="Sequence", memory=True) 129 | child = py_trees.behaviours.Success(name="Success") 130 | root.add_child(child) 131 | non_existant_child = py_trees.behaviours.Success(name="Ooops") 132 | 133 | with pytest.raises(IndexError) as context: # if raised, context survives 134 | root.remove_child_by_id(non_existant_child.id) 135 | py_trees.tests.print_assert_details("IndexError raised", "raised", "not raised") 136 | py_trees.tests.print_assert_details("IndexError raised", "yes", "yes") 137 | assert "IndexError" == context.typename 138 | py_trees.tests.print_assert_details( 139 | "Substring match", "not found", f"{context.value}" 140 | ) 141 | assert "not found" in str(context.value) 142 | -------------------------------------------------------------------------------- /tests/test_console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | 13 | 14 | ############################################################################## 15 | # Tests 16 | ############################################################################## 17 | 18 | 19 | def test_correct_encode() -> None: 20 | assert py_trees.console.define_symbol_or_fallback("\u26A1", "a", "ascii") == "a" 21 | assert ( 22 | py_trees.console.define_symbol_or_fallback("\u26A1", "a", "utf-8") == "\u26A1" 23 | ) 24 | -------------------------------------------------------------------------------- /tests/test_either_or.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Imports 8 | ############################################################################## 9 | 10 | import operator 11 | import typing 12 | 13 | import py_trees 14 | 15 | ############################################################################## 16 | # Either Or 17 | ############################################################################## 18 | 19 | 20 | def create_root() -> typing.Tuple[ 21 | py_trees.behaviour.Behaviour, 22 | py_trees.behaviour.Behaviour, 23 | py_trees.behaviour.Behaviour, 24 | ]: 25 | trigger_one = py_trees.decorators.FailureIsRunning( 26 | name="FisR", child=py_trees.behaviours.SuccessEveryN(name="Joystick 1", n=4) 27 | ) 28 | trigger_two = py_trees.decorators.FailureIsRunning( 29 | name="FisR", child=py_trees.behaviours.SuccessEveryN(name="Joystick 2", n=7) 30 | ) 31 | enable_joystick_one = py_trees.behaviours.SetBlackboardVariable( 32 | name="Joy1 - Enabled", 33 | variable_name="joystick_one", 34 | variable_value="enabled", 35 | overwrite=True, 36 | ) 37 | enable_joystick_two = py_trees.behaviours.SetBlackboardVariable( 38 | name="Joy2 - Enabled", 39 | variable_name="joystick_two", 40 | variable_value="enabled", 41 | overwrite=True, 42 | ) 43 | reset_joystick_one = py_trees.behaviours.SetBlackboardVariable( 44 | name="Joy1 - Disabled", 45 | variable_name="joystick_one", 46 | variable_value="disabled", 47 | overwrite=True, 48 | ) 49 | reset_joystick_two = py_trees.behaviours.SetBlackboardVariable( 50 | name="Joy2 - Disabled", 51 | variable_name="joystick_two", 52 | variable_value="disabled", 53 | overwrite=True, 54 | ) 55 | task_one = py_trees.behaviours.TickCounter( 56 | name="Task 1", duration=2, completion_status=py_trees.common.Status.SUCCESS 57 | ) 58 | task_two = py_trees.behaviours.TickCounter( 59 | name="Task 2", duration=2, completion_status=py_trees.common.Status.SUCCESS 60 | ) 61 | idle = py_trees.behaviours.Running(name="Idle") 62 | either_or = py_trees.idioms.either_or( 63 | name="EitherOr", 64 | conditions=[ 65 | py_trees.common.ComparisonExpression( 66 | "joystick_one", "enabled", operator.eq 67 | ), 68 | py_trees.common.ComparisonExpression( 69 | "joystick_two", "enabled", operator.eq 70 | ), 71 | ], 72 | subtrees=[task_one, task_two], 73 | namespace="either_or", 74 | ) 75 | root = py_trees.composites.Parallel( 76 | name="Root", 77 | policy=py_trees.common.ParallelPolicy.SuccessOnAll(synchronise=False), 78 | ) 79 | reset = py_trees.composites.Sequence(name="Reset", memory=True) 80 | reset.add_children([reset_joystick_one, reset_joystick_two]) 81 | joystick_one_events = py_trees.composites.Sequence(name="Joy1 Events", memory=True) 82 | joystick_one_events.add_children([trigger_one, enable_joystick_one]) 83 | joystick_two_events = py_trees.composites.Sequence(name="Joy2 Events", memory=True) 84 | joystick_two_events.add_children([trigger_two, enable_joystick_two]) 85 | tasks = py_trees.composites.Selector(name="Tasks", memory=False) 86 | tasks.add_children([either_or, idle]) 87 | root.add_children([reset, joystick_one_events, joystick_two_events, tasks]) 88 | return (root, task_one, task_two) 89 | 90 | 91 | def test_basic_workflow() -> None: 92 | # same as py-trees-demo-idiom-either-or 93 | root, task_one, task_two = create_root() 94 | # tree = py_trees.trees.BehaviourTree(root=root) 95 | root.tick_once() 96 | # tree.tick() 97 | py_trees.tests.print_assert_banner() 98 | py_trees.tests.print_assert_details( 99 | text="Tick 1 - tasks not yet ticked", 100 | expected=py_trees.common.Status.INVALID, 101 | result=task_one.status, 102 | ) 103 | assert task_one.status == py_trees.common.Status.INVALID 104 | root.tick_once() 105 | root.tick_once() 106 | root.tick_once() 107 | py_trees.tests.print_assert_details( 108 | text="Tick 4 - task one running", 109 | expected=py_trees.common.Status.RUNNING, 110 | result=task_one.status, 111 | ) 112 | assert task_one.status == py_trees.common.Status.RUNNING 113 | root.tick_once() # type: ignore[unreachable] 114 | root.tick_once() 115 | py_trees.tests.print_assert_details( 116 | text="Tick 6 - task one finished", 117 | expected=py_trees.common.Status.SUCCESS, 118 | result=task_one.status, 119 | ) 120 | assert task_one.status == py_trees.common.Status.SUCCESS 121 | root.tick_once() 122 | py_trees.tests.print_assert_details( 123 | text="Tick 7 - task two starts", 124 | expected=py_trees.common.Status.RUNNING, 125 | result=task_two.status, 126 | ) 127 | assert task_two.status == py_trees.common.Status.RUNNING 128 | root.tick_once() 129 | py_trees.tests.print_assert_details( 130 | text="Tick 8 - task one ignored", 131 | expected=py_trees.common.Status.INVALID, 132 | result=task_one.status, 133 | ) 134 | assert task_one.status == py_trees.common.Status.INVALID 135 | root.tick_once() 136 | py_trees.tests.print_assert_details( 137 | text="Tick 7 - task two finished", 138 | expected=py_trees.common.Status.SUCCESS, 139 | result=task_two.status, 140 | ) 141 | assert task_two.status == py_trees.common.Status.SUCCESS 142 | -------------------------------------------------------------------------------- /tests/test_meta.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | import py_trees.console as console 13 | 14 | ############################################################################## 15 | # Logging Level 16 | ############################################################################## 17 | 18 | py_trees.logging.level = py_trees.logging.Level.DEBUG 19 | logger = py_trees.logging.Logger("Nosetest") 20 | 21 | ############################################################################## 22 | # Tests 23 | ############################################################################## 24 | 25 | 26 | def test_behaviour_from_function_naming() -> None: 27 | console.banner("Test Behaviour From Function Naming") 28 | 29 | def foo() -> py_trees.common.Status: 30 | return py_trees.common.Status.SUCCESS 31 | 32 | foo_instance = py_trees.meta.create_behaviour_from_function(foo)(name="Foo") 33 | success = py_trees.behaviours.Success(name="Success") 34 | named_success = py_trees.meta.create_behaviour_from_function( 35 | py_trees.behaviours.success 36 | )(name="Woohoo") 37 | 38 | print("\n--------- Assertions ---------\n") 39 | print("foo_instance.name = {} [Foo]".format(foo_instance.name)) 40 | assert foo_instance.name == "Foo" 41 | print("success.name = {}".format(success.name)) 42 | assert success.name == "Success" 43 | print("named_success.name == {} Woohoo".format(named_success.name)) 44 | assert named_success.name == "Woohoo" 45 | -------------------------------------------------------------------------------- /tests/test_pickup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | ############################################################################## 7 | # Imports 8 | ############################################################################## 9 | 10 | import py_trees 11 | import py_trees.console as console 12 | 13 | ############################################################################## 14 | # Logging Level 15 | ############################################################################## 16 | 17 | py_trees.logging.level = py_trees.logging.Level.DEBUG 18 | logger = py_trees.logging.Logger("Nosetest") 19 | 20 | ############################################################################## 21 | # Tests 22 | ############################################################################## 23 | 24 | 25 | def test_high_priority_interrupt() -> None: 26 | console.banner("High Priority Interrupt") 27 | task_one = py_trees.behaviours.StatusQueue( 28 | name="Task 1", 29 | queue=[ 30 | py_trees.common.Status.RUNNING, 31 | py_trees.common.Status.RUNNING, 32 | ], 33 | eventually=py_trees.common.Status.SUCCESS, 34 | ) 35 | task_two = py_trees.behaviours.StatusQueue( 36 | name="Task 2", 37 | queue=[ 38 | py_trees.common.Status.RUNNING, 39 | py_trees.common.Status.RUNNING, 40 | ], 41 | eventually=py_trees.common.Status.SUCCESS, 42 | ) 43 | tasks = [task_one, task_two] 44 | high_priority_interrupt = py_trees.decorators.RunningIsFailure( 45 | name="High Priority", child=py_trees.behaviours.Periodic(name="Periodic", n=3) 46 | ) 47 | piwylo = py_trees.idioms.pick_up_where_you_left_off( 48 | name="Pick Up\nWhere You\nLeft Off", tasks=tasks 49 | ) 50 | root = py_trees.composites.Selector(name="Root", memory=False) 51 | root.add_children([high_priority_interrupt, piwylo]) 52 | 53 | print(py_trees.display.unicode_tree(root)) 54 | visitor = py_trees.visitors.DebugVisitor() 55 | py_trees.tests.tick_tree(root, 1, 3, visitors=[visitor]) 56 | print() 57 | 58 | print("\n--------- Assertions ---------\n") 59 | print("high_priority_interrupt.status == py_trees.common.Status.FAILURE") 60 | assert high_priority_interrupt.status == py_trees.common.Status.FAILURE 61 | print("piwylo.status == py_trees.common.Status.RUNNING") 62 | assert piwylo.status == py_trees.common.Status.RUNNING 63 | print("task_one.status == py_trees.common.Status.SUCCESS") 64 | assert task_one.status == py_trees.common.Status.SUCCESS 65 | print("task_two.status == py_trees.common.Status.RUNNING") 66 | assert task_two.status == py_trees.common.Status.RUNNING 67 | 68 | py_trees.tests.tick_tree(root, 4, 5, visitors=[visitor]) 69 | 70 | print("\n--------- Assertions ---------\n") 71 | print("high_priority_interrupt.status == py_trees.common.Status.SUCCESS") 72 | assert high_priority_interrupt.status == py_trees.common.Status.SUCCESS 73 | print("piwylo.status == py_trees.common.Status.INVALID") # type: ignore[unreachable] 74 | assert piwylo.status == py_trees.common.Status.INVALID 75 | print("task_one.status == py_trees.common.Status.INVALID") 76 | assert task_one.status == py_trees.common.Status.INVALID 77 | print("task_two.status == py_trees.common.Status.INVALID") 78 | assert task_two.status == py_trees.common.Status.INVALID 79 | 80 | py_trees.tests.tick_tree(root, 6, 8, visitors=[visitor]) 81 | 82 | print("\n--------- Assertions ---------\n") 83 | print("high_priority_interrupt.status == py_trees.common.Status.FAILURE") 84 | assert high_priority_interrupt.status == py_trees.common.Status.FAILURE 85 | print("piwylo.status == py_trees.common.Status.RUNNING") 86 | assert piwylo.status == py_trees.common.Status.RUNNING 87 | print("task_one.status == py_trees.common.Status.INVALID") 88 | assert task_one.status == py_trees.common.Status.INVALID 89 | print("task_two.status == py_trees.common.Status.RUNNING") 90 | assert task_two.status == py_trees.common.Status.RUNNING 91 | -------------------------------------------------------------------------------- /tests/test_probabilistic_behaviour.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | import py_trees.console as console 13 | import py_trees.tests 14 | import pytest 15 | 16 | ############################################################################## 17 | # Logging Level 18 | ############################################################################## 19 | 20 | py_trees.logging.level = py_trees.logging.Level.DEBUG 21 | logger = py_trees.logging.Logger("Tests") 22 | 23 | ############################################################################## 24 | # Tests 25 | ############################################################################## 26 | 27 | 28 | def test_probabilistic_behaviour_workflow() -> None: 29 | console.banner("Probabilistic Behaviour") 30 | 31 | with pytest.raises(ValueError) as context: # if raised, context survives 32 | # intentional error -> silence mypy 33 | unused_root = py_trees.behaviours.ProbabilisticBehaviour( # noqa: F841 [unused] 34 | name="ProbabilisticBehaviour", weights="invalid_type" # type: ignore[arg-type] 35 | ) 36 | py_trees.tests.print_assert_details("ValueError raised", "raised", "not raised") 37 | py_trees.tests.print_assert_details("ValueError raised", "yes", "yes") 38 | assert "ValueError" == context.typename 39 | 40 | root = py_trees.behaviours.ProbabilisticBehaviour( 41 | name="ProbabilisticBehaviour", weights=[0.0, 0.0, 1.0] 42 | ) 43 | 44 | py_trees.tests.print_assert_details( 45 | text="task not yet ticked", 46 | expected=py_trees.common.Status.INVALID, 47 | result=root.status, 48 | ) 49 | assert root.status == py_trees.common.Status.INVALID 50 | 51 | root.tick_once() 52 | py_trees.tests.print_assert_details( 53 | text="task ticked once", 54 | expected=py_trees.common.Status.RUNNING, 55 | result=root.status, 56 | ) 57 | assert root.status == py_trees.common.Status.RUNNING 58 | -------------------------------------------------------------------------------- /tests/test_timer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | import py_trees.console as console 13 | import py_trees.tests 14 | import pytest 15 | 16 | ############################################################################## 17 | # Logging Level 18 | ############################################################################## 19 | 20 | py_trees.logging.level = py_trees.logging.Level.DEBUG 21 | logger = py_trees.logging.Logger("Tests") 22 | 23 | ############################################################################## 24 | # Tests 25 | ############################################################################## 26 | 27 | 28 | def test_timer_invalid_duration() -> None: 29 | console.banner("Timer Exceptions - Invalid Duration") 30 | with pytest.raises(TypeError) as context: # if raised, context survives 31 | # intentional error -> silence mypy 32 | unused_timer = py_trees.timers.Timer( # noqa: F841 [unused] 33 | name="Timer", duration="invalid_type" # type: ignore[arg-type] 34 | ) 35 | py_trees.tests.print_assert_details("TypeError raised", "raised", "not raised") 36 | py_trees.tests.print_assert_details("TypeError raised", "yes", "yes") 37 | assert "TypeError" == context.typename 38 | py_trees.tests.print_assert_details( 39 | " substring match", "duration", f"{context.value}" 40 | ) 41 | assert "duration" in str(context.value) 42 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | import py_trees.console as console 13 | import py_trees.utilities as utilities 14 | 15 | ############################################################################## 16 | # Tests 17 | ############################################################################## 18 | 19 | 20 | def test_valid_filenames() -> None: 21 | console.banner("Valid Filenames") 22 | names = { 23 | "With\nNewlines": "with_newlines", 24 | "With Spaces": "with_spaces", 25 | " Leading Space": "leading_space", 26 | "Trailing Space ": "trailing_space", 27 | } 28 | print( 29 | console.green 30 | + "------------------ Assertions ------------------\n" 31 | + console.reset 32 | ) 33 | for name, expected_name in names.items(): 34 | print( 35 | console.cyan 36 | + repr(name) 37 | + ": " 38 | + console.yellow 39 | + expected_name 40 | + " [" 41 | + utilities.get_valid_filename(name) 42 | + "]" 43 | + console.reset 44 | ) 45 | assert utilities.get_valid_filename(name) == expected_name 46 | 47 | 48 | def test_get_fully_qualified_name() -> None: 49 | console.banner("Fully Qualified Names") 50 | pairs = { 51 | "py_trees.behaviours.Periodic": py_trees.behaviours.Periodic(name="foo", n=3), 52 | "py_trees.decorators.Inverter": py_trees.decorators.Inverter( 53 | name="Inverter", child=py_trees.behaviours.Success(name="Success") 54 | ), 55 | "py_trees.behaviours.Success": py_trees.behaviours.Success(name="Success"), 56 | } 57 | print( 58 | console.green 59 | + "------------------ Assertions ------------------\n" 60 | + console.reset 61 | ) 62 | for expected_name, object_instance in pairs.items(): 63 | print( 64 | console.cyan 65 | + expected_name 66 | + console.white 67 | + " == " 68 | + console.yellow 69 | + utilities.get_fully_qualified_name(object_instance) 70 | + console.reset 71 | ) 72 | assert expected_name == utilities.get_fully_qualified_name(object_instance) 73 | -------------------------------------------------------------------------------- /tests/test_visitors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # License: BSD 4 | # https://raw.githubusercontent.com/splintered-reality/py_trees/devel/LICENSE 5 | # 6 | 7 | ############################################################################## 8 | # Imports 9 | ############################################################################## 10 | 11 | import py_trees 12 | import py_trees.console as console 13 | 14 | ############################################################################## 15 | # Logging Level 16 | ############################################################################## 17 | 18 | py_trees.logging.level = py_trees.logging.Level.DEBUG 19 | logger = py_trees.logging.Logger("Tests") 20 | 21 | 22 | ############################################################################## 23 | # Helpers 24 | ############################################################################## 25 | 26 | 27 | def create_fffrrs_repeat_status_queue(name: str) -> py_trees.behaviours.StatusQueue: 28 | return py_trees.behaviours.StatusQueue( 29 | name=name, 30 | queue=[ 31 | py_trees.common.Status.FAILURE, 32 | py_trees.common.Status.FAILURE, 33 | py_trees.common.Status.FAILURE, 34 | py_trees.common.Status.RUNNING, 35 | py_trees.common.Status.RUNNING, 36 | py_trees.common.Status.SUCCESS, 37 | ], 38 | eventually=None, 39 | ) 40 | 41 | 42 | ############################################################################## 43 | # Tests 44 | ############################################################################## 45 | 46 | 47 | def test_snapshot_visitor() -> None: 48 | console.banner("Snapshot Visitor") 49 | 50 | root = py_trees.composites.Selector(name="Selector", memory=False) 51 | a = create_fffrrs_repeat_status_queue(name="A") 52 | b = create_fffrrs_repeat_status_queue(name="B") 53 | c = py_trees.behaviours.StatusQueue( 54 | name="C", 55 | queue=[ 56 | py_trees.common.Status.RUNNING, 57 | py_trees.common.Status.RUNNING, 58 | py_trees.common.Status.RUNNING, 59 | ], 60 | eventually=py_trees.common.Status.SUCCESS, 61 | ) 62 | root.add_child(a) 63 | root.add_child(b) 64 | root.add_child(c) 65 | print(py_trees.display.unicode_tree(root)) 66 | 67 | debug_visitor = py_trees.visitors.DebugVisitor() 68 | snapshot_visitor = py_trees.visitors.SnapshotVisitor() 69 | 70 | for i, result in zip(range(1, 5), [True, False, False, True]): 71 | py_trees.tests.tick_tree( 72 | root, i, i, visitors=[debug_visitor, snapshot_visitor], print_snapshot=True 73 | ) 74 | print("--------- Assertions ---------\n") 75 | print("snapshot_visitor.changed == {}".format(result)) 76 | assert snapshot_visitor.changed is result 77 | 78 | print("Done") 79 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Tox Configuration 3 | ################################################################################ 4 | 5 | [constants] 6 | source_locations = py_trees tests docs/examples 7 | 8 | [tox] 9 | envlist = py310, py312, format, check, mypy310, mypy312 10 | 11 | ################################################################################ 12 | # PyTest 13 | ################################################################################ 14 | 15 | [testenv] 16 | require_locked_deps = true 17 | require_poetry = true 18 | locked_deps = 19 | pytest 20 | pytest-console-scripts 21 | pytest-cov 22 | commands = 23 | pytest -s tests/ 24 | pytest --cov 25 | 26 | [coverage:run] 27 | # ensure conditional branches are explored (important) 28 | # https://coverage.readthedocs.io/en/latest/branch.html#branch 29 | branch = true 30 | 31 | ###################################################################### 32 | # Ufmt (black + isort) 33 | ###################################################################### 34 | 35 | [testenv:format] 36 | description = Un-opinionated auto-formatting. 37 | locked_deps = 38 | ufmt 39 | commands = 40 | ufmt format {[constants]source_locations} 41 | 42 | ################################################################################ 43 | # Flake 8 44 | ################################################################################ 45 | 46 | [testenv:check] 47 | skip_install = true 48 | description = Formatting checks and linting (flake8 & ufmt check). 49 | locked_deps = 50 | darglint 51 | flake8 52 | flake8-docstrings 53 | ufmt 54 | commands = 55 | flake8 {[constants]source_locations} 56 | ufmt check {[constants]source_locations} 57 | 58 | 59 | ################################################################################ 60 | # Flake 8 Configuration 61 | # 62 | # Don't require docstrings, but parse them correctly if they are there. 63 | # 64 | # D100 Missing docstring in public module 65 | # D101 Missing docstring in public class 66 | # D102 Missing docstring in public method 67 | # D103 Missing docstring in public function 68 | # D105 Missing docstring in magic method 69 | # D107 Missing docstring in __init__ 70 | # 71 | # Jamming docstrings into a single line looks cluttered. 72 | # 73 | # D200 One-line docstring should fit on one line with quotes 74 | #f 75 | # Weakly prefer breaking before a binary operator, so suppress that warning. 76 | # See https://github.com/python/peps/commit/c59c4376ad233a62ca4b3a6060c81368bd21e85b 77 | # 78 | # W503 line break before binary operator 79 | # 80 | ################################################################################ 81 | 82 | [flake8] 83 | # Relax various checks in the tests dir 84 | # - D*** documentation (docstrings) 85 | # - S101 use of assert warning (bandit) 86 | # - F401 unused import (from . import in __init__.py files) 87 | per-file-ignores = 88 | tests/*: D, S101, 89 | py_trees/__init__.py: F401 90 | 91 | # Match black line lengths 92 | # max-line-length = 88 93 | max-line-length = 120 94 | 95 | # Avoid overly complex functions 96 | # NB: this option by default is off, recommend complexity is 10 97 | # https://en.wikipedia.org/wiki/Cyclomatic_complexity 98 | max-complexity: 15 99 | 100 | # darglint docstring matching implementation checks 101 | # - short: one liners are not checked 102 | # - long: one liners and descriptions without args/returns are not checked 103 | strictness = long 104 | docstring_style=sphinx 105 | 106 | # Relax some of the more annoying checks 107 | # - C901 have a couple of stubborn methods (TODO) 108 | # - D105 magic method docstrings 109 | # - D107 prefer to include init args in the class documentation 110 | # - W503 deprecated PEP8, pay attention to 504 instead 111 | # https://www.flake8rules.com/rules/W503.html 112 | # - W504 line break after operator (TODO) 113 | ignore = C901, D105, D107, W503 114 | 115 | ################################################################################ 116 | # Mypy 117 | ################################################################################ 118 | 119 | [testenv:mypy10] 120 | skip_install = true 121 | description = Static type checking against python 3.10. 122 | locked_deps = 123 | mypy 124 | pytest 125 | commands = 126 | mypy --config-file {toxinidir}/tox.ini --python-version 3.10 {[constants]source_locations} 127 | 128 | [testenv:mypy312] 129 | skip_install = true 130 | description = Static type checking against python 3.12. 131 | commands = 132 | mypy --config-file {toxinidir}/tox.ini --python-version 3.12 {[constants]source_locations} 133 | locked_deps = 134 | mypy 135 | pytest 136 | 137 | [mypy] 138 | # With no options you get light coverage on some basics. 139 | # In general, you'll want to go further. Even for new code, it's 140 | # a great idea to enforce typehints on func args and returns as 141 | # it immensively improves the readability and traceability of 142 | # your code (and catches bugs!). 143 | 144 | #################### 145 | # Good to have 146 | #################### 147 | 148 | # Typehint all function args and returns 149 | disallow_untyped_defs = True 150 | 151 | # Unneeded # type: ignore comments. 152 | warn_unused_ignores = True 153 | 154 | # Complain if a function doesn't return anything when it should 155 | warn_no_return = True 156 | 157 | #################### 158 | # Stricter Options 159 | #################### 160 | 161 | # Catch zombie code 162 | warn_unreachable = True 163 | 164 | # Type correctness between variables inside functions 165 | check_untyped_defs = True 166 | 167 | # Complain if a func isn't configured to return 'Any' 168 | warn_return_any = True 169 | 170 | # Check on casting operations 171 | warn_redundant_casts = True 172 | 173 | [mypy-pydot.*] 174 | ignore_missing_imports = True 175 | --------------------------------------------------------------------------------